summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt2
-rw-r--r--src/app.c21
-rw-r--r--src/app.h11
-rw-r--r--src/gmdocument.c2
-rw-r--r--src/ui/documentwidget.c68
-rw-r--r--src/ui/inputwidget.c543
-rw-r--r--src/ui/inputwidget.h5
-rw-r--r--src/ui/root.c31
-rw-r--r--src/ui/root.h4
-rw-r--r--src/ui/text.c7
-rw-r--r--src/ui/util.c40
-rw-r--r--src/ui/util.h2
-rw-r--r--src/ui/widget.c13
-rw-r--r--src/ui/widget.h1
14 files changed, 591 insertions, 159 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 84e81e9f..17fded2b 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,7 +18,7 @@
18cmake_minimum_required (VERSION 3.9) 18cmake_minimum_required (VERSION 3.9)
19 19
20project (Lagrange 20project (Lagrange
21 VERSION 1.4.1 21 VERSION 1.5.0
22 DESCRIPTION "A Beautiful Gemini Client" 22 DESCRIPTION "A Beautiful Gemini Client"
23 LANGUAGES C 23 LANGUAGES C
24) 24)
diff --git a/src/app.c b/src/app.c
index 3b6752de..6d172c02 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1049,6 +1049,23 @@ void processEvents_App(enum iAppEventMode eventMode) {
1049 } 1049 }
1050 continue; 1050 continue;
1051 } 1051 }
1052 else if (ev.type == SDL_USEREVENT && ev.user.code == arrange_UserEventCode) {
1053 printf("[App] rearrange\n");
1054 resize_Window(d->window, -1, -1);
1055 iForIndices(i, d->window->roots) {
1056 if (d->window->roots[i]) {
1057 d->window->roots[i]->pendingArrange = iFalse;
1058 }
1059 }
1060// if (ev.user.data2 == d->window->roots[0]) {
1061// arrange_Widget(d->window->roots[0]->widget);
1062// }
1063// else if (d->window->roots[1]) {
1064// arrange_Widget(d->window->roots[1]->widget);
1065// }
1066// postRefresh_App();
1067 continue;
1068 }
1052 d->lastEventTime = SDL_GetTicks(); 1069 d->lastEventTime = SDL_GetTicks();
1053 if (d->isIdling) { 1070 if (d->isIdling) {
1054// printf("[App] ...woke up\n"); 1071// printf("[App] ...woke up\n");
@@ -1561,6 +1578,10 @@ static iBool handlePrefsCommands_(iWidget *d, const char *cmd) {
1561 setToggle_Widget(findChild_Widget(d, "prefs.ostheme"), iFalse); 1578 setToggle_Widget(findChild_Widget(d, "prefs.ostheme"), iFalse);
1562 } 1579 }
1563 } 1580 }
1581 else if (equalWidget_Command(cmd, d, "input.resized")) {
1582 updatePreferencesLayout_Widget(d);
1583 return iFalse;
1584 }
1564 return iFalse; 1585 return iFalse;
1565} 1586}
1566 1587
diff --git a/src/app.h b/src/app.h
index c8f0f1c2..aa647bfb 100644
--- a/src/app.h
+++ b/src/app.h
@@ -57,14 +57,15 @@ enum iAppEventMode {
57 57
58enum iUserEventCode { 58enum iUserEventCode {
59 command_UserEventCode = 1, 59 command_UserEventCode = 1,
60 refresh_UserEventCode = 2, 60 refresh_UserEventCode,
61 asleep_UserEventCode = 3, 61 arrange_UserEventCode,
62 asleep_UserEventCode,
62 /* The start of a potential touch tap event is notified via a custom event because 63 /* The start of a potential touch tap event is notified via a custom event because
63 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will 64 sending SDL_MOUSEBUTTONDOWN would be premature: we don't know how long the tap will
64 take, it could turn into a tap-and-hold for example. */ 65 take, it could turn into a tap-and-hold for example. */
65 widgetTapBegins_UserEventCode = 4, 66 widgetTapBegins_UserEventCode,
66 widgetTouchEnds_UserEventCode = 5, /* finger lifted, but momentum may continue */ 67 widgetTouchEnds_UserEventCode, /* finger lifted, but momentum may continue */
67 immediateRefresh_UserEventCode = 6, /* refresh even though more events are pending */ 68 immediateRefresh_UserEventCode, /* refresh even though more events are pending */
68}; 69};
69 70
70const iString *execPath_App (void); 71const iString *execPath_App (void);
diff --git a/src/gmdocument.c b/src/gmdocument.c
index da99fd0d..4fc0dd5e 100644
--- a/src/gmdocument.c
+++ b/src/gmdocument.c
@@ -726,7 +726,7 @@ static void doLayout_GmDocument_(iGmDocument *d) {
726 const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text; 726 const int wrapAvail = d->size.x - run.bounds.pos.x - rightMargin * gap_Text;
727 const int avail = isWordWrapped ? wrapAvail : 0; 727 const int avail = isWordWrapped ? wrapAvail : 0;
728 const char *contPos; 728 const char *contPos;
729 const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos); 729 const iInt2 dims = tryAdvance_Text(run.font, runLine, avail, &contPos);
730 iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x)); 730 iChangeFlags(run.flags, wide_GmRunFlag, (isPreformat && dims.x > d->size.x));
731 run.bounds.size.x = iMax(wrapAvail, dims.x); /* Extends to the right edge for selection. */ 731 run.bounds.size.x = iMax(wrapAvail, dims.x); /* Extends to the right edge for selection. */
732 run.bounds.size.y = dims.y; 732 run.bounds.size.y = dims.y;
diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c
index 048f8ce4..d23e95a6 100644
--- a/src/ui/documentwidget.c
+++ b/src/ui/documentwidget.c
@@ -304,7 +304,7 @@ void init_DocumentWidget(iDocumentWidget *d) {
304 iWidget *w = as_Widget(d); 304 iWidget *w = as_Widget(d);
305 init_Widget(w); 305 init_Widget(w);
306 setId_Widget(w, format_CStr("document%03d", ++docEnum_)); 306 setId_Widget(w, format_CStr("document%03d", ++docEnum_));
307 setFlags_Widget(w, hover_WidgetFlag, iTrue); 307 setFlags_Widget(w, hover_WidgetFlag | noBackground_WidgetFlag, iTrue);
308 init_PersistentDocumentState(&d->mod); 308 init_PersistentDocumentState(&d->mod);
309 d->flags = 0; 309 d->flags = 0;
310 d->phoneToolbar = NULL; 310 d->phoneToolbar = NULL;
@@ -1571,6 +1571,43 @@ static void togglePreFold_DocumentWidget_(iDocumentWidget *d, uint16_t preId) {
1571 refresh_Widget(as_Widget(d)); 1571 refresh_Widget(as_Widget(d));
1572} 1572}
1573 1573
1574static iString *makeQueryUrl_DocumentWidget_(const iDocumentWidget *d,
1575 const iString *userEnteredText) {
1576 iString *url = copy_String(d->mod.url);
1577 /* Remove the existing query string. */
1578 const size_t qPos = indexOfCStr_String(url, "?");
1579 if (qPos != iInvalidPos) {
1580 remove_Block(&url->chars, qPos, iInvalidSize);
1581 }
1582 appendCStr_String(url, "?");
1583 append_String(url, collect_String(urlEncode_String(userEnteredText)));
1584 return url;
1585}
1586
1587static void inputQueryValidator_(iInputWidget *input, void *context) {
1588 iDocumentWidget *d = context;
1589 iString *url = makeQueryUrl_DocumentWidget_(d, text_InputWidget(input));
1590 iWidget *dlg = parent_Widget(input);
1591 iLabelWidget *counter = findChild_Widget(dlg, "valueinput.counter");
1592 iAssert(counter);
1593 int avail = 1024 - (int) size_String(url);
1594 setFlags_Widget(findChild_Widget(dlg, "default"), disabled_WidgetFlag, avail < 0);
1595 setEnterKeyEnabled_InputWidget(input, avail >= 0);
1596 int len = length_String(text_InputWidget(input));
1597 if (len > 1024) {
1598 iString *trunc = copy_String(text_InputWidget(input));
1599 truncate_String(trunc, 1024);
1600 setText_InputWidget(input, trunc);
1601 delete_String(trunc);
1602 }
1603 setTextCStr_LabelWidget(counter, format_CStr("%d", avail)); /* Gemini URL maxlen */
1604 setTextColor_LabelWidget(counter,
1605 avail < 0 ? uiTextCaution_ColorId :
1606 avail < 128 ? uiTextStrong_ColorId
1607 : uiTextDim_ColorId);
1608 delete_String(url);
1609}
1610
1574static void checkResponse_DocumentWidget_(iDocumentWidget *d) { 1611static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1575 if (!d->request) { 1612 if (!d->request) {
1576 return; 1613 return;
@@ -1598,13 +1635,21 @@ static void checkResponse_DocumentWidget_(iDocumentWidget *d) {
1598 isEmpty_String(&resp->meta) 1635 isEmpty_String(&resp->meta)
1599 ? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path)) 1636 ? format_CStr(cstr_Lang("dlg.input.prompt"), cstr_Rangecc(parts.path))
1600 : cstr_String(&resp->meta), 1637 : cstr_String(&resp->meta),
1601 uiTextCaution_ColorEscape "${dlg.input.send} \u21d2", 1638 uiTextCaution_ColorEscape "${dlg.input.send}",
1602 format_CStr("!document.input.submit doc:%p", d)); 1639 format_CStr("!document.input.submit doc:%p", d));
1640 setId_Widget(addChildPosFlags_Widget(findChild_Widget(dlg, "dialogbuttons"),
1641 iClob(new_LabelWidget("", NULL)),
1642 front_WidgetAddPos, frameless_WidgetFlag),
1643 "valueinput.counter");
1644 setValidator_InputWidget(findChild_Widget(dlg, "input"), inputQueryValidator_, d);
1603 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"), 1645 setSensitiveContent_InputWidget(findChild_Widget(dlg, "input"),
1604 statusCode == sensitiveInput_GmStatusCode); 1646 statusCode == sensitiveInput_GmStatusCode);
1605 if (document_App() != d) { 1647 if (document_App() != d) {
1606 postCommandf_App("tabs.switch page:%p", d); 1648 postCommandf_App("tabs.switch page:%p", d);
1607 } 1649 }
1650 else {
1651 updateTheme_DocumentWidget_(d);
1652 }
1608 break; 1653 break;
1609 } 1654 }
1610 case categorySuccess_GmStatusCode: 1655 case categorySuccess_GmStatusCode:
@@ -2229,17 +2274,10 @@ static iBool handleCommand_DocumentWidget_(iDocumentWidget *d, const char *cmd)
2229 return iTrue; 2274 return iTrue;
2230 } 2275 }
2231 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) { 2276 else if (equal_Command(cmd, "document.input.submit") && document_Command(cmd) == d) {
2232 iString *value = suffix_Command(cmd, "value"); 2277 postCommandf_Root(w->root,
2233 set_String(value, collect_String(urlEncode_String(value))); 2278 "open url:%s",
2234 iString *url = collect_String(copy_String(d->mod.url)); 2279 cstrCollect_String(makeQueryUrl_DocumentWidget_
2235 const size_t qPos = indexOfCStr_String(url, "?"); 2280 (d, collect_String(suffix_Command(cmd, "value")))));
2236 if (qPos != iInvalidPos) {
2237 remove_Block(&url->chars, qPos, iInvalidSize);
2238 }
2239 appendCStr_String(url, "?");
2240 append_String(url, value);
2241 postCommandf_Root(w->root, "open url:%s", cstr_String(url));
2242 delete_String(value);
2243 return iTrue; 2281 return iTrue;
2244 } 2282 }
2245 else if (equal_Command(cmd, "valueinput.cancelled") && 2283 else if (equal_Command(cmd, "valueinput.cancelled") &&
@@ -4172,7 +4210,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4172 if (width_Rect(bounds) <= 0) { 4210 if (width_Rect(bounds) <= 0) {
4173 return; 4211 return;
4174 } 4212 }
4175 draw_Widget(w); 4213// draw_Widget(w);
4176 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) { 4214 if (d->drawBufs->flags & updateTimestampBuf_DrawBufsFlag) {
4177 updateTimestampBuf_DocumentWidget_(d); 4215 updateTimestampBuf_DocumentWidget_(d);
4178 } 4216 }
@@ -4250,7 +4288,7 @@ static void draw_DocumentWidget_(const iDocumentWidget *d) {
4250 if (colorTheme_App() == pureWhite_ColorTheme) { 4288 if (colorTheme_App() == pureWhite_ColorTheme) {
4251 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId); 4289 drawHLine_Paint(&ctx.paint, topLeft_Rect(bounds), width_Rect(bounds), uiSeparator_ColorId);
4252 } 4290 }
4253 draw_Widget(w); 4291 drawChildren_Widget(w);
4254 /* Alt text. */ 4292 /* Alt text. */
4255 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5; 4293 const float altTextOpacity = value_Anim(&d->altTextOpacity) * 6 - 5;
4256 if (d->hoverAltPre && altTextOpacity > 0) { 4294 if (d->hoverAltPre && altTextOpacity > 0) {
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index a1635128..5f86f5bf 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -75,21 +75,48 @@ enum iInputWidgetFlag {
75 isMarking_InputWidgetFlag = iBit(7), 75 isMarking_InputWidgetFlag = iBit(7),
76 markWords_InputWidgetFlag = iBit(8), 76 markWords_InputWidgetFlag = iBit(8),
77 needUpdateBuffer_InputWidgetFlag = iBit(9), 77 needUpdateBuffer_InputWidgetFlag = iBit(9),
78 enterKeyEnabled_InputWidgetFlag = iBit(10),
78}; 79};
79 80
81/*----------------------------------------------------------------------------------------------*/
82
83iDeclareType(InputLine)
84
85struct Impl_InputLine {
86 size_t offset; /* character position from the beginning */
87 iString text; /* UTF-8 */
88};
89
90static void init_InputLine(iInputLine *d) {
91 d->offset = 0;
92 init_String(&d->text);
93}
94
95static void deinit_InputLine(iInputLine *d) {
96 deinit_String(&d->text);
97}
98
99iDefineTypeConstruction(InputLine)
100
101/*----------------------------------------------------------------------------------------------*/
102
80struct Impl_InputWidget { 103struct Impl_InputWidget {
81 iWidget widget; 104 iWidget widget;
82 enum iInputMode mode; 105 enum iInputMode mode;
83 int inFlags; 106 int inFlags;
84 size_t maxLen; 107 size_t maxLen;
108 size_t maxLayoutLines;
85 iArray text; /* iChar[] */ 109 iArray text; /* iChar[] */
86 iArray oldText; /* iChar[] */ 110 iArray oldText; /* iChar[] */
111 iArray lines;
87 iString hint; 112 iString hint;
88 iString srcHint; 113 iString srcHint;
89 int leftPadding; 114 int leftPadding;
90 int rightPadding; 115 int rightPadding;
91 size_t cursor; 116 size_t cursor; /* offset from beginning */
92 size_t lastCursor; 117 size_t lastCursor;
118 size_t cursorLine;
119 int verticalMoveX;
93 iRanges mark; 120 iRanges mark;
94 iRanges initialMark; 121 iRanges initialMark;
95 iArray undoStack; 122 iArray undoStack;
@@ -98,6 +125,8 @@ struct Impl_InputWidget {
98 int cursorVis; 125 int cursorVis;
99 uint32_t timer; 126 uint32_t timer;
100 iTextBuf * buffered; 127 iTextBuf * buffered;
128 iInputWidgetValidatorFunc validator;
129 void * validatorContext;
101}; 130};
102 131
103iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) 132iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
@@ -109,8 +138,20 @@ static void clearUndo_InputWidget_(iInputWidget *d) {
109 clear_Array(&d->undoStack); 138 clear_Array(&d->undoStack);
110} 139}
111 140
141static void updateCursorLine_InputWidget_(iInputWidget *d) {
142 d->cursorLine = 0;
143 iConstForEach(Array, i, &d->lines) {
144 const iInputLine *line = i.value;
145 if (line->offset > d->cursor) {
146 break;
147 }
148 d->cursorLine = index_ArrayConstIterator(&i);
149 }
150}
151
112static void showCursor_InputWidget_(iInputWidget *d) { 152static void showCursor_InputWidget_(iInputWidget *d) {
113 d->cursorVis = 2; 153 d->cursorVis = 2;
154 updateCursorLine_InputWidget_(d);
114} 155}
115 156
116static void invalidateBuffered_InputWidget_(iInputWidget *d) { 157static void invalidateBuffered_InputWidget_(iInputWidget *d) {
@@ -120,6 +161,21 @@ static void invalidateBuffered_InputWidget_(iInputWidget *d) {
120 } 161 }
121} 162}
122 163
164iLocalDef iInt2 padding_(void) {
165 return init_I2(gap_UI / 2, gap_UI / 2);
166}
167
168static 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
123static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) { 179static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
124 if (d->maxLen) { 180 if (d->maxLen) {
125 /* Set a fixed size based on maximum possible width of the text. */ 181 /* Set a fixed size based on maximum possible width of the text. */
@@ -135,29 +191,122 @@ static void updateSizeForFixedLength_InputWidget_(iInputWidget *d) {
135 } 191 }
136} 192}
137 193
194static const iChar sensitiveChar_ = 0x25cf; /* black circle */
195
196static iString *utf32toUtf8_InputWidget_(const iInputWidget *d) {
197 return newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
198}
199
200static iString *visText_InputWidget_(const iInputWidget *d) {
201 iString *text;
202 if (~d->inFlags & isSensitive_InputWidgetFlag) {
203 text = utf32toUtf8_InputWidget_(d);
204 }
205 else {
206 text = new_String();
207 for (size_t i = 0; i < size_Array(&d->text); ++i) {
208 appendChar_String(text, sensitiveChar_);
209 }
210 }
211 return text;
212}
213
214static void clearLines_InputWidget_(iInputWidget *d) {
215 iForEach(Array, i, &d->lines) {
216 deinit_InputLine(i.value);
217 }
218 clear_Array(&d->lines);
219}
220
221static void updateLines_InputWidget_(iInputWidget *d) {
222 clearLines_InputWidget_(d);
223 if (d->maxLen) {
224 /* Everything on a single line. */
225 iInputLine line;
226 init_InputLine(&line);
227 iString *u8 = visText_InputWidget_(d);
228 set_String(&line.text, u8);
229 delete_String(u8);
230 pushBack_Array(&d->lines, &line);
231 updateCursorLine_InputWidget_(d);
232 return;
233 }
234 /* Word-wrapped lines. */
235 iString *u8 = visText_InputWidget_(d);
236 size_t charPos = 0;
237 iRangecc content = range_String(u8);
238 const int wrapWidth = contentBounds_InputWidget_(d).size.x;
239 while (wrapWidth > 0 && content.end != content.start) {
240 const char *endPos;
241 if (d->inFlags & isUrl_InputWidgetFlag) {
242 tryAdvanceNoWrap_Text(d->font, content, wrapWidth, &endPos);
243 }
244 else {
245 tryAdvance_Text(d->font, content, wrapWidth, &endPos);
246 }
247 const iRangecc part = (iRangecc){ content.start, endPos };
248 iInputLine line;
249 init_InputLine(&line);
250 setRange_String(&line.text, part);
251 line.offset = charPos;
252 pushBack_Array(&d->lines, &line);
253 charPos += length_String(&line.text);
254 content.start = endPos;
255 }
256 if (isEmpty_Array(&d->lines) || endsWith_String(u8, "\n")) {
257 /* Always at least one empty line. */
258 iInputLine line;
259 init_InputLine(&line);
260 pushBack_Array(&d->lines, &line);
261 }
262 else {
263 iAssert(charPos == length_String(u8));
264 }
265 delete_String(u8);
266 updateCursorLine_InputWidget_(d);
267}
268
269static int contentHeight_InputWidget_(const iInputWidget *d, iBool forLayout) {
270 size_t numLines = iMax(1, size_Array(&d->lines));
271 if (forLayout) {
272 numLines = iMin(numLines, d->maxLayoutLines);
273 }
274 return numLines * lineHeight_Text(d->font);
275}
276
138static void updateMetrics_InputWidget_(iInputWidget *d) { 277static void updateMetrics_InputWidget_(iInputWidget *d) {
139 iWidget *w = as_Widget(d); 278 iWidget *w = as_Widget(d);
140 updateSizeForFixedLength_InputWidget_(d); 279 updateSizeForFixedLength_InputWidget_(d);
141 /* Caller must arrange the width, but the height is fixed. */ 280 /* Caller must arrange the width, but the height is fixed. */
142 w->rect.size.y = lineHeight_Text(d->font) * 1.3f; 281 w->rect.size.y = contentHeight_InputWidget_(d, iTrue) + 3 * padding_().y; /* TODO: Why 3x? */
143 if (flags_Widget(w) & extraPadding_WidgetFlag) { 282 if (flags_Widget(w) & extraPadding_WidgetFlag) {
144 w->rect.size.y += 2 * gap_UI; 283 w->rect.size.y += 2 * gap_UI;
145 } 284 }
146 invalidateBuffered_InputWidget_(d); 285 invalidateBuffered_InputWidget_(d);
147 if (parent_Widget(w)) { 286 postCommand_Widget(d, "input.resized");
148 arrange_Widget(w); 287}
288
289static void updateLinesAndResize_InputWidget_(iInputWidget *d) {
290 const size_t oldCount = size_Array(&d->lines);
291 updateLines_InputWidget_(d);
292 if (oldCount != size_Array(&d->lines)) {
293 d->click.minHeight = contentHeight_InputWidget_(d, iFalse);
294 updateMetrics_InputWidget_(d);
149 } 295 }
150} 296}
151 297
152void init_InputWidget(iInputWidget *d, size_t maxLen) { 298void init_InputWidget(iInputWidget *d, size_t maxLen) {
153 iWidget *w = &d->widget; 299 iWidget *w = &d->widget;
154 init_Widget(w); 300 init_Widget(w);
301 d->validator = NULL;
302 d->validatorContext = NULL;
155 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag | touchDrag_WidgetFlag, iTrue); 303 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag | touchDrag_WidgetFlag, iTrue);
156#if defined (iPlatformMobile) 304#if defined (iPlatformMobile)
157 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue); 305 setFlags_Widget(w, extraPadding_WidgetFlag, iTrue);
158#endif 306#endif
159 init_Array(&d->text, sizeof(iChar)); 307 init_Array(&d->text, sizeof(iChar));
160 init_Array(&d->oldText, sizeof(iChar)); 308 init_Array(&d->oldText, sizeof(iChar));
309 init_Array(&d->lines, sizeof(iInputLine));
161 init_String(&d->hint); 310 init_String(&d->hint);
162 init_String(&d->srcHint); 311 init_String(&d->srcHint);
163 init_Array(&d->undoStack, sizeof(iInputUndo)); 312 init_Array(&d->undoStack, sizeof(iInputUndo));
@@ -166,18 +315,23 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
166 d->rightPadding = 0; 315 d->rightPadding = 0;
167 d->cursor = 0; 316 d->cursor = 0;
168 d->lastCursor = 0; 317 d->lastCursor = 0;
169 d->inFlags = eatEscape_InputWidgetFlag; 318 d->cursorLine = 0;
319 d->verticalMoveX = -1; /* TODO: Use this. */
320 d->inFlags = eatEscape_InputWidgetFlag | enterKeyEnabled_InputWidgetFlag;
170 iZap(d->mark); 321 iZap(d->mark);
171 setMaxLen_InputWidget(d, maxLen); 322 setMaxLen_InputWidget(d, maxLen);
323 d->maxLayoutLines = iInvalidSize;
172 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue); 324 setFlags_Widget(w, fixedHeight_WidgetFlag, iTrue);
173 init_Click(&d->click, d, SDL_BUTTON_LEFT); 325 init_Click(&d->click, d, SDL_BUTTON_LEFT);
174 d->timer = 0; 326 d->timer = 0;
175 d->cursorVis = 0; 327 d->cursorVis = 0;
176 d->buffered = NULL; 328 d->buffered = NULL;
329 updateLines_InputWidget_(d);
177 updateMetrics_InputWidget_(d); 330 updateMetrics_InputWidget_(d);
178} 331}
179 332
180void deinit_InputWidget(iInputWidget *d) { 333void deinit_InputWidget(iInputWidget *d) {
334 clearLines_InputWidget_(d);
181 if (isSelected_Widget(d)) { 335 if (isSelected_Widget(d)) {
182 SDL_StopTextInput(); 336 SDL_StopTextInput();
183 enableEditorKeysInMenus_(iTrue); 337 enableEditorKeysInMenus_(iTrue);
@@ -190,6 +344,7 @@ void deinit_InputWidget(iInputWidget *d) {
190 } 344 }
191 deinit_String(&d->srcHint); 345 deinit_String(&d->srcHint);
192 deinit_String(&d->hint); 346 deinit_String(&d->hint);
347 deinit_Array(&d->lines);
193 deinit_Array(&d->oldText); 348 deinit_Array(&d->oldText);
194 deinit_Array(&d->text); 349 deinit_Array(&d->text);
195} 350}
@@ -199,6 +354,11 @@ void setFont_InputWidget(iInputWidget *d, int fontId) {
199 updateMetrics_InputWidget_(d); 354 updateMetrics_InputWidget_(d);
200} 355}
201 356
357static const iInputLine *line_InputWidget_(const iInputWidget *d, size_t index) {
358 iAssert(!isEmpty_Array(&d->lines));
359 return constAt_Array(&d->lines, index);
360}
361
202static void pushUndo_InputWidget_(iInputWidget *d) { 362static void pushUndo_InputWidget_(iInputWidget *d) {
203 iInputUndo undo; 363 iInputUndo undo;
204 init_InputUndo_(&undo, &d->text, d->cursor); 364 init_InputUndo_(&undo, &d->text, d->cursor);
@@ -241,10 +401,6 @@ static const iString *omitDefaultScheme_(iString *url) {
241 return url; 401 return url;
242} 402}
243 403
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) { 404const iString *text_InputWidget(const iInputWidget *d) {
249 if (d) { 405 if (d) {
250 iString *text = collect_String(utf32toUtf8_InputWidget_(d)); 406 iString *text = collect_String(utf32toUtf8_InputWidget_(d));
@@ -267,6 +423,20 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
267 updateSizeForFixedLength_InputWidget_(d); 423 updateSizeForFixedLength_InputWidget_(d);
268} 424}
269 425
426void setMaxLayoutLines_InputWidget(iInputWidget *d, size_t maxLayoutLines) {
427 d->maxLayoutLines = maxLayoutLines;
428 updateMetrics_InputWidget_(d);
429}
430
431void setValidator_InputWidget(iInputWidget *d, iInputWidgetValidatorFunc validator, void *context) {
432 d->validator = validator;
433 d->validatorContext = context;
434}
435
436void setEnterKeyEnabled_InputWidget(iInputWidget *d, iBool enterKeyEnabled) {
437 iChangeFlags(d->inFlags, enterKeyEnabled_InputWidgetFlag, enterKeyEnabled);
438}
439
270void setHint_InputWidget(iInputWidget *d, const char *hintText) { 440void setHint_InputWidget(iInputWidget *d, const char *hintText) {
271 /* Keep original for retranslations. */ 441 /* Keep original for retranslations. */
272 setCStr_String(&d->srcHint, hintText); 442 setCStr_String(&d->srcHint, hintText);
@@ -285,27 +455,12 @@ void setContentPadding_InputWidget(iInputWidget *d, int left, int right) {
285 refresh_Widget(d); 455 refresh_Widget(d);
286} 456}
287 457
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) { 458static void updateBuffered_InputWidget_(iInputWidget *d) {
305 iWindow *win = get_Window();
306 invalidateBuffered_InputWidget_(d); 459 invalidateBuffered_InputWidget_(d);
307 iString *bufText = NULL; 460 iString *bufText = NULL;
461#if 0
308 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) { 462 if (d->inFlags & isUrl_InputWidgetFlag && as_Widget(d)->root == win->keyRoot) {
463 /* TODO: Move this omitting to `updateLines_`? */
309 /* Highlight the host name. */ 464 /* Highlight the host name. */
310 iUrl parts; 465 iUrl parts;
311 const iString *text = collect_String(utf32toUtf8_InputWidget_(d)); 466 const iString *text = collect_String(utf32toUtf8_InputWidget_(d));
@@ -319,6 +474,7 @@ static void updateBuffered_InputWidget_(iInputWidget *d) {
319 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) }); 474 appendRange_String(bufText, (iRangecc){ parts.host.end, constEnd_String(text) });
320 } 475 }
321 } 476 }
477#endif
322 if (!bufText) { 478 if (!bufText) {
323 bufText = visText_InputWidget_(d); 479 bufText = visText_InputWidget_(d);
324 } 480 }
@@ -350,7 +506,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
350 } 506 }
351 if (isFocused_Widget(d)) { 507 if (isFocused_Widget(d)) {
352 d->cursor = size_Array(&d->text); 508 d->cursor = size_Array(&d->text);
353 selectAll_InputWidget(d); 509// selectAll_InputWidget(d);
354 } 510 }
355 else { 511 else {
356 d->cursor = iMin(d->cursor, size_Array(&d->text)); 512 d->cursor = iMin(d->cursor, size_Array(&d->text));
@@ -359,6 +515,7 @@ void setText_InputWidget(iInputWidget *d, const iString *text) {
359 if (!isFocused_Widget(d)) { 515 if (!isFocused_Widget(d)) {
360 d->inFlags |= needUpdateBuffer_InputWidgetFlag; 516 d->inFlags |= needUpdateBuffer_InputWidgetFlag;
361 } 517 }
518 updateLinesAndResize_InputWidget_(d);
362 refresh_Widget(as_Widget(d)); 519 refresh_Widget(as_Widget(d));
363} 520}
364 521
@@ -404,8 +561,9 @@ void begin_InputWidget(iInputWidget *d) {
404 else { 561 else {
405 d->cursor = iMin(size_Array(&d->text), d->maxLen - 1); 562 d->cursor = iMin(size_Array(&d->text), d->maxLen - 1);
406 } 563 }
564 updateCursorLine_InputWidget_(d);
407 SDL_StartTextInput(); 565 SDL_StartTextInput();
408 setFlags_Widget(w, selected_WidgetFlag, iTrue); 566 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iTrue);
409 showCursor_InputWidget_(d); 567 showCursor_InputWidget_(d);
410 refresh_Widget(w); 568 refresh_Widget(w);
411 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d); 569 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, cursorTimer_, d);
@@ -433,9 +591,10 @@ void end_InputWidget(iInputWidget *d, iBool accept) {
433 SDL_RemoveTimer(d->timer); 591 SDL_RemoveTimer(d->timer);
434 d->timer = 0; 592 d->timer = 0;
435 SDL_StopTextInput(); 593 SDL_StopTextInput();
436 setFlags_Widget(w, selected_WidgetFlag, iFalse); 594 setFlags_Widget(w, selected_WidgetFlag | keepOnTop_WidgetFlag, iFalse);
437 const char *id = cstr_String(id_Widget(as_Widget(d))); 595 const char *id = cstr_String(id_Widget(as_Widget(d)));
438 if (!*id) id = "_"; 596 if (!*id) id = "_";
597 updateLinesAndResize_InputWidget_(d);
439 refresh_Widget(w); 598 refresh_Widget(w);
440 postCommand_Widget(w, 599 postCommand_Widget(w,
441 "input.ended id:%s enter:%d arg:%d", 600 "input.ended id:%s enter:%d arg:%d",
@@ -476,7 +635,6 @@ iLocalDef iBool isMarking_(void) {
476} 635}
477 636
478void setCursor_InputWidget(iInputWidget *d, size_t pos) { 637void setCursor_InputWidget(iInputWidget *d, size_t pos) {
479 showCursor_InputWidget_(d);
480 if (isEmpty_Array(&d->text)) { 638 if (isEmpty_Array(&d->text)) {
481 d->cursor = 0; 639 d->cursor = 0;
482 } 640 }
@@ -496,6 +654,51 @@ void setCursor_InputWidget(iInputWidget *d, size_t pos) {
496 else { 654 else {
497 iZap(d->mark); 655 iZap(d->mark);
498 } 656 }
657 showCursor_InputWidget_(d);
658}
659
660static size_t indexForRelativeX_InputWidget_(const iInputWidget *d, int x, const iInputLine *line) {
661 if (x <= 0) {
662 return line->offset;
663 }
664 const char *endPos;
665 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), x, &endPos);
666 size_t index = line->offset;
667 if (endPos == constEnd_String(&line->text)) {
668 index += length_String(&line->text);
669 }
670 else {
671 /* Need to know the actual character index. */
672 /* TODO: tryAdvance could tell us this directly with an extra return value */
673 iConstForEach(String, i, &line->text) {
674 if (i.pos >= endPos) break;
675 index++;
676 }
677 }
678 return index;
679}
680
681static iBool moveCursorByLine_InputWidget_(iInputWidget *d, int dir) {
682 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
683 int xPos = advanceN_Text(d->font, cstr_String(&line->text), d->cursor - line->offset).x;
684 size_t newCursor = iInvalidPos;
685 const size_t numLines = size_Array(&d->lines);
686 if (dir < 0 && d->cursorLine > 0) {
687 newCursor = indexForRelativeX_InputWidget_(d, xPos, --line);
688 }
689 else if (dir > 0 && d->cursorLine < numLines - 1) {
690 newCursor = indexForRelativeX_InputWidget_(d, xPos, ++line);
691 }
692 if (newCursor != iInvalidPos) {
693 /* Clamp it to the current line. */
694 newCursor = iMax(newCursor, line->offset);
695 newCursor = iMin(newCursor, line->offset + length_String(&line->text) -
696 /* last line is allowed to go to the cursorMax */
697 ((const void *) line < constAt_Array(&d->lines, numLines - 1) ? 1 : 0));
698 setCursor_InputWidget(d, newCursor);
699 return iTrue;
700 }
701 return iFalse;
499} 702}
500 703
501void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) { 704void setSensitiveContent_InputWidget(iInputWidget *d, iBool isSensitive) {
@@ -526,6 +729,10 @@ static iRanges mark_InputWidget_(const iInputWidget *d) {
526} 729}
527 730
528static void contentsWereChanged_InputWidget_(iInputWidget *d) { 731static void contentsWereChanged_InputWidget_(iInputWidget *d) {
732 if (d->validator) {
733 d->validator(d, d->validatorContext); /* this may change the contents */
734 }
735 updateLinesAndResize_InputWidget_(d);
529 if (d->inFlags & notifyEdits_InputWidgetFlag) { 736 if (d->inFlags & notifyEdits_InputWidgetFlag) {
530 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d)))); 737 postCommand_Widget(d, "input.edited id:%s", cstr_String(id_Widget(constAs_Widget(d))));
531 } 738 }
@@ -591,21 +798,18 @@ static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir)
591 return pos; 798 return pos;
592} 799}
593 800
594iLocalDef iInt2 padding_(void) { 801#if 0
595 return init_I2(gap_UI / 2, gap_UI / 2); 802static iInt2 textOrigin_InputWidget_(const iInputWidget *d) { //}, const char *visText) {
596} 803// const iWidget *w = constAs_Widget(d);
597 804 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), 805 addX_I2(padding_(), d->leftPadding),
602 neg_I2(addX_I2(padding_(), d->rightPadding))); 806 neg_I2(addX_I2(padding_(), d->rightPadding)));*/
603 const iInt2 emSize = advance_Text(d->font, "M"); 807// const iInt2 emSize = advance_Text(d->font, "M");
604 const int textWidth = advance_Text(d->font, visText).x; 808// const int textWidth = advance_Text(d->font, visText).x;
605 const int cursorX = advanceN_Text(d->font, visText, d->cursor).x; 809// const int cursorX = advanceN_Text(d->font, visText, d->cursor).x;
606 int xOff = 0; 810// int xOff = 0;
607 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 811// shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
608 if (d->maxLen == 0) { 812/* if (d->maxLen == 0) {
609 if (textWidth > width_Rect(bounds) - emSize.x) { 813 if (textWidth > width_Rect(bounds) - emSize.x) {
610 xOff = width_Rect(bounds) - emSize.x - textWidth; 814 xOff = width_Rect(bounds) - emSize.x - textWidth;
611 } 815 }
@@ -613,32 +817,20 @@ static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText)
613 xOff = width_Rect(bounds) / 2 - cursorX; 817 xOff = width_Rect(bounds) / 2 - cursorX;
614 } 818 }
615 xOff = iMin(xOff, 0); 819 xOff = iMin(xOff, 0);
616 } 820 }*/
617 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; 821// 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)); 822// return addY_I2(topLeft_Rect(bounds), yOff);
823
619} 824}
825#endif
620 826
621static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) { 827static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
622 iString *visText = visText_InputWidget_(d); 828 const iInt2 pos = sub_I2(coord, contentBounds_InputWidget_(d).pos);
623 iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText))); 829 const size_t lineNumber = iMin(pos.y / lineHeight_Text(d->font), (int) size_Array(&d->lines) - 1);
624 size_t index = 0; 830 const iInputLine *line = line_InputWidget_(d, lineNumber);
625 if (pos.x > 0) { 831 const char *endPos;
626 const char *endPos; 832 tryAdvanceNoWrap_Text(d->font, range_String(&line->text), pos.x, &endPos);
627 tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos); 833 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} 834}
643 835
644static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) { 836static iBool copy_InputWidget_(iInputWidget *d, iBool doCut) {
@@ -683,6 +875,14 @@ static iChar at_InputWidget_(const iInputWidget *d, size_t pos) {
683 return *(const iChar *) constAt_Array(&d->text, pos); 875 return *(const iChar *) constAt_Array(&d->text, pos);
684} 876}
685 877
878static iRanges lineRange_InputWidget_(const iInputWidget *d) {
879 if (isEmpty_Array(&d->lines)) {
880 return (iRanges){ 0, 0 };
881 }
882 const iInputLine *line = line_InputWidget_(d, d->cursorLine);
883 return (iRanges){ line->offset, line->offset + length_String(&line->text) };
884}
885
686static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) { 886static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) {
687 const size_t textLen = size_Array(&d->text); 887 const size_t textLen = size_Array(&d->text);
688 if (dir < 0 && *pos > 0) { 888 if (dir < 0 && *pos > 0) {
@@ -700,6 +900,20 @@ static void extendRange_InputWidget_(iInputWidget *d, size_t *pos, int dir) {
700 } 900 }
701} 901}
702 902
903static iRect bounds_InputWidget_(const iInputWidget *d) {
904 const iWidget *w = constAs_Widget(d);
905 iRect bounds = bounds_Widget(w);
906 if (!isFocused_Widget(d)) {
907 return bounds;
908 }
909 bounds.size.y = contentHeight_InputWidget_(d, iFalse) + 3 * padding_().y;
910 return bounds;
911}
912
913static iBool contains_InputWidget_(const iInputWidget *d, iInt2 coord) {
914 return contains_Rect(bounds_InputWidget_(d), coord);
915}
916
703static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 917static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
704 iWidget *w = as_Widget(d); 918 iWidget *w = as_Widget(d);
705 if (isCommand_Widget(w, ev, "focus.gained")) { 919 if (isCommand_Widget(w, ev, "focus.gained")) {
@@ -746,19 +960,22 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
746 } 960 }
747 else if (isMetricsChange_UserEvent(ev)) { 961 else if (isMetricsChange_UserEvent(ev)) {
748 updateMetrics_InputWidget_(d); 962 updateMetrics_InputWidget_(d);
963 updateLinesAndResize_InputWidget_(d);
749 } 964 }
750 else if (isResize_UserEvent(ev)) { 965 else if (isResize_UserEvent(ev)) {
751 if (d->inFlags & isUrl_InputWidgetFlag) { 966 if (d->inFlags & isUrl_InputWidgetFlag) {
752 /* Restore/omit the default scheme if necessary. */ 967 /* Restore/omit the default scheme if necessary. */
753 setText_InputWidget(d, text_InputWidget(d)); 968 setText_InputWidget(d, text_InputWidget(d));
754 } 969 }
970 updateLinesAndResize_InputWidget_(d);
755 } 971 }
756 else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) { 972 else if (isFocused_Widget(d) && isCommand_UserEvent(ev, "copy")) {
757 copy_InputWidget_(d, iFalse); 973 copy_InputWidget_(d, iFalse);
758 return iTrue; 974 return iTrue;
759 } 975 }
760 if (ev->type == SDL_MOUSEMOTION && isHover_Widget(d)) { 976 if (ev->type == SDL_MOUSEMOTION && (isHover_Widget(d) || flags_Widget(w) & keepOnTop_WidgetFlag)) {
761 const iInt2 inner = windowToInner_Widget(w, init_I2(ev->motion.x, ev->motion.y)); 977 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
978 const iInt2 inner = windowToInner_Widget(w, coord);
762 setCursor_Window(get_Window(), 979 setCursor_Window(get_Window(),
763 inner.x >= 2 * gap_UI + d->leftPadding && 980 inner.x >= 2 * gap_UI + d->leftPadding &&
764 inner.x < width_Widget(w) - d->rightPadding 981 inner.x < width_Widget(w) - d->rightPadding
@@ -768,30 +985,38 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
768 switch (processEvent_Click(&d->click, ev)) { 985 switch (processEvent_Click(&d->click, ev)) {
769 case none_ClickResult: 986 case none_ClickResult:
770 break; 987 break;
771 case started_ClickResult: 988 case started_ClickResult: {
772 setFocus_Widget(w); 989 setFocus_Widget(w);
990 const size_t oldCursor = d->cursor;
773 setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click))); 991 setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click)));
774 iZap(d->mark); 992 if (keyMods_Sym(modState_Keys()) == KMOD_SHIFT) {
775 iZap(d->initialMark); 993 d->mark = d->initialMark = (iRanges){ oldCursor, d->cursor };
776 d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag); 994 d->inFlags |= isMarking_InputWidgetFlag;
777 if (d->click.count == 2) {
778 d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag;
779 d->mark.start = d->mark.end = d->cursor;
780 extendRange_InputWidget_(d, &d->mark.start, -1);
781 extendRange_InputWidget_(d, &d->mark.end, +1);
782 d->initialMark = d->mark;
783 refresh_Widget(w);
784 } 995 }
785 if (d->click.count == 3) { 996 else {
786 selectAll_InputWidget(d); 997 iZap(d->mark);
998 iZap(d->initialMark);
999 d->inFlags &= ~(isMarking_InputWidgetFlag | markWords_InputWidgetFlag);
1000 if (d->click.count == 2) {
1001 d->inFlags |= isMarking_InputWidgetFlag | markWords_InputWidgetFlag;
1002 d->mark.start = d->mark.end = d->cursor;
1003 extendRange_InputWidget_(d, &d->mark.start, -1);
1004 extendRange_InputWidget_(d, &d->mark.end, +1);
1005 d->initialMark = d->mark;
1006 refresh_Widget(w);
1007 }
1008 if (d->click.count == 3) {
1009 selectAll_InputWidget(d);
1010 }
787 } 1011 }
788 return iTrue; 1012 return iTrue;
1013 }
789 case aborted_ClickResult: 1014 case aborted_ClickResult:
790 d->inFlags &= ~isMarking_InputWidgetFlag; 1015 d->inFlags &= ~isMarking_InputWidgetFlag;
791 return iTrue; 1016 return iTrue;
792 case drag_ClickResult: 1017 case drag_ClickResult:
793 showCursor_InputWidget_(d);
794 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click)); 1018 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click));
1019 showCursor_InputWidget_(d);
795 if (~d->inFlags & isMarking_InputWidgetFlag) { 1020 if (~d->inFlags & isMarking_InputWidgetFlag) {
796 d->inFlags |= isMarking_InputWidgetFlag; 1021 d->inFlags |= isMarking_InputWidgetFlag;
797 d->mark.start = d->cursor; 1022 d->mark.start = d->cursor;
@@ -808,6 +1033,12 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
808 d->inFlags &= ~isMarking_InputWidgetFlag; 1033 d->inFlags &= ~isMarking_InputWidgetFlag;
809 return iTrue; 1034 return iTrue;
810 } 1035 }
1036 if (ev->type == SDL_MOUSEMOTION && flags_Widget(w) & keepOnTop_WidgetFlag) {
1037 const iInt2 coord = init_I2(ev->motion.x, ev->motion.y);
1038 if (contains_Click(&d->click, coord)) {
1039 return iTrue;
1040 }
1041 }
811 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT && 1042 if (ev->type == SDL_MOUSEBUTTONDOWN && ev->button.button == SDL_BUTTON_RIGHT &&
812 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) { 1043 contains_Widget(w, init_I2(ev->button.x, ev->button.y))) {
813 iWidget *clipMenu = findWidget_App("clipmenu"); 1044 iWidget *clipMenu = findWidget_App("clipmenu");
@@ -822,7 +1053,10 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
822 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 1053 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
823 return iTrue; 1054 return iTrue;
824 } 1055 }
825 const size_t curMax = cursorMax_InputWidget_(d); 1056 const size_t curMax = cursorMax_InputWidget_(d);
1057 const iRanges lineRange = lineRange_InputWidget_(d);
1058 const size_t lineFirst = lineRange.start;
1059 const size_t lineLast = lineRange.end == curMax ? curMax : iMax(lineRange.start, lineRange.end - 1);
826 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 1060 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
827 const int key = ev->key.keysym.sym; 1061 const int key = ev->key.keysym.sym;
828 const int mods = keyMods_Sym(ev->key.keysym.mod); 1062 const int mods = keyMods_Sym(ev->key.keysym.mod);
@@ -852,8 +1086,17 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
852 return iTrue; 1086 return iTrue;
853 case SDLK_RETURN: 1087 case SDLK_RETURN:
854 case SDLK_KP_ENTER: 1088 case SDLK_KP_ENTER:
855 d->inFlags |= enterPressed_InputWidgetFlag; 1089 if (mods == KMOD_SHIFT) {
856 setFocus_Widget(NULL); 1090 pushUndo_InputWidget_(d);
1091 deleteMarked_InputWidget_(d);
1092 insertChar_InputWidget_(d, '\n');
1093 contentsWereChanged_InputWidget_(d);
1094 return iTrue;
1095 }
1096 if (d->inFlags & enterKeyEnabled_InputWidgetFlag) {
1097 d->inFlags |= enterPressed_InputWidgetFlag;
1098 setFocus_Widget(NULL);
1099 }
857 return iTrue; 1100 return iTrue;
858 case SDLK_ESCAPE: 1101 case SDLK_ESCAPE:
859 end_InputWidget(d, iFalse); 1102 end_InputWidget(d, iFalse);
@@ -927,7 +1170,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
927 break; 1170 break;
928 case SDLK_HOME: 1171 case SDLK_HOME:
929 case SDLK_END: 1172 case SDLK_END:
930 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax); 1173 setCursor_InputWidget(d, key == SDLK_HOME ? lineFirst : lineLast);
931 refresh_Widget(w); 1174 refresh_Widget(w);
932 return iTrue; 1175 return iTrue;
933 case SDLK_a: 1176 case SDLK_a:
@@ -944,7 +1187,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
944 /* fall through for Emacs-style Home/End */ 1187 /* fall through for Emacs-style Home/End */
945 case SDLK_e: 1188 case SDLK_e:
946 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) { 1189 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
947 setCursor_InputWidget(d, key == 'a' ? 0 : curMax); 1190 setCursor_InputWidget(d, key == 'a' ? lineFirst : lineLast);
948 refresh_Widget(w); 1191 refresh_Widget(w);
949 return iTrue; 1192 return iTrue;
950 } 1193 }
@@ -953,7 +1196,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
953 case SDLK_RIGHT: { 1196 case SDLK_RIGHT: {
954 const int dir = (key == SDLK_LEFT ? -1 : +1); 1197 const int dir = (key == SDLK_LEFT ? -1 : +1);
955 if (mods & byLine_KeyModifier) { 1198 if (mods & byLine_KeyModifier) {
956 setCursor_InputWidget(d, dir < 0 ? 0 : curMax); 1199 setCursor_InputWidget(d, dir < 0 ? lineFirst : lineLast);
957 } 1200 }
958 else if (mods & byWord_KeyModifier) { 1201 else if (mods & byWord_KeyModifier) {
959 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir)); 1202 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir));
@@ -970,9 +1213,22 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
970 return iTrue; 1213 return iTrue;
971 } 1214 }
972 case SDLK_TAB: 1215 case SDLK_TAB:
973 case SDLK_DOWN: /* for moving to lookup from url entry */
974 /* Allow focus switching. */ 1216 /* Allow focus switching. */
975 return processEvent_Widget(as_Widget(d), ev); 1217 return processEvent_Widget(as_Widget(d), ev);
1218 case SDLK_UP:
1219 if (moveCursorByLine_InputWidget_(d, -1)) {
1220 refresh_Widget(d);
1221 return iTrue;
1222 }
1223 /* For moving to lookup from url entry. */
1224 return processEvent_Widget(as_Widget(d), ev);
1225 case SDLK_DOWN:
1226 if (moveCursorByLine_InputWidget_(d, +1)) {
1227 refresh_Widget(d);
1228 return iTrue;
1229 }
1230 /* For moving to lookup from url entry. */
1231 return processEvent_Widget(as_Widget(d), ev);
976 } 1232 }
977 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) { 1233 if (mods & (KMOD_PRIMARY | KMOD_SECONDARY)) {
978 return iFalse; 1234 return iFalse;
@@ -1003,21 +1259,23 @@ static iBool isWhite_(const iString *str) {
1003 1259
1004static void draw_InputWidget_(const iInputWidget *d) { 1260static void draw_InputWidget_(const iInputWidget *d) {
1005 const iWidget *w = constAs_Widget(d); 1261 const iWidget *w = constAs_Widget(d);
1006 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_())); 1262 iRect bounds = adjusted_Rect(bounds_InputWidget_(d), padding_(), neg_I2(padding_()));
1007 iBool isHint = iFalse; 1263 iBool isHint = iFalse;
1008 const iBool isFocused = isFocused_Widget(w); 1264 const iBool isFocused = isFocused_Widget(w);
1009 const iBool isHover = isHover_Widget(w) && 1265 const iBool isHover = isHover_Widget(w) &&
1010 contains_Widget(w, mouseCoord_Window(get_Window())); 1266 contains_InputWidget_(d, mouseCoord_Window(get_Window()));
1011 if (d->inFlags & needUpdateBuffer_InputWidgetFlag) { 1267 if (d->inFlags & needUpdateBuffer_InputWidgetFlag) {
1012 updateBuffered_InputWidget_(iConstCast(iInputWidget *, d)); 1268 updateBuffered_InputWidget_(iConstCast(iInputWidget *, d));
1013 } 1269 }
1014 iPaint p; 1270 iPaint p;
1015 init_Paint(&p); 1271 init_Paint(&p);
1016 iString *text = visText_InputWidget_(d); 1272 /* `lines` is already up to date and ready for drawing. */
1017 if (isWhite_(text) && !isEmpty_String(&d->hint)) { 1273 /* TODO: If empty, draw the hint. */
1018 set_String(text, &d->hint); 1274// iString *text = visText_InputWidget_(d);
1019 isHint = iTrue; 1275// if (isWhite_(text) && !isEmpty_String(&d->hint)) {
1020 } 1276// set_String(text, &d->hint);
1277// isHint = iTrue;
1278// }
1021 fillRect_Paint( 1279 fillRect_Paint(
1022 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); 1280 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId);
1023 drawRectThickness_Paint(&p, 1281 drawRectThickness_Paint(&p,
@@ -1026,29 +1284,53 @@ static void draw_InputWidget_(const iInputWidget *d) {
1026 isFocused ? uiInputFrameFocused_ColorId 1284 isFocused ? uiInputFrameFocused_ColorId
1027 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 1285 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
1028 setClip_Paint(&p, adjusted_Rect(bounds, init_I2(d->leftPadding, 0), init_I2(-d->rightPadding, 0))); 1286 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)); 1287 const iRect contentBounds = contentBounds_InputWidget_(d);
1030 if (isFocused && !isEmpty_Range(&d->mark)) { 1288// const iInt2 textOrigin = textOrigin_InputWidget_(d); //, cstr_String(text));
1031 /* Draw the selected range. */ 1289 iInt2 drawPos = topLeft_Rect(contentBounds);
1032 const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x; 1290 const int fg = isHint ? uiAnnotation_ColorId
1033 const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x; 1291 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
1034 fillRect_Paint(&p, 1292 : uiInputText_ColorId;
1035 (iRect){ addX_I2(textOrigin, iMin(m1, m2)), 1293 /* TODO: If buffered, just draw the buffered copy. */
1036 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) }, 1294 iConstForEach(Array, i, &d->lines) {
1037 uiMarked_ColorId); 1295 const iInputLine *line = i.value;
1038 } 1296 const iBool isLast = index_ArrayConstIterator(&i) == size_Array(&d->lines) - 1;
1039 if (d->buffered && !isFocused && !isHint) { 1297 const iInputLine *nextLine = isLast ? NULL : (line + 1);
1040 /* Most input widgets will use this, since only one is focused at a time. */ 1298 const iRanges lineRange = { line->offset,
1041 draw_TextBuf(d->buffered, textOrigin, white_ColorId); 1299 nextLine ? nextLine->offset : size_Array(&d->text) };
1042 } 1300 if (isFocused && !isEmpty_Range(&d->mark)) {
1043 else { 1301 /* Draw the selected range. */
1044 draw_Text(d->font, 1302 const iRanges mark = mark_InputWidget_(d);
1045 textOrigin, 1303 if (mark.start < lineRange.end && mark.end > lineRange.start) {
1046 isHint ? uiAnnotation_ColorId 1304 const int m1 = advanceN_Text(d->font,
1047 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId 1305 cstr_String(&line->text),
1048 : uiInputText_ColorId, 1306 iMax(lineRange.start, mark.start) - line->offset)
1049 "%s", 1307 .x;
1050 cstr_String(text)); 1308 const int m2 = advanceN_Text(d->font,
1051 } 1309 cstr_String(&line->text),
1310 iMin(lineRange.end, mark.end) - line->offset)
1311 .x;
1312 fillRect_Paint(&p,
1313 (iRect){ addX_I2(drawPos, iMin(m1, m2)),
1314 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) },
1315 uiMarked_ColorId);
1316 }
1317 }
1318 drawRange_Text(d->font, drawPos, fg, range_String(&line->text));
1319 drawPos.y += lineHeight_Text(d->font);
1320 }
1321// if (d->buffered && !isFocused && !isHint) {
1322// /* Most input widgets will use this, since only one is focused at a time. */
1323// draw_TextBuf(d->buffered, textOrigin, white_ColorId);
1324// }
1325// else {
1326// draw_Text(d->font,
1327// textOrigin,
1328// isHint ? uiAnnotation_ColorId
1329// : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
1330// : uiInputText_ColorId,
1331// "%s",
1332// cstr_String(text));
1333// }
1052 unsetClip_Paint(&p); 1334 unsetClip_Paint(&p);
1053 /* Cursor blinking. */ 1335 /* Cursor blinking. */
1054 if (isFocused && d->cursorVis) { 1336 if (isFocused && d->cursorVis) {
@@ -1073,23 +1355,36 @@ static void draw_InputWidget_(const iInputWidget *d) {
1073 /* Bar cursor. */ 1355 /* Bar cursor. */
1074 curSize = init_I2(gap_UI / 2, lineHeight_Text(d->font)); 1356 curSize = init_I2(gap_UI / 2, lineHeight_Text(d->font));
1075 } 1357 }
1358 const iInputLine *curLine = line_InputWidget_(d, d->cursorLine);
1359 const iString * text = &curLine->text;
1076 /* The `gap_UI` offsets below are a hack. They are used because for some reason the 1360 /* 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_()`. */ 1361 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); 1362 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor - curLine->offset);
1079 const iInt2 curPos = addX_I2(textOrigin, prefixSize.x + 1363 const iInt2 curPos = addX_I2(addY_I2(contentBounds.pos, lineHeight_Text(d->font) * d->cursorLine),
1364 prefixSize.x +
1080 (d->mode == insert_InputMode ? -curSize.x / 2 : 0)); 1365 (d->mode == insert_InputMode ? -curSize.x / 2 : 0));
1081 const iRect curRect = { curPos, curSize }; 1366 const iRect curRect = { curPos, curSize };
1082 fillRect_Paint(&p, curRect, uiInputCursor_ColorId); 1367 fillRect_Paint(&p, curRect, uiInputCursor_ColorId);
1083 if (d->mode == overwrite_InputMode) { 1368 if (d->mode == overwrite_InputMode) {
1084 draw_Text(d->font, addX_I2(curPos, iMin(1, gap_UI / 8)), uiInputCursorText_ColorId, "%s", cstr_String(&cur)); 1369 draw_Text(d->font,
1370 addX_I2(curPos, iMin(1, gap_UI / 8)),
1371 uiInputCursorText_ColorId,
1372 "%s",
1373 cstr_String(&cur));
1085 deinit_String(&cur); 1374 deinit_String(&cur);
1086 } 1375 }
1087 } 1376 }
1088 delete_String(text); 1377// delete_String(text);
1089 drawChildren_Widget(w); 1378 drawChildren_Widget(w);
1090} 1379}
1091 1380
1381//static void sizeChanged_InputWidget_(iInputWidget *d) {
1382// printf("[InputWidget] %p: size changed, updating layout\n", d);
1383// updateLinesAndResize_InputWidget_(d, iFalse);
1384//}
1385
1092iBeginDefineSubclass(InputWidget, Widget) 1386iBeginDefineSubclass(InputWidget, Widget)
1093 .processEvent = (iAny *) processEvent_InputWidget_, 1387 .processEvent = (iAny *) processEvent_InputWidget_,
1094 .draw = (iAny *) draw_InputWidget_, 1388 .draw = (iAny *) draw_InputWidget_,
1389// .sizeChanged = (iAny *) sizeChanged_InputWidget_,
1095iEndDefineSubclass(InputWidget) 1390iEndDefineSubclass(InputWidget)
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h
index 86484dcc..cb32a29c 100644
--- a/src/ui/inputwidget.h
+++ b/src/ui/inputwidget.h
@@ -39,6 +39,8 @@ struct Impl_InputWidgetContentPadding {
39 int right; 39 int right;
40}; 40};
41 41
42typedef void (*iInputWidgetValidatorFunc)(iInputWidget *, void *context);
43
42void setHint_InputWidget (iInputWidget *, const char *hintText); 44void setHint_InputWidget (iInputWidget *, const char *hintText);
43void setMode_InputWidget (iInputWidget *, enum iInputMode mode); 45void setMode_InputWidget (iInputWidget *, enum iInputMode mode);
44void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); 46void setMaxLen_InputWidget (iInputWidget *, size_t maxLen);
@@ -47,6 +49,9 @@ void setTextCStr_InputWidget (iInputWidget *, const char *cstr);
47void setFont_InputWidget (iInputWidget *, int fontId); 49void setFont_InputWidget (iInputWidget *, int fontId);
48void setCursor_InputWidget (iInputWidget *, size_t pos); 50void setCursor_InputWidget (iInputWidget *, size_t pos);
49void setContentPadding_InputWidget (iInputWidget *, int left, int right); /* only affects the text entry */ 51void setContentPadding_InputWidget (iInputWidget *, int left, int right); /* only affects the text entry */
52void setMaxLayoutLines_InputWidget (iInputWidget *, size_t maxLayoutLines);
53void setValidator_InputWidget (iInputWidget *, iInputWidgetValidatorFunc validator, void *context);
54void setEnterKeyEnabled_InputWidget (iInputWidget *, iBool enterKeyEnabled);
50void begin_InputWidget (iInputWidget *); 55void begin_InputWidget (iInputWidget *);
51void end_InputWidget (iInputWidget *, iBool accept); 56void end_InputWidget (iInputWidget *, iBool accept);
52void selectAll_InputWidget (iInputWidget *); 57void selectAll_InputWidget (iInputWidget *);
diff --git a/src/ui/root.c b/src/ui/root.c
index 43fadbfa..76ef05c4 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);
@@ -518,13 +534,17 @@ static iBool willPerformSearchQuery_(const iString *userInput) {
518 return !isEmpty_String(&prefs_App()->searchUrl) && !isLikelyUrl_String(userInput); 534 return !isEmpty_String(&prefs_App()->searchUrl) && !isLikelyUrl_String(userInput);
519} 535}
520 536
537static void updateUrlInputContentPadding_(iWidget *navBar) {
538 iInputWidget *url = findChild_Widget(navBar, "url");
539 const iWidget *indicators = findChild_Widget(navBar, "url.rightembed");
540 setContentPadding_InputWidget(url, -1,
541 width_Widget(indicators));
542}
543
521static void showSearchQueryIndicator_(iBool show) { 544static void showSearchQueryIndicator_(iBool show) {
522 iWidget *indicator = findWidget_App("input.indicator.search"); 545 iWidget *indicator = findWidget_App("input.indicator.search");
523 showCollapsed_Widget(indicator, show); 546 showCollapsed_Widget(indicator, show);
524 iAssert(isInstance_Object(parent_Widget(parent_Widget(indicator)), &Class_InputWidget)); 547 updateUrlInputContentPadding_(findWidget_Root("navbar"));
525 iInputWidget *url = (iInputWidget *) parent_Widget(parent_Widget(indicator));
526 setContentPadding_InputWidget(url, -1, contentPadding_InputWidget(url).left +
527 (show ? width_Widget(indicator) : 0));
528} 548}
529 549
530static int navBarAvailableSpace_(iWidget *navBar) { 550static int navBarAvailableSpace_(iWidget *navBar) {
@@ -992,6 +1012,7 @@ void createUserInterface_Root(iRoot *d) {
992 setFlags_Widget(as_Widget(url), resizeHeightOfChildren_WidgetFlag, iTrue); 1012 setFlags_Widget(as_Widget(url), resizeHeightOfChildren_WidgetFlag, iTrue);
993 setSelectAllOnFocus_InputWidget(url, iTrue); 1013 setSelectAllOnFocus_InputWidget(url, iTrue);
994 setId_Widget(as_Widget(url), "url"); 1014 setId_Widget(as_Widget(url), "url");
1015 setMaxLayoutLines_InputWidget(url, 1);
995 setUrlContent_InputWidget(url, iTrue); 1016 setUrlContent_InputWidget(url, iTrue);
996 setNotifyEdits_InputWidget(url, iTrue); 1017 setNotifyEdits_InputWidget(url, iTrue);
997 setTextCStr_InputWidget(url, "gemini://"); 1018 setTextCStr_InputWidget(url, "gemini://");
@@ -1017,7 +1038,7 @@ void createUserInterface_Root(iRoot *d) {
1017 moveToParentRightEdge_WidgetFlag); 1038 moveToParentRightEdge_WidgetFlag);
1018 /* Feeds refresh indicator is inside the input field. */ { 1039 /* Feeds refresh indicator is inside the input field. */ {
1019 iLabelWidget *queryInd = 1040 iLabelWidget *queryInd =
1020 new_LabelWidget(uiTextAction_ColorEscape "\u21d2 ${status.query}", NULL); 1041 new_LabelWidget(uiTextAction_ColorEscape "${status.query} \u21a9", NULL);
1021 setId_Widget(as_Widget(queryInd), "input.indicator.search"); 1042 setId_Widget(as_Widget(queryInd), "input.indicator.search");
1022 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId); 1043 setBackgroundColor_Widget(as_Widget(queryInd), uiBackground_ColorId);
1023 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId); 1044 setFrameColor_Widget(as_Widget(queryInd), uiTextAction_ColorId);
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 8cf4464e..9838fb00 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;
diff --git a/src/ui/util.c b/src/ui/util.c
index 92cf85b7..04cdf27f 100644
--- a/src/ui/util.c
+++ b/src/ui/util.c
@@ -378,10 +378,20 @@ void init_Click(iClick *d, iAnyObject *widget, int button) {
378 d->isActive = iFalse; 378 d->isActive = iFalse;
379 d->button = button; 379 d->button = button;
380 d->bounds = as_Widget(widget); 380 d->bounds = as_Widget(widget);
381 d->minHeight = 0;
381 d->startPos = zero_I2(); 382 d->startPos = zero_I2();
382 d->pos = zero_I2(); 383 d->pos = zero_I2();
383} 384}
384 385
386iBool contains_Click(const iClick *d, iInt2 coord) {
387 if (d->minHeight) {
388 iRect rect = bounds_Widget(d->bounds);
389 rect.size.y = iMax(d->minHeight, rect.size.y);
390 return contains_Rect(rect, coord);
391 }
392 return contains_Widget(d->bounds, coord);
393}
394
385enum iClickResult processEvent_Click(iClick *d, const SDL_Event *event) { 395enum iClickResult processEvent_Click(iClick *d, const SDL_Event *event) {
386 if (event->type == SDL_MOUSEMOTION) { 396 if (event->type == SDL_MOUSEMOTION) {
387 const iInt2 pos = init_I2(event->motion.x, event->motion.y); 397 const iInt2 pos = init_I2(event->motion.x, event->motion.y);
@@ -403,7 +413,7 @@ enum iClickResult processEvent_Click(iClick *d, const SDL_Event *event) {
403 } 413 }
404 if (!d->isActive) { 414 if (!d->isActive) {
405 if (mb->state == SDL_PRESSED) { 415 if (mb->state == SDL_PRESSED) {
406 if (contains_Widget(d->bounds, pos)) { 416 if (contains_Click(d, pos)) {
407 d->isActive = iTrue; 417 d->isActive = iTrue;
408 d->startPos = d->pos = pos; 418 d->startPos = d->pos = pos;
409 setMouseGrab_Widget(d->bounds); 419 setMouseGrab_Widget(d->bounds);
@@ -413,7 +423,7 @@ enum iClickResult processEvent_Click(iClick *d, const SDL_Event *event) {
413 } 423 }
414 else { /* Active. */ 424 else { /* Active. */
415 if (mb->state == SDL_RELEASED) { 425 if (mb->state == SDL_RELEASED) {
416 enum iClickResult result = contains_Widget(d->bounds, pos) 426 enum iClickResult result = contains_Click(d, pos)
417 ? finished_ClickResult 427 ? finished_ClickResult
418 : aborted_ClickResult; 428 : aborted_ClickResult;
419 d->isActive = iFalse; 429 d->isActive = iFalse;
@@ -891,6 +901,14 @@ static iBool isTabPage_Widget_(const iWidget *tabs, const iWidget *page) {
891 return page && page->parent == findChild_Widget(tabs, "tabs.pages"); 901 return page && page->parent == findChild_Widget(tabs, "tabs.pages");
892} 902}
893 903
904static void unfocusFocusInsideTabPage_(const iWidget *page) {
905 iWidget *focus = focus_Widget();
906 if (page && focus && hasParent_Widget(focus, page)) {
907 printf("unfocus inside page: %p\n", focus);
908 setFocus_Widget(NULL);
909 }
910}
911
894static iBool tabSwitcher_(iWidget *tabs, const char *cmd) { 912static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
895 if (equal_Command(cmd, "tabs.switch")) { 913 if (equal_Command(cmd, "tabs.switch")) {
896 iWidget *target = pointerLabel_Command(cmd, "page"); 914 iWidget *target = pointerLabel_Command(cmd, "page");
@@ -898,6 +916,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
898 target = findChild_Widget(tabs, cstr_Rangecc(range_Command(cmd, "id"))); 916 target = findChild_Widget(tabs, cstr_Rangecc(range_Command(cmd, "id")));
899 } 917 }
900 if (!target) return iFalse; 918 if (!target) return iFalse;
919 unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs));
901 if (flags_Widget(target) & focusable_WidgetFlag) { 920 if (flags_Widget(target) & focusable_WidgetFlag) {
902 setFocus_Widget(target); 921 setFocus_Widget(target);
903 } 922 }
@@ -915,6 +934,7 @@ static iBool tabSwitcher_(iWidget *tabs, const char *cmd) {
915 } 934 }
916 } 935 }
917 else if (equal_Command(cmd, "tabs.next") || equal_Command(cmd, "tabs.prev")) { 936 else if (equal_Command(cmd, "tabs.next") || equal_Command(cmd, "tabs.prev")) {
937 unfocusFocusInsideTabPage_(currentTabPage_Widget(tabs));
918 iWidget *pages = findChild_Widget(tabs, "tabs.pages"); 938 iWidget *pages = findChild_Widget(tabs, "tabs.pages");
919 int tabIndex = 0; 939 int tabIndex = 0;
920 iConstForEach(ObjectList, i, pages->children) { 940 iConstForEach(ObjectList, i, pages->children) {
@@ -1881,6 +1901,9 @@ iWidget *makeDialogButtons_Widget(const iMenuItem *actions, size_t numActions) {
1881 } 1901 }
1882 iLabelWidget *button = 1902 iLabelWidget *button =
1883 addChild_Widget(div, iClob(newKeyMods_LabelWidget(label, key, kmods, cmd))); 1903 addChild_Widget(div, iClob(newKeyMods_LabelWidget(label, key, kmods, cmd)));
1904 if (isDefault) {
1905 setId_Widget(as_Widget(button), "default");
1906 }
1884 setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault); 1907 setFlags_Widget(as_Widget(button), alignLeft_WidgetFlag | drawKey_WidgetFlag, isDefault);
1885 setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]); 1908 setFont_LabelWidget(button, isDefault ? fonts[1] : fonts[0]);
1886 } 1909 }
@@ -2175,6 +2198,10 @@ static void addDialogInputWithHeading_(iWidget *headings, iWidget *values, const
2175 setPadding_Widget(as_Widget(head), 0, gap_UI, 0, 0); 2198 setPadding_Widget(as_Widget(head), 0, gap_UI, 0, 0);
2176#endif 2199#endif
2177 setId_Widget(addChild_Widget(values, input), inputId); 2200 setId_Widget(addChild_Widget(values, input), inputId);
2201 if (deviceType_App() != phone_AppDeviceType) {
2202 /* Ensure that the label has the same height as the input widget. */
2203 as_Widget(head)->sizeRef = as_Widget(input);
2204 }
2178} 2205}
2179 2206
2180iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values, 2207iInputWidget *addTwoColumnDialogInputField_Widget(iWidget *headings, iWidget *values,
@@ -2202,10 +2229,13 @@ iWidget *makePreferences_Widget(void) {
2202 /* General preferences. */ { 2229 /* General preferences. */ {
2203 appendTwoColumnPage_(tabs, "${heading.prefs.general}", '1', &headings, &values); 2230 appendTwoColumnPage_(tabs, "${heading.prefs.general}", '1', &headings, &values);
2204#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT) 2231#if defined (LAGRANGE_ENABLE_DOWNLOAD_EDIT)
2205 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.downloads}"))); 2232 //addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.downloads}")));
2206 setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads"); 2233 //setId_Widget(addChild_Widget(values, iClob(new_InputWidget(0))), "prefs.downloads");
2234 addPrefsInputWithHeading_(headings, values, "prefs.downloads", iClob(new_InputWidget(0)));
2207#endif 2235#endif
2208 addPrefsInputWithHeading_(headings, values, "prefs.searchurl", iClob(new_InputWidget(0))); 2236 iInputWidget *searchUrl;
2237 addPrefsInputWithHeading_(headings, values, "prefs.searchurl", iClob(searchUrl = new_InputWidget(0)));
2238 setUrlContent_InputWidget(searchUrl, iTrue);
2209 addChild_Widget(headings, iClob(makePadding_Widget(bigGap))); 2239 addChild_Widget(headings, iClob(makePadding_Widget(bigGap)));
2210 addChild_Widget(values, iClob(makePadding_Widget(bigGap))); 2240 addChild_Widget(values, iClob(makePadding_Widget(bigGap)));
2211 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.collapsepreonload}"))); 2241 addChild_Widget(headings, iClob(makeHeading_Widget("${prefs.collapsepreonload}")));
diff --git a/src/ui/util.h b/src/ui/util.h
index cbedefaa..50845280 100644
--- a/src/ui/util.h
+++ b/src/ui/util.h
@@ -154,6 +154,7 @@ struct Impl_Click {
154 int button; 154 int button;
155 int count; 155 int count;
156 iWidget *bounds; 156 iWidget *bounds;
157 int minHeight;
157 iInt2 startPos; 158 iInt2 startPos;
158 iInt2 pos; 159 iInt2 pos;
159}; 160};
@@ -166,6 +167,7 @@ iBool isMoved_Click (const iClick *);
166iInt2 pos_Click (const iClick *); 167iInt2 pos_Click (const iClick *);
167iRect rect_Click (const iClick *); 168iRect rect_Click (const iClick *);
168iInt2 delta_Click (const iClick *); 169iInt2 delta_Click (const iClick *);
170iBool contains_Click (const iClick *, iInt2 coord);
169 171
170/*-----------------------------------------------------------------------------------------------*/ 172/*-----------------------------------------------------------------------------------------------*/
171 173
diff --git a/src/ui/widget.c b/src/ui/widget.c
index 67ce1345..c1c920d2 100644
--- a/src/ui/widget.c
+++ b/src/ui/widget.c
@@ -54,6 +54,7 @@ void init_Widget(iWidget *d) {
54 d->flags = 0; 54 d->flags = 0;
55 d->rect = zero_Rect(); 55 d->rect = zero_Rect();
56 d->minSize = zero_I2(); 56 d->minSize = zero_I2();
57 d->sizeRef = NULL;
57 d->bgColor = none_ColorId; 58 d->bgColor = none_ColorId;
58 d->frameColor = none_ColorId; 59 d->frameColor = none_ColorId;
59 init_Anim(&d->visualOffset, 0.0f); 60 init_Anim(&d->visualOffset, 0.0f);
@@ -329,6 +330,9 @@ static void setWidth_Widget_(iWidget *d, int width) {
329 330
330static void setHeight_Widget_(iWidget *d, int height) { 331static void setHeight_Widget_(iWidget *d, int height) {
331 iAssert(height >= 0); 332 iAssert(height >= 0);
333 if (d->sizeRef) {
334 return; /* height defined by another widget */
335 }
332 TRACE(d, "attempt to set height to %d (current: %d, min height: %d)", height, d->rect.size.y, d->minSize.y); 336 TRACE(d, "attempt to set height to %d (current: %d, min height: %d)", height, d->rect.size.y, d->minSize.y);
333 height = iMax(height, d->minSize.y); 337 height = iMax(height, d->minSize.y);
334 if (~d->flags & fixedHeight_WidgetFlag) { //} || d->flags & collapse_WidgetFlag) { 338 if (~d->flags & fixedHeight_WidgetFlag) { //} || d->flags & collapse_WidgetFlag) {
@@ -415,6 +419,10 @@ static void boundsOfChildren_Widget_(const iWidget *d, iRect *bounds_out) {
415 419
416static void arrange_Widget_(iWidget *d) { 420static void arrange_Widget_(iWidget *d) {
417 TRACE(d, "arranging..."); 421 TRACE(d, "arranging...");
422 if (d->sizeRef) {
423 d->rect.size.y = height_Widget(d->sizeRef);
424 TRACE(d, "use referenced height: %d", d->rect.size.y);
425 }
418 if (d->flags & moveToParentLeftEdge_WidgetFlag) { 426 if (d->flags & moveToParentLeftEdge_WidgetFlag) {
419 d->rect.pos.x = d->padding[0]; /* FIXME: Shouldn't this be d->parent->padding[0]? */ 427 d->rect.pos.x = d->padding[0]; /* FIXME: Shouldn't this be d->parent->padding[0]? */
420 TRACE(d, "move to parent left edge: %d", d->rect.pos.x); 428 TRACE(d, "move to parent left edge: %d", d->rect.pos.x);
@@ -1035,6 +1043,7 @@ void drawBackground_Widget(const iWidget *d) {
1035 init_Paint(&p); 1043 init_Paint(&p);
1036 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30); 1044 drawSoftShadow_Paint(&p, bounds_Widget(d), 12 * gap_UI, black_ColorId, 30);
1037 } 1045 }
1046
1038 if (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) { 1047 if (fadeBackground && ~d->flags & noFadeBackground_WidgetFlag) {
1039 iPaint p; 1048 iPaint p;
1040 init_Paint(&p); 1049 init_Paint(&p);
@@ -1131,10 +1140,10 @@ void drawChildren_Widget(const iWidget *d) {
1131 } 1140 }
1132 } 1141 }
1133 /* Root draws the on-top widgets on top of everything else. */ 1142 /* Root draws the on-top widgets on top of everything else. */
1134 if (!d->parent) { 1143 if (d == d->root->widget) {
1135 iConstForEach(PtrArray, i, onTop_Root(d->root)) { 1144 iConstForEach(PtrArray, i, onTop_Root(d->root)) {
1136 const iWidget *top = *i.value; 1145 const iWidget *top = *i.value;
1137 draw_Widget(top); 1146 class_Widget(top)->draw(top);
1138 } 1147 }
1139 } 1148 }
1140} 1149}
diff --git a/src/ui/widget.h b/src/ui/widget.h
index 5b6b18e1..5c05e917 100644
--- a/src/ui/widget.h
+++ b/src/ui/widget.h
@@ -134,6 +134,7 @@ struct Impl_Widget {
134 int64_t flags; 134 int64_t flags;
135 iRect rect; 135 iRect rect;
136 iInt2 minSize; 136 iInt2 minSize;
137 iWidget * sizeRef;
137 int padding[4]; /* left, top, right, bottom */ 138 int padding[4]; /* left, top, right, bottom */
138 iAnim visualOffset; 139 iAnim visualOffset;
139 int bgColor; 140 int bgColor;