From 7c51814e51a67c82391477125ddf360a8300d4e6 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 19 Aug 2021 17:22:07 +0300 Subject: InputWidget: Cursor behavior around invisible codepoints Emoji variation selectors would confuse cursor positioning and insertion behavior, breaking the assumption that each line ends with a single newline. --- src/ui/inputwidget.c | 87 ++++++++++++++++++++++++++++++++-------------------- src/ui/text.c | 7 +++-- 2 files changed, 59 insertions(+), 35 deletions(-) (limited to 'src/ui') diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 9f233345..2f0cabbb 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c @@ -360,35 +360,64 @@ static int endX_InputWidget_(const iInputWidget *d, int y) { return line->range.end - (isLastLine_InputWidget_(d, line) ? 0 : 1) - line->range.start; } +static iBool isCursorFocusable_Char_(iChar c) { + return !isDefaultIgnorable_Char(c) && + !isVariationSelector_Char(c) && + !isFitzpatrickType_Char(c); +} + +static iChar at_InputWidget_(const iInputWidget *d, iInt2 pos) { + if (pos.y >= 0 && pos.y < size_Array(&d->lines) && + pos.x >= 0 && pos.x <= endX_InputWidget_(d, pos.y)) { + iChar ch = 0; + decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), + constEnd_String(lineString_InputWidget_(d, pos.y)), + &ch); + return ch; + } + return ' '; +} + static iInt2 movedCursor_InputWidget_(const iInputWidget *d, iInt2 pos, int xDir, int yDir) { - iChar ch; - if (xDir < 0) { - if (pos.x == 0) { - if (pos.y > 0) { - pos.x = endX_InputWidget_(d, --pos.y); + iChar ch = 0; + int n = 0; + /* TODO: The cursor should never land on any combining codepoints either. */ + for (;;) { + if (xDir < 0) { + if (pos.x == 0) { + if (pos.y > 0) { + pos.x = endX_InputWidget_(d, --pos.y); + } } - } - else { - iAssert(pos.x > 0); - int n = decodePrecedingBytes_MultibyteChar(charPos_InputWidget_(d, pos), + else { + iAssert(pos.x > 0); + n = decodePrecedingBytes_MultibyteChar(charPos_InputWidget_(d, pos), cstr_String(lineString_InputWidget_(d, pos.y)), &ch); - pos.x -= n; - } - } - else if (xDir > 0) { - if (pos.x == endX_InputWidget_(d, pos.y)) { - if (pos.y < size_Array(&d->lines) - 1) { - pos.y++; - pos.x = 0; + pos.x -= n; + if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { + continue; + } } } - else { - int n = decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), + else if (xDir > 0) { + if (pos.x == endX_InputWidget_(d, pos.y)) { + if (pos.y < size_Array(&d->lines) - 1) { + pos.y++; + pos.x = 0; + } + } + else { + n = decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), constEnd_String(lineString_InputWidget_(d, pos.y)), &ch); - pos.x += n; + pos.x += n; + if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { + continue; + } + } } + break; } return pos; } @@ -1026,7 +1055,9 @@ static void insertRange_InputWidget_(iInputWidget *d, iRangecc range) { cstr_String(&line->text) + d->cursor.x, constEnd_String(&line->text) }); truncate_String(&line->text, d->cursor.x); - appendCStr_String(&line->text, "\n"); + if (!endsWith_String(&line->text, "\n")) { + appendCStr_String(&line->text, "\n"); + } insert_Array(&d->lines, ++d->cursor.y, &split); d->cursor.x = 0; } @@ -1059,6 +1090,8 @@ void setCursor_InputWidget(iInputWidget *d, iInt2 pos) { iAssert(!isEmpty_Array(&d->lines)); pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); d->cursor = pos; + iChar ch = at_InputWidget_(d, pos); + printf("cursor x:%d ch:%08x (%lc)\n", pos.x, ch, (int)ch); /* Update selection. */ if (isMarking_()) { if (isEmpty_Range(&d->mark)) { @@ -1221,18 +1254,6 @@ static iBool deleteMarked_InputWidget_(iInputWidget *d) { return iFalse; } -static iChar at_InputWidget_(const iInputWidget *d, iInt2 pos) { - if (pos.y >= 0 && pos.y < size_Array(&d->lines) && - pos.x >= 0 && pos.x <= endX_InputWidget_(d, pos.y)) { - iChar ch = 0; - decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), - constEnd_String(lineString_InputWidget_(d, pos.y)), - &ch); - return ch; - } - return ' '; -} - static iBool isWordChar_InputWidget_(const iInputWidget *d, iInt2 pos) { return isAlphaNumeric_Char(at_InputWidget_(d, pos)); } diff --git a/src/ui/text.c b/src/ui/text.c index 0a8386e4..639d8f13 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -1433,6 +1433,7 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { iBool isFirst = iTrue; const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); const iBool checkHitChar = wrap && wrap->hitChar; + iBool wasCharHit = iFalse; while (!isEmpty_Range(&wrapRuns)) { if (isFirst) { isFirst = iFalse; @@ -1452,9 +1453,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { const iAttributedRun *run = at_Array(&attrText.runs, runIndex); if (run->flags.isLineBreak) { - if (checkHitChar) { + if (checkHitChar && !wasCharHit) { if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) { wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); + wasCharHit = iTrue; } } wrapPosRange.end = run->logical.start; @@ -1479,9 +1481,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { if (logPos < wrapPosRange.start || logPos >= wrapPosRange.end) { continue; } - if (checkHitChar) { + if (checkHitChar && !wasCharHit) { if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, logPos)) { wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); + wasCharHit = iTrue; } } /* Check if the hit point is on the left side of this line. */ -- cgit v1.2.3