diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-05-14 07:24:32 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2021-05-14 07:24:32 +0300 |
commit | fe59111e9671fba8d1c64def9831716860ea5e44 (patch) | |
tree | fdb44222af394b4924ade2715205a813f1db2f1b /src/ui/inputwidget.c | |
parent | 713dcca5da21897fd8f5d7519112429273d7233e (diff) |
InputWidget: Fixed cursor moving; scroll the dialog
Up/down movement sometimes ended up in the wrong cursor position.
Now the nearest overflow-scrollable parent scrolls to keep the cursor visible.
Diffstat (limited to 'src/ui/inputwidget.c')
-rw-r--r-- | src/ui/inputwidget.c | 123 |
1 files changed, 83 insertions, 40 deletions
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 51447f21..6639f957 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c | |||
@@ -42,8 +42,12 @@ static const size_t maxUndo_InputWidget_ = 64; | |||
42 | 42 | ||
43 | static void enableEditorKeysInMenus_(iBool enable) { | 43 | static void enableEditorKeysInMenus_(iBool enable) { |
44 | #if defined (iPlatformAppleDesktop) | 44 | #if defined (iPlatformAppleDesktop) |
45 | enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); | 45 | enableMenuItemsByKey_MacOS(SDLK_LEFT, KMOD_PRIMARY, enable); |
46 | enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable); | 46 | enableMenuItemsByKey_MacOS(SDLK_RIGHT, KMOD_PRIMARY, enable); |
47 | enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY, enable); | ||
48 | enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY, enable); | ||
49 | enableMenuItemsByKey_MacOS(SDLK_UP, KMOD_PRIMARY | KMOD_SHIFT, enable); | ||
50 | enableMenuItemsByKey_MacOS(SDLK_DOWN, KMOD_PRIMARY | KMOD_SHIFT, enable); | ||
47 | #else | 51 | #else |
48 | iUnused(enable); | 52 | iUnused(enable); |
49 | #endif | 53 | #endif |
@@ -84,6 +88,7 @@ iDeclareType(InputLine) | |||
84 | 88 | ||
85 | struct Impl_InputLine { | 89 | struct Impl_InputLine { |
86 | size_t offset; /* character position from the beginning */ | 90 | size_t offset; /* character position from the beginning */ |
91 | size_t len; /* length as characters */ | ||
87 | iString text; /* UTF-8 */ | 92 | iString text; /* UTF-8 */ |
88 | }; | 93 | }; |
89 | 94 | ||
@@ -138,7 +143,23 @@ static void clearUndo_InputWidget_(iInputWidget *d) { | |||
138 | clear_Array(&d->undoStack); | 143 | clear_Array(&d->undoStack); |
139 | } | 144 | } |
140 | 145 | ||
146 | iLocalDef iInt2 padding_(void) { | ||
147 | return init_I2(gap_UI / 2, gap_UI / 2); | ||
148 | } | ||
149 | |||
150 | static iRect contentBounds_InputWidget_(const iInputWidget *d) { | ||
151 | const iWidget *w = constAs_Widget(d); | ||
152 | // const iRect widgetBounds = bounds_Widget(w); | ||
153 | iRect bounds = adjusted_Rect(bounds_Widget(w), | ||
154 | addX_I2(padding_(), d->leftPadding), | ||
155 | neg_I2(addX_I2(padding_(), d->rightPadding))); | ||
156 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); | ||
157 | bounds.pos.y += padding_().y / 2; | ||
158 | return bounds; | ||
159 | } | ||
160 | |||
141 | static void updateCursorLine_InputWidget_(iInputWidget *d) { | 161 | static void updateCursorLine_InputWidget_(iInputWidget *d) { |
162 | iWidget *w = as_Widget(d); | ||
142 | d->cursorLine = 0; | 163 | d->cursorLine = 0; |
143 | iConstForEach(Array, i, &d->lines) { | 164 | iConstForEach(Array, i, &d->lines) { |
144 | const iInputLine *line = i.value; | 165 | const iInputLine *line = i.value; |
@@ -147,6 +168,20 @@ static void updateCursorLine_InputWidget_(iInputWidget *d) { | |||
147 | } | 168 | } |
148 | d->cursorLine = index_ArrayConstIterator(&i); | 169 | d->cursorLine = index_ArrayConstIterator(&i); |
149 | } | 170 | } |
171 | /* May need to scroll to keep the cursor visible. */ | ||
172 | iWidget *flow = findOverflowScrollable_Widget(w); | ||
173 | if (flow) { | ||
174 | const iRect rootRect = rect_Root(w->root); | ||
175 | int yCursor = contentBounds_InputWidget_(d).pos.y + | ||
176 | lineHeight_Text(d->font) * d->cursorLine; | ||
177 | const int margin = lineHeight_Text(d->font) * 3; | ||
178 | if (yCursor < top_Rect(rootRect) + margin) { | ||
179 | scrollOverflow_Widget(flow, top_Rect(rootRect) + margin - yCursor); | ||
180 | } | ||
181 | else if (yCursor > bottom_Rect(rootRect) - margin * 3 / 2) { | ||
182 | scrollOverflow_Widget(flow, bottom_Rect(rootRect) - margin * 3 / 2 - yCursor); | ||
183 | } | ||
184 | } | ||
150 | } | 185 | } |
151 | 186 | ||
152 | static void showCursor_InputWidget_(iInputWidget *d) { | 187 | static void showCursor_InputWidget_(iInputWidget *d) { |
@@ -161,21 +196,6 @@ static void invalidateBuffered_InputWidget_(iInputWidget *d) { | |||
161 | } | 196 | } |
162 | } | 197 | } |
163 | 198 | ||
164 | iLocalDef iInt2 padding_(void) { | ||
165 | return init_I2(gap_UI / 2, gap_UI / 2); | ||
166 | } | ||
167 | |||
168 | static iRect contentBounds_InputWidget_(const iInputWidget *d) { | ||
169 | const iWidget *w = constAs_Widget(d); | ||
170 | const iRect widgetBounds = bounds_Widget(w); | ||
171 | iRect bounds = adjusted_Rect(bounds_Widget(w), | ||
172 | addX_I2(padding_(), d->leftPadding), | ||
173 | neg_I2(addX_I2(padding_(), d->rightPadding))); | ||
174 | shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); | ||
175 | bounds.pos.y += padding_().y / 2; | ||
176 | return bounds; | ||
177 | } | ||
178 | |||
179 | static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { | 199 | static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { |
180 | if (d->maxLen) { | 200 | if (d->maxLen) { |
181 | /* Set a fixed size based on maximum possible width of the text. */ | 201 | /* Set a fixed size based on maximum possible width of the text. */ |
@@ -226,6 +246,7 @@ static void updateLines_InputWidget_(iInputWidget *d) { | |||
226 | init_InputLine(&line); | 246 | init_InputLine(&line); |
227 | iString *u8 = visText_InputWidget_(d); | 247 | iString *u8 = visText_InputWidget_(d); |
228 | set_String(&line.text, u8); | 248 | set_String(&line.text, u8); |
249 | line.len = length_String(u8); | ||
229 | delete_String(u8); | 250 | delete_String(u8); |
230 | pushBack_Array(&d->lines, &line); | 251 | pushBack_Array(&d->lines, &line); |
231 | updateCursorLine_InputWidget_(d); | 252 | updateCursorLine_InputWidget_(d); |
@@ -249,14 +270,16 @@ static void updateLines_InputWidget_(iInputWidget *d) { | |||
249 | init_InputLine(&line); | 270 | init_InputLine(&line); |
250 | setRange_String(&line.text, part); | 271 | setRange_String(&line.text, part); |
251 | line.offset = charPos; | 272 | line.offset = charPos; |
273 | line.len = length_String(&line.text); | ||
252 | pushBack_Array(&d->lines, &line); | 274 | pushBack_Array(&d->lines, &line); |
253 | charPos += length_String(&line.text); | 275 | charPos += line.len; |
254 | content.start = endPos; | 276 | content.start = endPos; |
255 | } | 277 | } |
256 | if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) { | 278 | if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) { |
257 | /* Always at least one empty line. */ | 279 | /* Always at least one empty line. */ |
258 | iInputLine line; | 280 | iInputLine line; |
259 | init_InputLine(&line); | 281 | init_InputLine(&line); |
282 | line.offset = charPos; | ||
260 | pushBack_Array(&d->lines, &line); | 283 | pushBack_Array(&d->lines, &line); |
261 | } | 284 | } |
262 | else { | 285 | else { |
@@ -578,7 +601,11 @@ void begin_InputWidget(iInputWidget *d) { | |||
578 | } | 601 | } |
579 | updateCursorLine_InputWidget_(d); | 602 | updateCursorLine_InputWidget_(d); |
580 | SDL_StartTextInput(); | 603 | SDL_StartTextInput(); |
581 | setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iTrue); | 604 | setFlags_Widget(w, selected_WidgetFlag, iTrue); |
605 | if (d->maxLayoutLines != iInvalidSize) { | ||
606 | /* This will extend beyond the arranged region. */ | ||
607 | setFlags_Widget(w, keepOnTop_WidgetFlag, iTrue); | ||
608 | } | ||
582 | showCursor_InputWidget_(d); | 609 | showCursor_InputWidget_(d); |
583 | refresh_Widget(w); | 610 | refresh_Widget(w); |
584 | d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); | 611 | d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); |
@@ -672,6 +699,10 @@ void setCursor_InputWidget(iInputWidget *d, size_t pos) { | |||
672 | showCursor_InputWidget_(d); | 699 | showCursor_InputWidget_(d); |
673 | } | 700 | } |
674 | 701 | ||
702 | iLocalDef iBool isLastLine_InputWidget_(const iInputWidget *d, const iInputLine *line) { | ||
703 | return (const void *) line == constAt_Array(&d->lines, size_Array(&d->lines) - 1); | ||
704 | } | ||
705 | |||
675 | static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) { | 706 | static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) { |
676 | if (x <= 0) { | 707 | if (x <= 0) { |
677 | return line->offset; | 708 | return line->offset; |
@@ -680,7 +711,7 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const | |||
680 | tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos); | 711 | tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos); |
681 | size_t index = line->offset; | 712 | size_t index = line->offset; |
682 | if (endPos == constEnd_String(&line->text)) { | 713 | if (endPos == constEnd_String(&line->text)) { |
683 | index += length_String(&line->text); | 714 | index += line->len; |
684 | } | 715 | } |
685 | else { | 716 | else { |
686 | /* Need to know the actual character index. */ | 717 | /* Need to know the actual character index. */ |
@@ -690,6 +721,9 @@ static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const | |||
690 | index++; | 721 | index++; |
691 | } | 722 | } |
692 | } | 723 | } |
724 | if (!isLastLine_InputWidget_(d, line) && index == line->offset + line->len) { | ||
725 | index = iMax(index - 1, line->offset); | ||
726 | } | ||
693 | return index; | 727 | return index; |
694 | } | 728 | } |
695 | 729 | ||
@@ -705,11 +739,6 @@ static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) { | |||
705 | newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line); | 739 | newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line); |
706 | } | 740 | } |
707 | if (newCursor != iInvalidPos) { | 741 | if (newCursor != iInvalidPos) { |
708 | /* Clamp it to the current line. */ | ||
709 | newCursor = iMax(newCursor, line->offset); | ||
710 | newCursor = iMin(newCursor, line->offset + length_String(&line->text) - | ||
711 | /* last line is allowed to go to the cursorMax */ | ||
712 | ((const void *) line < constAt_Array(&d->lines, numLines - 1) ? 1 : 0)); | ||
713 | setCursor_InputWidget(d, newCursor); | 742 | setCursor_InputWidget(d, newCursor); |
714 | return iTrue; | 743 | return iTrue; |
715 | } | 744 | } |
@@ -896,7 +925,7 @@ static iRanges lineRange_InputWidget_(const iInputWidget *d) { | |||
896 | return (iRanges){ 0, 0 }; | 925 | return (iRanges){ 0, 0 }; |
897 | } | 926 | } |
898 | const iInputLine *line = line_InputWidget_(d, d->cursorLine); | 927 | const iInputLine *line = line_InputWidget_(d, d->cursorLine); |
899 | return (iRanges){ line->offset, line->offset + length_String(&line->text) }; | 928 | return (iRanges){ line->offset, line->offset + line->len }; |
900 | } | 929 | } |
901 | 930 | ||
902 | static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) { | 931 | static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) { |
@@ -1094,6 +1123,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1094 | return iTrue; | 1123 | return iTrue; |
1095 | } | 1124 | } |
1096 | } | 1125 | } |
1126 | #if defined (iPlatformApple) | ||
1127 | if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { | ||
1128 | switch (key) { | ||
1129 | case SDLK_UP: | ||
1130 | case SDLK_DOWN: | ||
1131 | setCursor_InputWidget(d, key == SDLK_UP ? 0 : curMax); | ||
1132 | refresh_Widget(d); | ||
1133 | return iTrue; | ||
1134 | } | ||
1135 | } | ||
1136 | #endif | ||
1097 | d->lastCursor = d->cursor; | 1137 | d->lastCursor = d->cursor; |
1098 | switch (key) { | 1138 | switch (key) { |
1099 | case SDLK_INSERT: | 1139 | case SDLK_INSERT: |
@@ -1187,7 +1227,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1187 | break; | 1227 | break; |
1188 | case SDLK_HOME: | 1228 | case SDLK_HOME: |
1189 | case SDLK_END: | 1229 | case SDLK_END: |
1190 | setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast); | 1230 | if (mods == KMOD_PRIMARY || mods == (KMOD_PRIMARY | KMOD_SHIFT)) { |
1231 | setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax); | ||
1232 | } | ||
1233 | else { | ||
1234 | setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast); | ||
1235 | } | ||
1191 | refresh_Widget(w); | 1236 | refresh_Widget(w); |
1192 | return iTrue; | 1237 | return iTrue; |
1193 | case SDLK_a: | 1238 | case SDLK_a: |
@@ -1233,19 +1278,20 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1233 | /* Allow focus switching. */ | 1278 | /* Allow focus switching. */ |
1234 | return processEvent_Widget(as_Widget(d), ev); | 1279 | return processEvent_Widget(as_Widget(d), ev); |
1235 | case SDLK_UP: | 1280 | case SDLK_UP: |
1236 | if (moveCursorByLine_InputWidget_(d, -1)) { | ||
1237 | refresh_Widget(d); | ||
1238 | return iTrue; | ||
1239 | } | ||
1240 | /* For moving to lookup from url entry. */ | ||
1241 | return processEvent_Widget(as_Widget(d), ev); | ||
1242 | case SDLK_DOWN: | 1281 | case SDLK_DOWN: |
1243 | if (moveCursorByLine_InputWidget_(d, +1)) { | 1282 | if (moveCursorByLine_InputWidget_(d, key == SDLK_UP ? -1 : +1)) { |
1244 | refresh_Widget(d); | 1283 | refresh_Widget(d); |
1245 | return iTrue; | 1284 | return iTrue; |
1246 | } | 1285 | } |
1247 | /* For moving to lookup from url entry. */ | 1286 | /* For moving to lookup from url entry. */ |
1248 | return processEvent_Widget(as_Widget(d), ev); | 1287 | return processEvent_Widget(as_Widget(d), ev); |
1288 | case SDLK_PAGEUP: | ||
1289 | case SDLK_PAGEDOWN: | ||
1290 | for (int count = 0; count < 5; count++) { | ||
1291 | moveCursorByLine_InputWidget_(d, key == SDLK_PAGEUP ? -1 : +1); | ||
1292 | } | ||
1293 | refresh_Widget(d); | ||
1294 | return iTrue; | ||
1249 | } | 1295 | } |
1250 | if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { | 1296 | if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { |
1251 | return iFalse; | 1297 | return iFalse; |
@@ -1265,6 +1311,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
1265 | return processEvent_Widget(w, ev); | 1311 | return processEvent_Widget(w, ev); |
1266 | } | 1312 | } |
1267 | 1313 | ||
1314 | #if 0 | ||
1268 | static iBool isWhite_(const iString *str) { | 1315 | static iBool isWhite_(const iString *str) { |
1269 | iConstForEach(String, i, str) { | 1316 | iConstForEach(String, i, str) { |
1270 | if (!isSpace_Char(i.value)) { | 1317 | if (!isSpace_Char(i.value)) { |
@@ -1273,6 +1320,7 @@ static iBool isWhite_(const iString *str) { | |||
1273 | } | 1320 | } |
1274 | return iTrue; | 1321 | return iTrue; |
1275 | } | 1322 | } |
1323 | #endif | ||
1276 | 1324 | ||
1277 | static void draw_InputWidget_(const iInputWidget *d) { | 1325 | static void draw_InputWidget_(const iInputWidget *d) { |
1278 | const iWidget *w = constAs_Widget(d); | 1326 | const iWidget *w = constAs_Widget(d); |
@@ -1337,7 +1385,8 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
1337 | .x; | 1385 | .x; |
1338 | fillRect_Paint(&p, | 1386 | fillRect_Paint(&p, |
1339 | (iRect){ addX_I2(drawPos, iMin(m1, m2)), | 1387 | (iRect){ addX_I2(drawPos, iMin(m1, m2)), |
1340 | init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) }, | 1388 | init_I2(iMax(gap_UI / 3, iAbs(m2 - m1)), |
1389 | lineHeight_Text(d->font)) }, | ||
1341 | uiMarked_ColorId); | 1390 | uiMarked_ColorId); |
1342 | } | 1391 | } |
1343 | } | 1392 | } |
@@ -1405,13 +1454,7 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
1405 | drawChildren_Widget(w); | 1454 | drawChildren_Widget(w); |
1406 | } | 1455 | } |
1407 | 1456 | ||
1408 | //static void sizeChanged_InputWidget_(iInputWidget *d) { | ||
1409 | // printf("[InputWidget] %p: size changed, updating layout\n", d); | ||
1410 | // updateLinesAndResize_InputWidget_(d, iFalse); | ||
1411 | //} | ||
1412 | |||
1413 | iBeginDefineSubclass(InputWidget, Widget) | 1457 | iBeginDefineSubclass(InputWidget, Widget) |
1414 | .processEvent = (iAny *) processEvent_InputWidget_, | 1458 | .processEvent = (iAny *) processEvent_InputWidget_, |
1415 | .draw = (iAny *) draw_InputWidget_, | 1459 | .draw = (iAny *) draw_InputWidget_, |
1416 | // .sizeChanged = (iAny *) sizeChanged_InputWidget_, | ||
1417 | iEndDefineSubclass(InputWidget) | 1460 | iEndDefineSubclass(InputWidget) |