diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-08-19 17:22:07 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-08-19 17:22:07 +0300 |
commit | 7c51814e51a67c82391477125ddf360a8300d4e6 (patch) | |
tree | 3dc5bd1d030d06ff5baab6832350555e5f19fd5b /src/ui | |
parent | 599be9cf0710f6f56133a8f5a9af37fb8ae6f013 (diff) |
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.
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/inputwidget.c | 87 | ||||
-rw-r--r-- | src/ui/text.c | 7 |
2 files changed, 59 insertions, 35 deletions
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) { | |||
360 | return line->range.end - (isLastLine_InputWidget_(d, line) ? 0 : 1) - line->range.start; | 360 | return line->range.end - (isLastLine_InputWidget_(d, line) ? 0 : 1) - line->range.start; |
361 | } | 361 | } |
362 | 362 | ||
363 | static iBool isCursorFocusable_Char_(iChar c) { | ||
364 | return !isDefaultIgnorable_Char(c) && | ||
365 | !isVariationSelector_Char(c) && | ||
366 | !isFitzpatrickType_Char(c); | ||
367 | } | ||
368 | |||
369 | static iChar at_InputWidget_(const iInputWidget *d, iInt2 pos) { | ||
370 | if (pos.y >= 0 && pos.y < size_Array(&d->lines) && | ||
371 | pos.x >= 0 && pos.x <= endX_InputWidget_(d, pos.y)) { | ||
372 | iChar ch = 0; | ||
373 | decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), | ||
374 | constEnd_String(lineString_InputWidget_(d, pos.y)), | ||
375 | &ch); | ||
376 | return ch; | ||
377 | } | ||
378 | return ' '; | ||
379 | } | ||
380 | |||
363 | static iInt2 movedCursor_InputWidget_(const iInputWidget *d, iInt2 pos, int xDir, int yDir) { | 381 | static iInt2 movedCursor_InputWidget_(const iInputWidget *d, iInt2 pos, int xDir, int yDir) { |
364 | iChar ch; | 382 | iChar ch = 0; |
365 | if (xDir < 0) { | 383 | int n = 0; |
366 | if (pos.x == 0) { | 384 | /* TODO: The cursor should never land on any combining codepoints either. */ |
367 | if (pos.y > 0) { | 385 | for (;;) { |
368 | pos.x = endX_InputWidget_(d, --pos.y); | 386 | if (xDir < 0) { |
387 | if (pos.x == 0) { | ||
388 | if (pos.y > 0) { | ||
389 | pos.x = endX_InputWidget_(d, --pos.y); | ||
390 | } | ||
369 | } | 391 | } |
370 | } | 392 | else { |
371 | else { | 393 | iAssert(pos.x > 0); |
372 | iAssert(pos.x > 0); | 394 | n = decodePrecedingBytes_MultibyteChar(charPos_InputWidget_(d, pos), |
373 | int n = decodePrecedingBytes_MultibyteChar(charPos_InputWidget_(d, pos), | ||
374 | cstr_String(lineString_InputWidget_(d, pos.y)), | 395 | cstr_String(lineString_InputWidget_(d, pos.y)), |
375 | &ch); | 396 | &ch); |
376 | pos.x -= n; | 397 | pos.x -= n; |
377 | } | 398 | if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { |
378 | } | 399 | continue; |
379 | else if (xDir > 0) { | 400 | } |
380 | if (pos.x == endX_InputWidget_(d, pos.y)) { | ||
381 | if (pos.y < size_Array(&d->lines) - 1) { | ||
382 | pos.y++; | ||
383 | pos.x = 0; | ||
384 | } | 401 | } |
385 | } | 402 | } |
386 | else { | 403 | else if (xDir > 0) { |
387 | int n = decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), | 404 | if (pos.x == endX_InputWidget_(d, pos.y)) { |
405 | if (pos.y < size_Array(&d->lines) - 1) { | ||
406 | pos.y++; | ||
407 | pos.x = 0; | ||
408 | } | ||
409 | } | ||
410 | else { | ||
411 | n = decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), | ||
388 | constEnd_String(lineString_InputWidget_(d, pos.y)), | 412 | constEnd_String(lineString_InputWidget_(d, pos.y)), |
389 | &ch); | 413 | &ch); |
390 | pos.x += n; | 414 | pos.x += n; |
415 | if (!isCursorFocusable_Char_(at_InputWidget_(d, pos))) { | ||
416 | continue; | ||
417 | } | ||
418 | } | ||
391 | } | 419 | } |
420 | break; | ||
392 | } | 421 | } |
393 | return pos; | 422 | return pos; |
394 | } | 423 | } |
@@ -1026,7 +1055,9 @@ static void insertRange_InputWidget_(iInputWidget *d, iRangecc range) { | |||
1026 | cstr_String(&line->text) + d->cursor.x, constEnd_String(&line->text) | 1055 | cstr_String(&line->text) + d->cursor.x, constEnd_String(&line->text) |
1027 | }); | 1056 | }); |
1028 | truncate_String(&line->text, d->cursor.x); | 1057 | truncate_String(&line->text, d->cursor.x); |
1029 | appendCStr_String(&line->text, "\n"); | 1058 | if (!endsWith_String(&line->text, "\n")) { |
1059 | appendCStr_String(&line->text, "\n"); | ||
1060 | } | ||
1030 | insert_Array(&d->lines, ++d->cursor.y, &split); | 1061 | insert_Array(&d->lines, ++d->cursor.y, &split); |
1031 | d->cursor.x = 0; | 1062 | d->cursor.x = 0; |
1032 | } | 1063 | } |
@@ -1059,6 +1090,8 @@ void setCursor_InputWidget(iInputWidget *d, iInt2 pos) { | |||
1059 | iAssert(!isEmpty_Array(&d->lines)); | 1090 | iAssert(!isEmpty_Array(&d->lines)); |
1060 | pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); | 1091 | pos.x = iClamp(pos.x, 0, endX_InputWidget_(d, pos.y)); |
1061 | d->cursor = pos; | 1092 | d->cursor = pos; |
1093 | iChar ch = at_InputWidget_(d, pos); | ||
1094 | printf("cursor x:%d ch:%08x (%lc)\n", pos.x, ch, (int)ch); | ||
1062 | /* Update selection. */ | 1095 | /* Update selection. */ |
1063 | if (isMarking_()) { | 1096 | if (isMarking_()) { |
1064 | if (isEmpty_Range(&d->mark)) { | 1097 | if (isEmpty_Range(&d->mark)) { |
@@ -1221,18 +1254,6 @@ static iBool deleteMarked_InputWidget_(iInputWidget *d) { | |||
1221 | return iFalse; | 1254 | return iFalse; |
1222 | } | 1255 | } |
1223 | 1256 | ||
1224 | static iChar at_InputWidget_(const iInputWidget *d, iInt2 pos) { | ||
1225 | if (pos.y >= 0 && pos.y < size_Array(&d->lines) && | ||
1226 | pos.x >= 0 && pos.x <= endX_InputWidget_(d, pos.y)) { | ||
1227 | iChar ch = 0; | ||
1228 | decodeBytes_MultibyteChar(charPos_InputWidget_(d, pos), | ||
1229 | constEnd_String(lineString_InputWidget_(d, pos.y)), | ||
1230 | &ch); | ||
1231 | return ch; | ||
1232 | } | ||
1233 | return ' '; | ||
1234 | } | ||
1235 | |||
1236 | static iBool isWordChar_InputWidget_(const iInputWidget *d, iInt2 pos) { | 1257 | static iBool isWordChar_InputWidget_(const iInputWidget *d, iInt2 pos) { |
1237 | return isAlphaNumeric_Char(at_InputWidget_(d, pos)); | 1258 | return isAlphaNumeric_Char(at_InputWidget_(d, pos)); |
1238 | } | 1259 | } |
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) { | |||
1433 | iBool isFirst = iTrue; | 1433 | iBool isFirst = iTrue; |
1434 | const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); | 1434 | const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); |
1435 | const iBool checkHitChar = wrap && wrap->hitChar; | 1435 | const iBool checkHitChar = wrap && wrap->hitChar; |
1436 | iBool wasCharHit = iFalse; | ||
1436 | while (!isEmpty_Range(&wrapRuns)) { | 1437 | while (!isEmpty_Range(&wrapRuns)) { |
1437 | if (isFirst) { | 1438 | if (isFirst) { |
1438 | isFirst = iFalse; | 1439 | isFirst = iFalse; |
@@ -1452,9 +1453,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1452 | for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { | 1453 | for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { |
1453 | const iAttributedRun *run = at_Array(&attrText.runs, runIndex); | 1454 | const iAttributedRun *run = at_Array(&attrText.runs, runIndex); |
1454 | if (run->flags.isLineBreak) { | 1455 | if (run->flags.isLineBreak) { |
1455 | if (checkHitChar) { | 1456 | if (checkHitChar && !wasCharHit) { |
1456 | if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) { | 1457 | if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, run->logical.start)) { |
1457 | wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); | 1458 | wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); |
1459 | wasCharHit = iTrue; | ||
1458 | } | 1460 | } |
1459 | } | 1461 | } |
1460 | wrapPosRange.end = run->logical.start; | 1462 | wrapPosRange.end = run->logical.start; |
@@ -1479,9 +1481,10 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) { | |||
1479 | if (logPos < wrapPosRange.start || logPos >= wrapPosRange.end) { | 1481 | if (logPos < wrapPosRange.start || logPos >= wrapPosRange.end) { |
1480 | continue; | 1482 | continue; |
1481 | } | 1483 | } |
1482 | if (checkHitChar) { | 1484 | if (checkHitChar && !wasCharHit) { |
1483 | if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, logPos)) { | 1485 | if (wrap->hitChar == sourcePtr_AttributedText_(&attrText, logPos)) { |
1484 | wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); | 1486 | wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); |
1487 | wasCharHit = iTrue; | ||
1485 | } | 1488 | } |
1486 | } | 1489 | } |
1487 | /* Check if the hit point is on the left side of this line. */ | 1490 | /* Check if the hit point is on the left side of this line. */ |