summaryrefslogtreecommitdiff
path: root/src/ui
diff options
context:
space:
mode:
authorJaakko Keränen <jaakko.keranen@iki.fi>2021-05-11 18:14:33 +0300
committerJaakko Keränen <jaakko.keranen@iki.fi>2021-05-11 18:14:33 +0300
commit15c5ab9ec7563fb6fcb49657e075729c18461c3b (patch)
treeac203b1966879f9793055f11d86967f49ff6256e /src/ui
parenta13543aa922647ea2f8cc40cb9f3f797df8758df (diff)
InputWidget: Word wrapping, inserting newlines
Diffstat (limited to 'src/ui')
-rw-r--r--src/ui/inputwidget.c433
-rw-r--r--src/ui/root.c16
-rw-r--r--src/ui/root.h4
-rw-r--r--src/ui/text.c7
4 files changed, 357 insertions, 103 deletions
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index a1635128..22dd4c92 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -77,6 +77,28 @@ enum iInputWidgetFlag {
77 needUpdateBuffer_InputWidgetFlag = iBit(9), 77 needUpdateBuffer_InputWidgetFlag = iBit(9),
78}; 78};
79 79
80/*----------------------------------------------------------------------------------------------*/
81
82iDeclareType(InputLine)
83
84struct Impl_InputLine {
85 size_t offset; /* character position from the beginning */
86 iString text; /* UTF-8 */
87};
88
89static void init_InputLine(iInputLine *d) {
90 d->offset = 0;
91 init_String(&d->text);
92}
93
94static void deinit_InputLine(iInputLine *d) {
95 deinit_String(&d->text);
96}
97
98iDefineTypeConstruction(InputLine)
99
100/*----------------------------------------------------------------------------------------------*/
101
80struct Impl_InputWidget { 102struct Impl_InputWidget {
81 iWidget widget; 103 iWidget widget;
82 enum iInputMode mode; 104 enum iInputMode mode;
@@ -84,12 +106,15 @@ struct Impl_InputWidget {
84 size_t maxLen; 106 size_t maxLen;
85 iArray text; /* iChar[] */ 107 iArray text; /* iChar[] */
86 iArray oldText; /* iChar[] */ 108 iArray oldText; /* iChar[] */
109 iArray lines;
87 iString hint; 110 iString hint;
88 iString srcHint; 111 iString srcHint;
89 int leftPadding; 112 int leftPadding;
90 int rightPadding; 113 int rightPadding;
91 size_t cursor; 114 size_t cursor; /* offset from beginning */
92 size_t lastCursor; 115 size_t lastCursor;
116 size_t cursorLine;
117 int verticalMoveX;
93 iRanges mark; 118 iRanges mark;
94 iRanges initialMark; 119 iRanges initialMark;
95 iArray undoStack; 120 iArray undoStack;
@@ -109,8 +134,20 @@ static void clearUndo_InputWidget_(iInputWidget *d) {
109 clear_Array(&d->undoStack); 134 clear_Array(&d->undoStack);
110} 135}
111 136
137static void updateCursorLine_InputWidget_(iInputWidget *d) {
138 d->cursorLine = 0;
139 iConstForEach(Array, i, &d->lines) {
140 const iInputLine *line = i.value;
141 if (line->offset > d->cursor) {
142 break;
143 }
144 d->cursorLine = index_ArrayConstIterator(&i);
145 }
146}
147
112static void showCursor_InputWidget_(iInputWidget *d) { 148static void showCursor_InputWidget_(iInputWidget *d) {
113 d->cursorVis = 2; 149 d->cursorVis = 2;
150 updateCursorLine_InputWidget_(d);
114} 151}
115 152
116static void invalidateBuffered_InputWidget_(iInputWidget *d) { 153static void invalidateBuffered_InputWidget_(iInputWidget *d) {
@@ -120,6 +157,20 @@ static void invalidateBuffered_InputWidget_(iInputWidget *d) {
120 } 157 }
121} 158}
122 159
160iLocalDef iInt2 padding_(void) {
161 return init_I2(gap_UI / 2, gap_UI / 2);
162}
163
164static iRect contentBounds_InputWidget_(const iInputWidget *d) {
165 const iWidget *w = constAs_Widget(d);
166 iRect bounds = adjusted_Rect(bounds_Widget(w),
167 addX_I2(padding_(), d->leftPadding),
168 neg_I2(addX_I2(padding_(), d->rightPadding)));
169 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
170 bounds.pos.y += padding_().y / 2;
171 return bounds;
172}
173
123static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { 174static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
124 if (d->maxLen) { 175 if (d->maxLen) {
125 /* Set a fixed size based on maximum possible width of the text. */ 176 /* Set a fixed size based on maximum possible width of the text. */
@@ -135,17 +186,97 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
135 } 186 }
136} 187}
137 188
189static const iChar sensitiveChar_ = 0x25cf; /* black circle */
190
191static iString *utf32toUtf8_InputWidget_(const iInputWidget *d) {
192 return newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
193}
194
195static iString *visText_InputWidget_(const iInputWidget *d) {
196 iString *text;
197 if (~d->inFlags & isSensitive_InputWidgetFlag) {
198 text = utf32toUtf8_InputWidget_(d);
199 }
200 else {
201 text = new_String();
202 for (size_t i = 0; i < size_Array(&d->text); ++i) {
203 appendChar_String(text, sensitiveChar_);
204 }
205 }
206 return text;
207}
208
209static void clearLines_InputWidget_(iInputWidget *d) {
210 iForEach(Array, i, &d->lines) {
211 deinit_InputLine(i.value);
212 }
213 clear_Array(&d->lines);
214}
215
216static void updateLines_InputWidget_(iInputWidget *d) {
217 clearLines_InputWidget_(d);
218 if (d->maxLen) {
219 /* Everything on a single line. */
220 iInputLine line;
221 init_InputLine(&line);
222 iString *u8 = visText_InputWidget_(d);
223 set_String(&line.text, u8);
224 delete_String(u8);
225 pushBack_Array(&d->lines, &line);
226 updateCursorLine_InputWidget_(d);
227 return;
228 }
229 /* Word-wrapped lines. */
230 iString *u8 = visText_InputWidget_(d);
231 size_t charPos = 0;
232 iRangecc content = range_String(u8);
233 const int wrapWidth = contentBounds_InputWidget_(d).size.x;
234 while (wrapWidth > 0 && content.end != content.start) {
235 const char *endPos;
236 tryAdvance_Text(d->font, content, wrapWidth, &endPos);
237 const iRangecc part = (iRangecc){ content.start, endPos };
238 iInputLine line;
239 init_InputLine(&line);
240 setRange_String(&line.text, part);
241 line.offset = charPos;
242 pushBack_Array(&d->lines, &line);
243 charPos += length_String(&line.text);
244 content.start = endPos;
245 }
246 if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) {
247 /* Always at least one empty line. */
248 iInputLine line;
249 init_InputLine(&line);
250 pushBack_Array(&d->lines, &line);
251 }
252 else {
253 iAssert(charPos == length_String(u8));
254 }
255 delete_String(u8);
256 updateCursorLine_InputWidget_(d);
257}
258
259static int contentHeight_InputWidget_(const iInputWidget *d) {
260 return iMax(1, size_Array(&d->lines)) * lineHeight_Text(d->font);
261}
262
138static void updateMetrics_InputWidget_(iInputWidget *d) { 263static void updateMetrics_InputWidget_(iInputWidget *d) {
139 iWidget *w = as_Widget(d); 264 iWidget *w = as_Widget(d);
140 updateSizeForFixedLength_InputWidget_(d); 265 updateSizeForFixedLength_InputWidget_(d);
141 /* Caller must arrange the width, but the height is fixed. */ 266 /* Caller must arrange the width, but the height is fixed. */
142 w->rect.size.y = lineHeight_Text(d->font) * 1.3f; 267 w->rect.size.y = contentHeight_InputWidget_(d) + 3 * padding_().y; /* TODO: Why 3x? */
143 if (flags_Widget(w) & extraPadding_WidgetFlag) { 268 if (flags_Widget(w) & extraPadding_WidgetFlag) {
144 w->rect.size.y += 2 * gap_UI; 269 w->rect.size.y += 2 * gap_UI;
145 } 270 }
146 invalidateBuffered_InputWidget_(d); 271 invalidateBuffered_InputWidget_(d);
147 if (parent_Widget(w)) { 272 postCommand_Widget(d, "input.resized");
148 arrange_Widget(w); 273}
274
275static void updateLinesAndResize_InputWidget_(iInputWidget *d) {
276 const size_t oldCount = size_Array(&d->lines);
277 updateLines_InputWidget_(d);
278 if (oldCount != size_Array(&d->lines)) {
279 updateMetrics_InputWidget_(d);
149 } 280 }
150} 281}
151 282
@@ -158,6 +289,7 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
158#endif 289#endif
159 init_Array(&d->text, sizeof(iChar)); 290 init_Array(&d->text, sizeof(iChar));
160 init_Array(&d->oldText, sizeof(iChar)); 291 init_Array(&d->oldText, sizeof(iChar));
292 init_Array(&d->lines, sizeof(iInputLine));
161 init_String(&d->hint); 293 init_String(&d->hint);
162 init_String(&d->srcHint); 294 init_String(&d->srcHint);
163 init_Array(&d->undoStack, sizeof(iInputUndo)); 295 init_Array(&d->undoStack, sizeof(iInputUndo));
@@ -166,6 +298,8 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
166 d->rightPadding = 0; 298 d->rightPadding = 0;
167 d->cursor = 0; 299 d->cursor = 0;
168 d->lastCursor = 0; 300 d->lastCursor = 0;
301 d->cursorLine = 0;
302 d->verticalMoveX = -1;
169 d->inFlags = eatEscape_InputWidgetFlag; 303 d->inFlags = eatEscape_InputWidgetFlag;
170 iZap(d->mark); 304 iZap(d->mark);
171 setMaxLen_InputWidget(d, maxLen); 305 setMaxLen_InputWidget(d, maxLen);
@@ -174,10 +308,12 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
174 d->timer = 0; 308 d->timer = 0;
175 d->cursorVis = 0; 309 d->cursorVis = 0;
176 d->buffered = NULL; 310 d->buffered = NULL;
311 updateLines_InputWidget_(d);
177 updateMetrics_InputWidget_(d); 312 updateMetrics_InputWidget_(d);
178} 313}
179 314
180void deinit_InputWidget(iInputWidget *d) { 315void deinit_InputWidget(iInputWidget *d) {
316 clearLines_InputWidget_(d);
181 if (isSelected_Widget(d)) { 317 if (isSelected_Widget(d)) {
182 SDL_StopTextInput(); 318 SDL_StopTextInput();
183 enableEditorKeysInMenus_(iTrue); 319 enableEditorKeysInMenus_(iTrue);
@@ -190,6 +326,7 @@ void deinit_InputWidget(iInputWidget *d) {
190 } 326 }
191 deinit_String(&d->srcHint); 327 deinit_String(&d->srcHint);
192 deinit_String(&d->hint); 328 deinit_String(&d->hint);
329 deinit_Array(&d->lines);
193 deinit_Array(&d->oldText); 330 deinit_Array(&d->oldText);
194 deinit_Array(&d->text); 331 deinit_Array(&d->text);
195} 332}
@@ -199,6 +336,11 @@ void setFont_InputWidget(iInputWidget *d, int fontId) {
199 updateMetrics_InputWidget_(d); 336 updateMetrics_InputWidget_(d);
200} 337}
201 338
339static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) {
340 iAssert(!isEmpty_Array(&d->lines));
341 return constAt_Array(&d->lines, index);
342}
343
202static void pushUndo_InputWidget_(iInputWidget *d) { 344static void pushUndo_InputWidget_(iInputWidget *d) {
203 iInputUndo undo; 345 iInputUndo undo;
204 init_InputUndo_(&undo, &d->text, d->cursor); 346 init_InputUndo_(&undo, &d->text, d->cursor);
@@ -241,10 +383,6 @@ static const iString *omitDefaultScheme_(iString *url) {
241 return url; 383 return url;
242} 384}
243 385
244static iString *utf32toUtf8_InputWidget_(const iInputWidget *d) {
245 return newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
246}
247
248const iString *text_InputWidget(const iInputWidget *d) { 386const iString *text_InputWidget(const iInputWidget *d) {
249 if (d) { 387 if (d) {
250 iString *text = collect_String(utf32toUtf8_InputWidget_(d)); 388 iString *text = collect_String(utf32toUtf8_InputWidget_(d));
@@ -285,27 +423,13 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) {
285 refresh_Widget(d); 423 refresh_Widget(d);
286} 424}
287 425
288static const iChar sensitiveChar_ = 0x25cf; /* black circle */
289
290static iString *visText_InputWidget_(const iInputWidget *d) {
291 iString *text;
292 if (~d->inFlags & isSensitive_InputWidgetFlag) {
293 text = newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
294 }
295 else {
296 text = new_String();
297 for (size_t i = 0; i < size_Array(&d->text); ++i) {
298 appendChar_String(text, sensitiveChar_);
299 }
300 }
301 return text;
302}
303
304static void updateBuffered_InputWidget_(iInputWidget *d) { 426static void updateBuffered_InputWidget_(iInputWidget *d) {
305 iWindow *win = get_Window(); 427// iWindow *win = get_Window();
306 invalidateBuffered_InputWidget_(d); 428 invalidateBuffered_InputWidget_(d);
307 iString *bufText = NULL; 429 iString *bufText = NULL;
430#if 0
308 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) { 431 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) {
432 /* TODO: Move this omitting to `updateLines_`? */
309 /* Highlight the host name. */ 433 /* Highlight the host name. */
310 iUrl parts; 434 iUrl parts;
311 const iString *text = collect_String(utf32toUtf8_InputWidget_(d)); 435 const iString *text = collect_String(utf32toUtf8_InputWidget_(d));
@@ -319,6 +443,7 @@ static void updateBuffered_InputWidget_(iInputWidget *d) {
319 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) }); 443 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) });
320 } 444 }
321 } 445 }
446#endif
322 if (!bufText) { 447 if (!bufText) {
323 bufText = visText_InputWidget_(d); 448 bufText = visText_InputWidget_(d);
324 } 449 }
@@ -350,7 +475,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
350 } 475 }
351 if (isFocused_Widget(d)) { 476 if (isFocused_Widget(d)) {
352 d->cursor = size_Array(&d->text); 477 d->cursor = size_Array(&d->text);
353 selectAll_InputWidget(d); 478// selectAll_InputWidget(d);
354 } 479 }
355 else { 480 else {
356 d->cursor = iMin(d->cursor, size_Array(&d->text)); 481 d->cursor = iMin(d->cursor, size_Array(&d->text));
@@ -359,6 +484,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
359 if (!isFocused_Widget(d)) { 484 if (!isFocused_Widget(d)) {
360 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 485 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
361 } 486 }
487 updateLinesAndResize_InputWidget_(d);
362 refresh_Widget(as_Widget(d)); 488 refresh_Widget(as_Widget(d));
363} 489}
364 490
@@ -404,6 +530,7 @@ void begin_InputWidget(iInputWidget *d) {
404 else { 530 else {
405 d->cursor = iMin(size_Array(&d->text), d->maxLen - 1); 531 d->cursor = iMin(size_Array(&d->text), d->maxLen - 1);
406 } 532 }
533 updateCursorLine_InputWidget_(d);
407 SDL_StartTextInput(); 534 SDL_StartTextInput();
408 setFlags_Widget(w, selected_WidgetFlag, iTrue); 535 setFlags_Widget(w, selected_WidgetFlag, iTrue);
409 showCursor_InputWidget_(d); 536 showCursor_InputWidget_(d);
@@ -436,6 +563,7 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
436 setFlags_Widget(w, selected_WidgetFlag, iFalse); 563 setFlags_Widget(w, selected_WidgetFlag, iFalse);
437 const char *id = cstr_String(id_Widget(as_Widget(d))); 564 const char *id = cstr_String(id_Widget(as_Widget(d)));
438 if (!*id) id = "_"; 565 if (!*id) id = "_";
566 updateLinesAndResize_InputWidget_(d);
439 refresh_Widget(w); 567 refresh_Widget(w);
440 postCommand_Widget(w, 568 postCommand_Widget(w,
441 "input.ended id:%s enter:%d arg:%d", 569 "input.ended id:%s enter:%d arg:%d",
@@ -476,7 +604,6 @@ iLocalDef iBool isMarking_(void) {
476} 604}
477 605
478void setCursor_InputWidget(iInputWidget *d, size_t pos) { 606void setCursor_InputWidget(iInputWidget *d, size_t pos) {
479 showCursor_InputWidget_(d);
480 if (isEmpty_Array(&d->text)) { 607 if (isEmpty_Array(&d->text)) {
481 d->cursor = 0; 608 d->cursor = 0;
482 } 609 }
@@ -496,6 +623,51 @@ void setCursor_InputWidget(iInputWidget *d, size_t pos) {
496 else { 623 else {
497 iZap(d->mark); 624 iZap(d->mark);
498 } 625 }
626 showCursor_InputWidget_(d);
627}
628
629static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) {
630 if (x <= 0) {
631 return line->offset;
632 }
633 const char *endPos;
634 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos);
635 size_t index = line->offset;
636 if (endPos == constEnd_String(&line->text)) {
637 index += length_String(&line->text);
638 }
639 else {
640 /* Need to know the actual character index. */
641 /* TODO: tryAdvance could tell us this directly with an extra return value */
642 iConstForEach(String, i, &line->text) {
643 if (i.pos >= endPos) break;
644 index++;
645 }
646 }
647 return index;
648}
649
650static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) {
651 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
652 int xPos = advanceN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).x;
653 size_t newCursor = iInvalidPos;
654 const size_t numLines = size_Array(&d->lines);
655 if (dir < 0 && d->cursorLine > 0) {
656 newCursor = indexForRelativeX_InputWidget_(d, xPos, --line);
657 }
658 else if (dir > 0 && d->cursorLine < numLines - 1) {
659 newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line);
660 }
661 if (newCursor != iInvalidPos) {
662 /* Clamp it to the current line. */
663 newCursor = iMax(newCursor, line->offset);
664 newCursor = iMin(newCursor, line->offset + length_String(&line->text) -
665 /* last line is allowed to go to the cursorMax */
666 ((const void *) line < constAt_Array(&d->lines, numLines - 1) ? 1 : 0));
667 setCursor_InputWidget(d, newCursor);
668 return iTrue;
669 }
670 return iFalse;
499} 671}
500 672
501void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { 673void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) {
@@ -526,6 +698,7 @@ static iRanges mark_InputWidget_(const iInputWidget *d) {
526} 698}
527 699
528static void contentsWereChanged_InputWidget_(iInputWidget *d) { 700static void contentsWereChanged_InputWidget_(iInputWidget *d) {
701 updateLinesAndResize_InputWidget_(d);
529 if (d->inFlags & notifyEdits_InputWidgetFlag) { 702 if (d->inFlags & notifyEdits_InputWidgetFlag) {
530 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); 703 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d))));
531 } 704 }
@@ -591,21 +764,18 @@ static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir)
591 return pos; 764 return pos;
592} 765}
593 766
594iLocalDef iInt2 padding_(void) { 767#if 0
595 return init_I2(gap_UI / 2, gap_UI / 2); 768static iInt2 textOrigin_InputWidget_(const iInputWidget *d) { //}, const char *visText) {
596} 769// const iWidget *w = constAs_Widget(d);
597 770 iRect bounds = contentBounds_InputWidget_(d);/* adjusted_Rect(bounds_Widget(w),
598static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) {
599 const iWidget *w = constAs_Widget(d);
600 iRect bounds = adjusted_Rect(bounds_Widget(w),
601 addX_I2(padding_(), d->leftPadding), 771 addX_I2(padding_(), d->leftPadding),
602 neg_I2(addX_I2(padding_(), d->rightPadding))); 772 neg_I2(addX_I2(padding_(), d->rightPadding)));*/
603 const iInt2 emSize = advance_Text(d->font, "M"); 773// const iInt2 emSize = advance_Text(d->font, "M");
604 const int textWidth = advance_Text(d->font, visText).x; 774// const int textWidth = advance_Text(d->font, visText).x;
605 const int cursorX = advanceN_Text(d->font, visText, d->cursor).x; 775// const int cursorX = advanceN_Text(d->font, visText, d->cursor).x;
606 int xOff = 0; 776// int xOff = 0;
607 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 777// shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
608 if (d->maxLen == 0) { 778/* if (d->maxLen == 0) {
609 if (textWidth > width_Rect(bounds) - emSize.x) { 779 if (textWidth > width_Rect(bounds) - emSize.x) {
610 xOff = width_Rect(bounds) - emSize.x - textWidth; 780 xOff = width_Rect(bounds) - emSize.x - textWidth;
611 } 781 }
@@ -613,32 +783,20 @@ static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText)
613 xOff = width_Rect(bounds) / 2 - cursorX; 783 xOff = width_Rect(bounds) / 2 - cursorX;
614 } 784 }
615 xOff = iMin(xOff, 0); 785 xOff = iMin(xOff, 0);
616 } 786 }*/
617 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; 787// const int yOff = 0.3f * lineHeight_Text(d->font); // (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
618 return add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)); 788// return addY_I2(topLeft_Rect(bounds), yOff);
789
619} 790}
791#endif
620 792
621static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) { 793static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
622 iString *visText = visText_InputWidget_(d); 794 const iInt2 pos = sub_I2(coord, contentBounds_InputWidget_(d).pos);
623 iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText))); 795 const size_t lineNumber = iMin(pos.y / lineHeight_Text(d->font), (int) size_Array(&d->lines) - 1);
624 size_t index = 0; 796 const iInputLine *line = line_InputWidget_(d, lineNumber);
625 if (pos.x > 0) { 797 const char *endPos;
626 const char *endPos; 798 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), pos.x, &endPos);
627 tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos); 799 return indexForRelativeX_InputWidget_(d, pos.x, line);
628 if (endPos == constEnd_String(visText)) {
629 index = cursorMax_InputWidget_(d);
630 }
631 else {
632 /* Need to know the actual character index. */
633 /* TODO: tryAdvance could tell us this directly with an extra return value */
634 iConstForEach(String, i, visText) {
635 if (i.pos >= endPos) break;
636 index++;
637 }
638 }
639 }
640 delete_String(visText);
641 return index;
642} 800}
643 801
644static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) { 802static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) {
@@ -683,6 +841,14 @@ static iChar at_InputWidget_(const iInputWidget *d, size_t pos) {
683 return *(const iChar *) constAt_Array(&d->text, pos); 841 return *(const iChar *) constAt_Array(&d->text, pos);
684} 842}
685 843
844static iRanges lineRange_InputWidget_(const iInputWidget *d) {
845 if (isEmpty_Array(&d->lines)) {
846 return (iRanges){ 0, 0 };
847 }
848 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
849 return (iRanges){ line->offset, line->offset + length_String(&line->text) };
850}
851
686static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) { 852static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) {
687 const size_t textLen = size_Array(&d->text); 853 const size_t textLen = size_Array(&d->text);
688 if (dir < 0 && *pos > 0) { 854 if (dir < 0 && *pos > 0) {
@@ -746,12 +912,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
746 } 912 }
747 else if (isMetricsChange_UserEvent(ev)) { 913 else if (isMetricsChange_UserEvent(ev)) {
748 updateMetrics_InputWidget_(d); 914 updateMetrics_InputWidget_(d);
915 updateLinesAndResize_InputWidget_(d);
749 } 916 }
750 else if (isResize_UserEvent(ev)) { 917 else if (isResize_UserEvent(ev)) {
751 if (d->inFlags & isUrl_InputWidgetFlag) { 918 if (d->inFlags & isUrl_InputWidgetFlag) {
752 /* Restore/omit the default scheme if necessary. */ 919 /* Restore/omit the default scheme if necessary. */
753 setText_InputWidget(d, text_InputWidget(d)); 920 setText_InputWidget(d, text_InputWidget(d));
754 } 921 }
922 updateLinesAndResize_InputWidget_(d);
755 } 923 }
756 else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { 924 else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) {
757 copy_InputWidget_(d, iFalse); 925 copy_InputWidget_(d, iFalse);
@@ -790,8 +958,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
790 d->inFlags &= ~isMarking_InputWidgetFlag; 958 d->inFlags &= ~isMarking_InputWidgetFlag;
791 return iTrue; 959 return iTrue;
792 case drag_ClickResult: 960 case drag_ClickResult:
793 showCursor_InputWidget_(d);
794 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click)); 961 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click));
962 showCursor_InputWidget_(d);
795 if (~d->inFlags & isMarking_InputWidgetFlag) { 963 if (~d->inFlags & isMarking_InputWidgetFlag) {
796 d->inFlags |= isMarking_InputWidgetFlag; 964 d->inFlags |= isMarking_InputWidgetFlag;
797 d->mark.start = d->cursor; 965 d->mark.start = d->cursor;
@@ -822,7 +990,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
822 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 990 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
823 return iTrue; 991 return iTrue;
824 } 992 }
825 const size_t curMax = cursorMax_InputWidget_(d); 993 const size_t curMax = cursorMax_InputWidget_(d);
994 const iRanges lineRange = lineRange_InputWidget_(d);
995 const size_t lineFirst = lineRange.start;
996 const size_t lineLast = lineRange.end == curMax ? curMax : iMax(lineRange.start, lineRange.end - 1);
826 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 997 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
827 const int key = ev->key.keysym.sym; 998 const int key = ev->key.keysym.sym;
828 const int mods = keyMods_Sym(ev->key.keysym.mod); 999 const int mods = keyMods_Sym(ev->key.keysym.mod);
@@ -852,6 +1023,13 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
852 return iTrue; 1023 return iTrue;
853 case SDLK_RETURN: 1024 case SDLK_RETURN:
854 case SDLK_KP_ENTER: 1025 case SDLK_KP_ENTER:
1026 if (mods == KMOD_SHIFT) {
1027 pushUndo_InputWidget_(d);
1028 deleteMarked_InputWidget_(d);
1029 insertChar_InputWidget_(d, '\n');
1030 contentsWereChanged_InputWidget_(d);
1031 return iTrue;
1032 }
855 d->inFlags |= enterPressed_InputWidgetFlag; 1033 d->inFlags |= enterPressed_InputWidgetFlag;
856 setFocus_Widget(NULL); 1034 setFocus_Widget(NULL);
857 return iTrue; 1035 return iTrue;
@@ -927,7 +1105,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
927 break; 1105 break;
928 case SDLK_HOME: 1106 case SDLK_HOME:
929 case SDLK_END: 1107 case SDLK_END:
930 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax); 1108 setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast);
931 refresh_Widget(w); 1109 refresh_Widget(w);
932 return iTrue; 1110 return iTrue;
933 case SDLK_a: 1111 case SDLK_a:
@@ -944,7 +1122,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
944 /* fall through for Emacs-style Home/End */ 1122 /* fall through for Emacs-style Home/End */
945 case SDLK_e: 1123 case SDLK_e:
946 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { 1124 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
947 setCursor_InputWidget(d, key == 'a' ? 0 : curMax); 1125 setCursor_InputWidget(d, key == 'a' ? lineFirst : lineLast);
948 refresh_Widget(w); 1126 refresh_Widget(w);
949 return iTrue; 1127 return iTrue;
950 } 1128 }
@@ -953,7 +1131,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
953 case SDLK_RIGHT: { 1131 case SDLK_RIGHT: {
954 const int dir = (key == SDLK_LEFT ? -1 : +1); 1132 const int dir = (key == SDLK_LEFT ? -1 : +1);
955 if (mods & byLine_KeyModifier) { 1133 if (mods & byLine_KeyModifier) {
956 setCursor_InputWidget(d, dir < 0 ? 0 : curMax); 1134 setCursor_InputWidget(d, dir < 0 ? lineFirst : lineLast);
957 } 1135 }
958 else if (mods & byWord_KeyModifier) { 1136 else if (mods & byWord_KeyModifier) {
959 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir)); 1137 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir));
@@ -970,9 +1148,21 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
970 return iTrue; 1148 return iTrue;
971 } 1149 }
972 case SDLK_TAB: 1150 case SDLK_TAB:
973 case SDLK_DOWN: /* for moving to lookup from url entry */
974 /* Allow focus switching. */ 1151 /* Allow focus switching. */
975 return processEvent_Widget(as_Widget(d), ev); 1152 return processEvent_Widget(as_Widget(d), ev);
1153 case SDLK_UP:
1154 if (moveCursorByLine_InputWidget_(d, -1)) {
1155 refresh_Widget(d);
1156 return iTrue;
1157 }
1158 break;
1159 case SDLK_DOWN:
1160 if (moveCursorByLine_InputWidget_(d, +1)) {
1161 refresh_Widget(d);
1162 return iTrue;
1163 }
1164 /* For moving to lookup from url entry. */
1165 return processEvent_Widget(as_Widget(d), ev);
976 } 1166 }
977 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { 1167 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) {
978 return iFalse; 1168 return iFalse;
@@ -1013,11 +1203,13 @@ static void draw_InputWidget_(const iInputWidget *d) {
1013 } 1203 }
1014 iPaint p; 1204 iPaint p;
1015 init_Paint(&p); 1205 init_Paint(&p);
1016 iString *text = visText_InputWidget_(d); 1206 /* `lines` is already up to date and ready for drawing. */
1017 if (isWhite_(text) && !isEmpty_String(&d->hint)) { 1207 /* TODO: If empty, draw the hint. */
1018 set_String(text, &d->hint); 1208// iString *text = visText_InputWidget_(d);
1019 isHint = iTrue; 1209// if (isWhite_(text) && !isEmpty_String(&d->hint)) {
1020 } 1210// set_String(text, &d->hint);
1211// isHint = iTrue;
1212// }
1021 fillRect_Paint( 1213 fillRect_Paint(
1022 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); 1214 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId);
1023 drawRectThickness_Paint(&p, 1215 drawRectThickness_Paint(&p,
@@ -1026,29 +1218,53 @@ static void draw_InputWidget_(const iInputWidget *d) {
1026 isFocused ? uiInputFrameFocused_ColorId 1218 isFocused ? uiInputFrameFocused_ColorId
1027 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 1219 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
1028 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, 0))); 1220 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, 0)));
1029 const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text)); 1221 const iRect contentBounds = contentBounds_InputWidget_(d);
1030 if (isFocused && !isEmpty_Range(&d->mark)) { 1222// const iInt2 textOrigin = textOrigin_InputWidget_(d); //, cstr_String(text));
1031 /* Draw the selected range. */ 1223 iInt2 drawPos = topLeft_Rect(contentBounds);
1032 const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x; 1224 const int fg = isHint ? uiAnnotation_ColorId
1033 const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x; 1225 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
1034 fillRect_Paint(&p, 1226 : uiInputText_ColorId;
1035 (iRect){ addX_I2(textOrigin, iMin(m1, m2)), 1227 /* TODO: If buffered, just draw the buffered copy. */
1036 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) }, 1228 iConstForEach(Array, i, &d->lines) {
1037 uiMarked_ColorId); 1229 const iInputLine *line = i.value;
1038 } 1230 const iBool isLast = index_ArrayConstIterator(&i) == size_Array(&d->lines) - 1;
1039 if (d->buffered && !isFocused && !isHint) { 1231 const iInputLine *nextLine = isLast ? NULL : (line + 1);
1040 /* Most input widgets will use this, since only one is focused at a time. */ 1232 const iRanges lineRange = { line->offset,
1041 draw_TextBuf(d->buffered, textOrigin, white_ColorId); 1233 nextLine ? nextLine->offset : size_Array(&d->text) };
1042 } 1234 if (isFocused && !isEmpty_Range(&d->mark)) {
1043 else { 1235 /* Draw the selected range. */
1044 draw_Text(d->font, 1236 const iRanges mark = mark_InputWidget_(d);
1045 textOrigin, 1237 if (mark.start < lineRange.end && mark.end > lineRange.start) {
1046 isHint ? uiAnnotation_ColorId 1238 const int m1 = advanceN_Text(d->font,
1047 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId 1239 cstr_String(&line->text),
1048 : uiInputText_ColorId, 1240 iMax(lineRange.start, mark.start) - line->offset)
1049 "%s", 1241 .x;
1050 cstr_String(text)); 1242 const int m2 = advanceN_Text(d->font,
1051 } 1243 cstr_String(&line->text),
1244 iMin(lineRange.end, mark.end) - line->offset)
1245 .x;
1246 fillRect_Paint(&p,
1247 (iRect){ addX_I2(drawPos, iMin(m1, m2)),
1248 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) },
1249 uiMarked_ColorId);
1250 }
1251 }
1252 drawRange_Text(d->font, drawPos, fg, range_String(&line->text));
1253 drawPos.y += lineHeight_Text(d->font);
1254 }
1255// if (d->buffered && !isFocused && !isHint) {
1256// /* Most input widgets will use this, since only one is focused at a time. */
1257// draw_TextBuf(d->buffered, textOrigin, white_ColorId);
1258// }
1259// else {
1260// draw_Text(d->font,
1261// textOrigin,
1262// isHint ? uiAnnotation_ColorId
1263// : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
1264// : uiInputText_ColorId,
1265// "%s",
1266// cstr_String(text));
1267// }
1052 unsetClip_Paint(&p); 1268 unsetClip_Paint(&p);
1053 /* Cursor blinking. */ 1269 /* Cursor blinking. */
1054 if (isFocused && d->cursorVis) { 1270 if (isFocused && d->cursorVis) {
@@ -1073,23 +1289,36 @@ static void draw_InputWidget_(const iInputWidget *d) {
1073 /* Bar cursor. */ 1289 /* Bar cursor. */
1074 curSize = init_I2(gap_UI / 2, lineHeight_Text(d->font)); 1290 curSize = init_I2(gap_UI / 2, lineHeight_Text(d->font));
1075 } 1291 }
1292 const iInputLine *curLine = line_InputWidget_(d, d->cursorLine);
1293 const iString * text = &curLine->text;
1076 /* The `gap_UI` offsets below are a hack. They are used because for some reason the 1294 /* The `gap_UI` offsets below are a hack. They are used because for some reason the
1077 cursor rect and the glyph inside don't quite position like during `run_Text_()`. */ 1295 cursor rect and the glyph inside don't quite position like during `run_Text_()`. */
1078 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor); 1296 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor - curLine->offset);
1079 const iInt2 curPos = addX_I2(textOrigin, prefixSize.x + 1297 const iInt2 curPos = addX_I2(addY_I2(contentBounds.pos, lineHeight_Text(d->font) * d->cursorLine),
1298 prefixSize.x +
1080 (d->mode == insert_InputMode ? -curSize.x / 2 : 0)); 1299 (d->mode == insert_InputMode ? -curSize.x / 2 : 0));
1081 const iRect curRect = { curPos, curSize }; 1300 const iRect curRect = { curPos, curSize };
1082 fillRect_Paint(&p, curRect, uiInputCursor_ColorId); 1301 fillRect_Paint(&p, curRect, uiInputCursor_ColorId);
1083 if (d->mode == overwrite_InputMode) { 1302 if (d->mode == overwrite_InputMode) {
1084 draw_Text(d->font, addX_I2(curPos, iMin(1, gap_UI / 8)), uiInputCursorText_ColorId, "%s", cstr_String(&cur)); 1303 draw_Text(d->font,
1304 addX_I2(curPos, iMin(1, gap_UI / 8)),
1305 uiInputCursorText_ColorId,
1306 "%s",
1307 cstr_String(&cur));
1085 deinit_String(&cur); 1308 deinit_String(&cur);
1086 } 1309 }
1087 } 1310 }
1088 delete_String(text); 1311// delete_String(text);
1089 drawChildren_Widget(w); 1312 drawChildren_Widget(w);
1090} 1313}
1091 1314
1315//static void sizeChanged_InputWidget_(iInputWidget *d) {
1316// printf("[InputWidget] %p: size changed, updating layout\n", d);
1317// updateLinesAndResize_InputWidget_(d, iFalse);
1318//}
1319
1092iBeginDefineSubclass(InputWidget, Widget) 1320iBeginDefineSubclass(InputWidget, Widget)
1093 .processEvent = (iAny *) processEvent_InputWidget_, 1321 .processEvent = (iAny *) processEvent_InputWidget_,
1094 .draw = (iAny *) draw_InputWidget_, 1322 .draw = (iAny *) draw_InputWidget_,
1323// .sizeChanged = (iAny *) sizeChanged_InputWidget_,
1095iEndDefineSubclass(InputWidget) 1324iEndDefineSubclass(InputWidget)
diff --git a/src/ui/root.c b/src/ui/root.c
index 43fadbfa..8db8f7af 100644
--- a/src/ui/root.c
+++ b/src/ui/root.c
@@ -285,6 +285,16 @@ void destroyPending_Root(iRoot *d) {
285 setCurrent_Root(NULL); 285 setCurrent_Root(NULL);
286} 286}
287 287
288void postArrange_Root(iRoot *d) {
289 if (!d->pendingArrange) {
290 d->pendingArrange = iTrue;
291 SDL_Event ev = { .type = SDL_USEREVENT };
292 ev.user.code = arrange_UserEventCode;
293 ev.user.data2 = d;
294 SDL_PushEvent(&ev);
295 }
296}
297
288iPtrArray *onTop_Root(iRoot *d) { 298iPtrArray *onTop_Root(iRoot *d) {
289 if (!d->onTop) { 299 if (!d->onTop) {
290 d->onTop = new_PtrArray(); 300 d->onTop = new_PtrArray();
@@ -337,6 +347,12 @@ static iBool handleRootCommands_(iWidget *root, const char *cmd) {
337 setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id")))); 347 setFocus_Widget(findWidget_App(cstr_Rangecc(range_Command(cmd, "id"))));
338 return iTrue; 348 return iTrue;
339 } 349 }
350 else if (equal_Command(cmd, "input.resized")) {
351 /* No parent handled this, so do a full rearrangement. */
352 arrange_Widget(root);
353 postRefresh_App();
354 return iTrue;
355 }
340 else if (equal_Command(cmd, "window.focus.lost")) { 356 else if (equal_Command(cmd, "window.focus.lost")) {
341#if !defined (iPlatformMobile) /* apps don't share input focus on mobile */ 357#if !defined (iPlatformMobile) /* apps don't share input focus on mobile */
342 setFocus_Widget(NULL); 358 setFocus_Widget(NULL);
diff --git a/src/ui/root.h b/src/ui/root.h
index 00555224..96864a15 100644
--- a/src/ui/root.h
+++ b/src/ui/root.h
@@ -11,6 +11,7 @@ struct Impl_Root {
11 iWidget * widget; 11 iWidget * widget;
12 iPtrArray *onTop; /* order is important; last one is topmost */ 12 iPtrArray *onTop; /* order is important; last one is topmost */
13 iPtrSet * pendingDestruction; 13 iPtrSet * pendingDestruction;
14 iBool pendingArrange;
14 int loadAnimTimer; 15 int loadAnimTimer;
15 iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */ 16 iColor tmPalette[tmMax_ColorId]; /* theme-specific palette */
16}; 17};
@@ -28,6 +29,7 @@ iAnyObject *findWidget_Root (const char *id); /* under curre
28 29
29iPtrArray * onTop_Root (iRoot *); 30iPtrArray * onTop_Root (iRoot *);
30void destroyPending_Root (iRoot *); 31void destroyPending_Root (iRoot *);
32void postArrange_Root (iRoot *);
31 33
32void updateMetrics_Root (iRoot *); 34void updateMetrics_Root (iRoot *);
33void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */ 35void updatePadding_Root (iRoot *); /* TODO: is part of metrics? */
@@ -39,4 +41,4 @@ iRect rect_Root (const iRoot *);
39iRect safeRect_Root (const iRoot *); 41iRect safeRect_Root (const iRoot *);
40iInt2 visibleSize_Root (const iRoot *); /* may be obstructed by software keyboard */ 42iInt2 visibleSize_Root (const iRoot *); /* may be obstructed by software keyboard */
41iBool isNarrow_Root (const iRoot *); 43iBool isNarrow_Root (const iRoot *);
42int appIconSize_Root (void); \ No newline at end of file 44int appIconSize_Root (void);
diff --git a/src/ui/text.c b/src/ui/text.c
index 9532e35a..aacc8d3d 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -923,6 +923,13 @@ static iRect run_Font_(iFont *d, const iRunArgs *args) {
923 } 923 }
924 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */ 924 /* TODO: Check out if `uc_wordbreak_property()` from libunistring can be used here. */
925 if (ch == '\n') { 925 if (ch == '\n') {
926 if (args->xposLimit > 0 && ~mode & noWrapFlag_RunMode) {
927 /* Stop the line here, this is a hard warp. */
928 if (args->continueFrom_out) {
929 *args->continueFrom_out = chPos;
930 }
931 break;
932 }
926 xpos = xposExtend = orig.x; 933 xpos = xposExtend = orig.x;
927 ypos += d->height; 934 ypos += d->height;
928 prevCh = ch; 935 prevCh = ch;