summaryrefslogtreecommitdiff
path: root/src/ui/inputwidget.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/ui/inputwidget.c')
-rw-r--r--src/ui/inputwidget.c452
1 files changed, 387 insertions, 65 deletions
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c
index d367952d..d583b109 100644
--- a/src/ui/inputwidget.c
+++ b/src/ui/inputwidget.c
@@ -1,3 +1,25 @@
1/* Copyright 2020 Jaakko Keränen <jaakko.keranen@iki.fi>
2
3Redistribution and use in source and binary forms, with or without
4modification, are permitted provided that the following conditions are met:
5
61. Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
82. Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
16ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
17(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
18LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
19ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
20(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
21SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */
22
1#include "inputwidget.h" 23#include "inputwidget.h"
2#include "paint.h" 24#include "paint.h"
3#include "util.h" 25#include "util.h"
@@ -7,17 +29,40 @@
7#include <SDL_clipboard.h> 29#include <SDL_clipboard.h>
8#include <SDL_timer.h> 30#include <SDL_timer.h>
9 31
10static const int REFRESH_INTERVAL = 256; 32static const int refreshInterval_InputWidget_ = 256;
33static const size_t maxUndo_InputWidget_ = 64;
34
35iDeclareType(InputUndo)
36
37struct Impl_InputUndo {
38 iArray text;
39 size_t cursor;
40};
41
42static void init_InputUndo_(iInputUndo *d, const iArray *text, size_t cursor) {
43 initCopy_Array(&d->text, text);
44 d->cursor = cursor;
45}
46
47static void deinit_InputUndo_(iInputUndo *d) {
48 deinit_Array(&d->text);
49}
11 50
12struct Impl_InputWidget { 51struct Impl_InputWidget {
13 iWidget widget; 52 iWidget widget;
14 enum iInputMode mode; 53 enum iInputMode mode;
15 iBool isSensitive; 54 iBool isSensitive;
16 iBool enterPressed; 55 iBool enterPressed;
56 iBool selectAllOnFocus;
17 size_t maxLen; 57 size_t maxLen;
18 iArray text; /* iChar[] */ 58 iArray text; /* iChar[] */
19 iArray oldText; /* iChar[] */ 59 iArray oldText; /* iChar[] */
60 iString hint;
20 size_t cursor; 61 size_t cursor;
62 size_t lastCursor;
63 iBool isMarking;
64 iRanges mark;
65 iArray undoStack;
21 int font; 66 int font;
22 iClick click; 67 iClick click;
23 uint32_t timer; 68 uint32_t timer;
@@ -25,16 +70,29 @@ struct Impl_InputWidget {
25 70
26iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) 71iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen)
27 72
73static void clearUndo_InputWidget_(iInputWidget *d) {
74 iForEach(Array, i, &d->undoStack) {
75 deinit_InputUndo_(i.value);
76 }
77 clear_Array(&d->undoStack);
78}
79
28void init_InputWidget(iInputWidget *d, size_t maxLen) { 80void init_InputWidget(iInputWidget *d, size_t maxLen) {
29 iWidget *w = &d->widget; 81 iWidget *w = &d->widget;
30 init_Widget(w); 82 init_Widget(w);
31 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue); 83 setFlags_Widget(w, focusable_WidgetFlag | hover_WidgetFlag, iTrue);
32 init_Array(&d->text, sizeof(iChar)); 84 init_Array(&d->text, sizeof(iChar));
33 init_Array(&d->oldText, sizeof(iChar)); 85 init_Array(&d->oldText, sizeof(iChar));
86 init_String(&d->hint);
87 init_Array(&d->undoStack, sizeof(iInputUndo));
34 d->font = uiInput_FontId; 88 d->font = uiInput_FontId;
35 d->cursor = 0; 89 d->cursor = 0;
36 d->isSensitive = iFalse; 90 d->lastCursor = 0;
37 d->enterPressed = iFalse; 91 d->isMarking = iFalse;
92 iZap(d->mark);
93 d->isSensitive = iFalse;
94 d->enterPressed = iFalse;
95 d->selectAllOnFocus = iFalse;
38 setMaxLen_InputWidget(d, maxLen); 96 setMaxLen_InputWidget(d, maxLen);
39 /* Caller must arrange the width, but the height is fixed. */ 97 /* Caller must arrange the width, but the height is fixed. */
40 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; 98 w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI;
@@ -44,13 +102,39 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) {
44} 102}
45 103
46void deinit_InputWidget(iInputWidget *d) { 104void deinit_InputWidget(iInputWidget *d) {
105 clearUndo_InputWidget_(d);
106 deinit_Array(&d->undoStack);
47 if (d->timer) { 107 if (d->timer) {
48 SDL_RemoveTimer(d->timer); 108 SDL_RemoveTimer(d->timer);
49 } 109 }
110 deinit_String(&d->hint);
50 deinit_Array(&d->oldText); 111 deinit_Array(&d->oldText);
51 deinit_Array(&d->text); 112 deinit_Array(&d->text);
52} 113}
53 114
115static void pushUndo_InputWidget_(iInputWidget *d) {
116 iInputUndo undo;
117 init_InputUndo_(&undo, &d->text, d->cursor);
118 pushBack_Array(&d->undoStack, &undo);
119 if (size_Array(&d->undoStack) > maxUndo_InputWidget_) {
120 deinit_InputUndo_(front_Array(&d->undoStack));
121 popFront_Array(&d->undoStack);
122 }
123}
124
125static iBool popUndo_InputWidget_(iInputWidget *d) {
126 if (!isEmpty_Array(&d->undoStack)) {
127 iInputUndo *undo = back_Array(&d->undoStack);
128 setCopy_Array(&d->text, &undo->text);
129 d->cursor = undo->cursor;
130 deinit_InputUndo_(undo);
131 popBack_Array(&d->undoStack);
132 iZap(d->mark);
133 return iTrue;
134 }
135 return iFalse;
136}
137
54void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { 138void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) {
55 d->mode = mode; 139 d->mode = mode;
56} 140}
@@ -78,7 +162,12 @@ void setMaxLen_InputWidget(iInputWidget *d, size_t maxLen) {
78 } 162 }
79} 163}
80 164
165void setHint_InputWidget(iInputWidget *d, const char *hintText) {
166 setCStr_String(&d->hint, hintText);
167}
168
81void setText_InputWidget(iInputWidget *d, const iString *text) { 169void setText_InputWidget(iInputWidget *d, const iString *text) {
170 clearUndo_InputWidget_(d);
82 clear_Array(&d->text); 171 clear_Array(&d->text);
83 iConstForEach(String, i, text) { 172 iConstForEach(String, i, text) {
84 pushBack_Array(&d->text, &i.value); 173 pushBack_Array(&d->text, &i.value);
@@ -92,10 +181,6 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) {
92 delete_String(str); 181 delete_String(str);
93} 182}
94 183
95void setCursor_InputWidget(iInputWidget *d, size_t pos) {
96 d->cursor = iMin(pos, size_Array(&d->text));
97}
98
99static uint32_t refreshTimer_(uint32_t interval, void *d) { 184static uint32_t refreshTimer_(uint32_t interval, void *d) {
100 refresh_Widget(d); 185 refresh_Widget(d);
101 return interval; 186 return interval;
@@ -118,8 +203,14 @@ void begin_InputWidget(iInputWidget *d) {
118 SDL_StartTextInput(); 203 SDL_StartTextInput();
119 setFlags_Widget(w, selected_WidgetFlag, iTrue); 204 setFlags_Widget(w, selected_WidgetFlag, iTrue);
120 refresh_Widget(w); 205 refresh_Widget(w);
121 d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); 206 d->timer = SDL_AddTimer(refreshInterval_InputWidget_, refreshTimer_, d);
122 d->enterPressed = iFalse; 207 d->enterPressed = iFalse;
208 if (d->selectAllOnFocus) {
209 d->mark = (iRanges){ 0, size_Array(&d->text) };
210 }
211 else {
212 iZap(d->mark);
213 }
123} 214}
124 215
125void end_InputWidget(iInputWidget *d, iBool accept) { 216void end_InputWidget(iInputWidget *d, iBool accept) {
@@ -159,6 +250,168 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) {
159 refresh_Widget(as_Widget(d)); 250 refresh_Widget(as_Widget(d));
160} 251}
161 252
253iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) {
254 return iMin(size_Array(&d->text), d->maxLen - 1);
255}
256
257iLocalDef iBool isMarking_(void) {
258 return (SDL_GetModState() & KMOD_SHIFT) != 0;
259}
260
261void setCursor_InputWidget(iInputWidget *d, size_t pos) {
262 if (isEmpty_Array(&d->text)) {
263 d->cursor = 0;
264 }
265 else {
266 d->cursor = iClamp(pos, 0, cursorMax_InputWidget_(d));
267 }
268 /* Update selection. */
269 if (isMarking_()) {
270 if (isEmpty_Range(&d->mark)) {
271 d->mark.start = d->lastCursor;
272 d->mark.end = d->cursor;
273 }
274 else {
275 d->mark.end = d->cursor;
276 }
277 }
278 else {
279 iZap(d->mark);
280 }
281}
282
283void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) {
284 d->selectAllOnFocus = selectAllOnFocus;
285}
286
287static iRanges mark_InputWidget_(const iInputWidget *d) {
288 return (iRanges){ iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) };
289}
290
291static iBool deleteMarked_InputWidget_(iInputWidget *d) {
292 const iRanges m = mark_InputWidget_(d);
293 if (!isEmpty_Range(&m)) {
294 removeRange_Array(&d->text, m);
295 setCursor_InputWidget(d, m.start);
296 iZap(d->mark);
297 return iTrue;
298 }
299 return iFalse;
300}
301
302static iBool isWordChar_InputWidget_(const iInputWidget *d, size_t pos) {
303 const iChar ch = pos < size_Array(&d->text) ? constValue_Array(&d->text, pos, iChar) : ' ';
304 return isAlphaNumeric_Char(ch);
305}
306
307iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, size_t *pos, int dir) {
308 if (dir < 0) {
309 if (*pos > 0) (*pos)--; else return iFalse;
310 }
311 else {
312 if (*pos < cursorMax_InputWidget_(d)) (*pos)++; else return iFalse;
313 }
314 return iTrue;
315}
316
317static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir) {
318 const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos);
319 if (!movePos_InputWidget_(d, &pos, dir)) {
320 return pos;
321 }
322 /* Skip any non-word characters at start position. */
323 while (!isWordChar_InputWidget_(d, pos)) {
324 if (!movePos_InputWidget_(d, &pos, dir)) {
325 return pos;
326 }
327 }
328 if (startedAtNonWord && dir > 0) {
329 return pos; /* Found the start of a word. */
330 }
331 /* Skip the word. */
332 while (isWordChar_InputWidget_(d, pos)) {
333 if (!movePos_InputWidget_(d, &pos, dir)) {
334 return pos;
335 }
336 }
337 if (dir > 0) {
338 /* Skip to the beginning of the word. */
339 while (!isWordChar_InputWidget_(d, pos)) {
340 if (!movePos_InputWidget_(d, &pos, dir)) {
341 return pos;
342 }
343 }
344 }
345 else {
346 movePos_InputWidget_(d, &pos, +1);
347 }
348 return pos;
349}
350
351static const iChar sensitiveChar_ = 0x25cf; /* black circle */
352
353static iString *visText_InputWidget_(const iInputWidget *d) {
354 iString *text;
355 if (!d->isSensitive) {
356 text = newUnicodeN_String(constData_Array(&d->text), size_Array(&d->text));
357 }
358 else {
359 text = new_String();
360 for (size_t i = 0; i < size_Array(&d->text); ++i) {
361 appendChar_String(text, sensitiveChar_);
362 }
363 }
364 return text;
365}
366
367iLocalDef iInt2 padding_(void) {
368 return init_I2(gap_UI / 2, gap_UI / 2);
369}
370
371static iInt2 textOrigin_InputWidget_(const iInputWidget *d, const char *visText) {
372 const iWidget *w = constAs_Widget(d);
373 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
374 const iInt2 emSize = advance_Text(d->font, "M");
375 const int textWidth = advance_Text(d->font, visText).x;
376 const int cursorX = advanceN_Text(d->font, visText, d->cursor).x;
377 int xOff = 0;
378 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0));
379 if (d->maxLen == 0) {
380 if (textWidth > width_Rect(bounds) - emSize.x) {
381 xOff = width_Rect(bounds) - emSize.x - textWidth;
382 }
383 if (cursorX + xOff < width_Rect(bounds) / 2) {
384 xOff = width_Rect(bounds) / 2 - cursorX;
385 }
386 xOff = iMin(xOff, 0);
387 }
388 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
389 return add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff));
390}
391
392static size_t coordIndex_InputWidget_(const iInputWidget *d, iInt2 coord) {
393 iString *visText = visText_InputWidget_(d);
394 iInt2 pos = sub_I2(coord, textOrigin_InputWidget_(d, cstr_String(visText)));
395 size_t index = 0;
396 if (pos.x > 0) {
397 const char *endPos;
398 tryAdvanceNoWrap_Text(d->font, range_String(visText), pos.x, &endPos);
399 if (endPos == constEnd_String(visText)) {
400 index = cursorMax_InputWidget_(d);
401 }
402 else {
403 /* Need to know the actual character index. */
404 /* TODO: tryAdvance could tell us this directly with an extra return value */
405 iConstForEach(String, i, visText) {
406 if (i.pos >= endPos) break;
407 index++;
408 }
409 }
410 }
411 delete_String(visText);
412 return index;
413}
414
162static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { 415static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
163 iWidget *w = as_Widget(d); 416 iWidget *w = as_Widget(d);
164 if (isCommand_Widget(w, ev, "focus.gained")) { 417 if (isCommand_Widget(w, ev, "focus.gained")) {
@@ -173,25 +426,51 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
173 case none_ClickResult: 426 case none_ClickResult:
174 break; 427 break;
175 case started_ClickResult: 428 case started_ClickResult:
176 case drag_ClickResult: 429 setFocus_Widget(w);
430 setCursor_InputWidget(d, coordIndex_InputWidget_(d, pos_Click(&d->click)));
431 iZap(d->mark);
432 d->isMarking = iFalse;
433 return iTrue;
177 case double_ClickResult: 434 case double_ClickResult:
178 case aborted_ClickResult: 435 case aborted_ClickResult:
179 return iTrue; 436 return iTrue;
437 case drag_ClickResult:
438 d->cursor = coordIndex_InputWidget_(d, pos_Click(&d->click));
439 if (!d->isMarking) {
440 d->isMarking = iTrue;
441 d->mark.start = d->cursor;
442 }
443 d->mark.end = d->cursor;
444 refresh_Widget(w);
445 return iTrue;
180 case finished_ClickResult: 446 case finished_ClickResult:
181 setFocus_Widget(as_Widget(d));
182 return iTrue; 447 return iTrue;
183 } 448 }
184 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { 449 if (ev->type == SDL_KEYUP && isFocused_Widget(w)) {
185 return iTrue; 450 return iTrue;
186 } 451 }
187 const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1); 452 const size_t curMax = cursorMax_InputWidget_(d);
188 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { 453 if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) {
189 const int key = ev->key.keysym.sym; 454 const int key = ev->key.keysym.sym;
190 const int mods = keyMods_Sym(ev->key.keysym.mod); 455 const int mods = keyMods_Sym(ev->key.keysym.mod);
191 if (mods == KMOD_PRIMARY) { 456 if (mods == KMOD_PRIMARY) {
192 switch (key) { 457 switch (key) {
458 case 'c':
459 case 'x':
460 if (!isEmpty_Range(&d->mark)) {
461 const iRanges m = mark_InputWidget_(d);
462 SDL_SetClipboardText(cstrCollect_String(
463 newUnicodeN_String(constAt_Array(&d->text, m.start), size_Range(&m))));
464 if (key == 'x') {
465 pushUndo_InputWidget_(d);
466 deleteMarked_InputWidget_(d);
467 }
468 }
469 return iTrue;
193 case 'v': 470 case 'v':
194 if (SDL_HasClipboardText()) { 471 if (SDL_HasClipboardText()) {
472 pushUndo_InputWidget_(d);
473 deleteMarked_InputWidget_(d);
195 char *text = SDL_GetClipboardText(); 474 char *text = SDL_GetClipboardText();
196 iString *paste = collect_String(newCStr_String(text)); 475 iString *paste = collect_String(newCStr_String(text));
197 SDL_free(text); 476 SDL_free(text);
@@ -200,8 +479,14 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
200 } 479 }
201 } 480 }
202 return iTrue; 481 return iTrue;
482 case 'z':
483 if (popUndo_InputWidget_(d)) {
484 refresh_Widget(w);
485 }
486 return iTrue;
203 } 487 }
204 } 488 }
489 d->lastCursor = d->cursor;
205 switch (key) { 490 switch (key) {
206 case SDLK_RETURN: 491 case SDLK_RETURN:
207 case SDLK_KP_ENTER: 492 case SDLK_KP_ENTER:
@@ -213,11 +498,18 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
213 setFocus_Widget(NULL); 498 setFocus_Widget(NULL);
214 return iTrue; 499 return iTrue;
215 case SDLK_BACKSPACE: 500 case SDLK_BACKSPACE:
216 if (mods & KMOD_ALT) { 501 if (!isEmpty_Range(&d->mark)) {
217 clear_Array(&d->text); 502 pushUndo_InputWidget_(d);
218 d->cursor = 0; 503 deleteMarked_InputWidget_(d);
504 }
505 else if (mods & KMOD_ALT) {
506 pushUndo_InputWidget_(d);
507 d->mark.start = d->cursor;
508 d->mark.end = skipWord_InputWidget_(d, d->cursor, -1);
509 deleteMarked_InputWidget_(d);
219 } 510 }
220 else if (d->cursor > 0) { 511 else if (d->cursor > 0) {
512 pushUndo_InputWidget_(d);
221 remove_Array(&d->text, --d->cursor); 513 remove_Array(&d->text, --d->cursor);
222 } 514 }
223 refresh_Widget(w); 515 refresh_Widget(w);
@@ -225,49 +517,79 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
225 case SDLK_d: 517 case SDLK_d:
226 if (mods != KMOD_CTRL) break; 518 if (mods != KMOD_CTRL) break;
227 case SDLK_DELETE: 519 case SDLK_DELETE:
228 if (d->cursor < size_Array(&d->text)) { 520 if (!isEmpty_Range(&d->mark)) {
521 pushUndo_InputWidget_(d);
522 deleteMarked_InputWidget_(d);
523 }
524 else if (mods & KMOD_ALT) {
525 pushUndo_InputWidget_(d);
526 d->mark.start = d->cursor;
527 d->mark.end = skipWord_InputWidget_(d, d->cursor, +1);
528 deleteMarked_InputWidget_(d);
529 }
530 else if (d->cursor < size_Array(&d->text)) {
531 pushUndo_InputWidget_(d);
229 remove_Array(&d->text, d->cursor); 532 remove_Array(&d->text, d->cursor);
230 refresh_Widget(w);
231 } 533 }
534 refresh_Widget(w);
232 return iTrue; 535 return iTrue;
233 case SDLK_k: 536 case SDLK_k:
234 if (mods == KMOD_CTRL) { 537 if (mods == KMOD_CTRL) {
235 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); 538 if (!isEmpty_Range(&d->mark)) {
539 pushUndo_InputWidget_(d);
540 deleteMarked_InputWidget_(d);
541 }
542 else {
543 pushUndo_InputWidget_(d);
544 removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor);
545 }
236 refresh_Widget(w); 546 refresh_Widget(w);
237 return iTrue; 547 return iTrue;
238 } 548 }
239 break; 549 break;
240 case SDLK_HOME: 550 case SDLK_HOME:
241 case SDLK_END: 551 case SDLK_END:
242 d->cursor = (key == SDLK_HOME ? 0 : curMax); 552 setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax);
243 refresh_Widget(w); 553 refresh_Widget(w);
244 return iTrue; 554 return iTrue;
245 case SDLK_a: 555 case SDLK_a:
556#if defined (iPlatformApple)
557 if (mods == KMOD_PRIMARY) {
558 d->mark.start = 0;
559 d->mark.end = curMax;
560 d->cursor = curMax;
561 refresh_Widget(w);
562 return iTrue;
563 }
564#endif
565 /* fall through for Emacs-style Home/End */
246 case SDLK_e: 566 case SDLK_e:
247 if (mods == KMOD_CTRL) { 567 if (mods == KMOD_CTRL || mods == (KMOD_CTRL | KMOD_SHIFT)) {
248 d->cursor = (key == 'a' ? 0 : curMax); 568 setCursor_InputWidget(d, key == 'a' ? 0 : curMax);
249 refresh_Widget(w); 569 refresh_Widget(w);
250 return iTrue; 570 return iTrue;
251 } 571 }
252 break; 572 break;
253 case SDLK_LEFT: 573 case SDLK_LEFT:
574 case SDLK_RIGHT: {
575 const int dir = (key == SDLK_LEFT ? -1 : +1);
254 if (mods & KMOD_PRIMARY) { 576 if (mods & KMOD_PRIMARY) {
255 d->cursor = 0; 577 setCursor_InputWidget(d, dir < 0 ? 0 : curMax);
256 } 578 }
257 else if (d->cursor > 0) { 579 else if (mods & KMOD_ALT) {
258 d->cursor--; 580 setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir));
259 } 581 }
260 refresh_Widget(w); 582 else if (!isMarking_() && !isEmpty_Range(&d->mark)) {
261 return iTrue; 583 const iRanges m = mark_InputWidget_(d);
262 case SDLK_RIGHT: 584 setCursor_InputWidget(d, dir < 0 ? m.start : m.end);
263 if (mods & KMOD_PRIMARY) { 585 iZap(d->mark);
264 d->cursor = curMax;
265 } 586 }
266 else if (d->cursor < curMax) { 587 else if ((dir < 0 && d->cursor > 0) || (dir > 0 && d->cursor < curMax)) {
267 d->cursor++; 588 setCursor_InputWidget(d, d->cursor + dir);
268 } 589 }
269 refresh_Widget(w); 590 refresh_Widget(w);
270 return iTrue; 591 return iTrue;
592 }
271 case SDLK_TAB: 593 case SDLK_TAB:
272 /* Allow focus switching. */ 594 /* Allow focus switching. */
273 return processEvent_Widget(as_Widget(d), ev); 595 return processEvent_Widget(as_Widget(d), ev);
@@ -278,6 +600,8 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
278 return iTrue; 600 return iTrue;
279 } 601 }
280 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) { 602 else if (ev->type == SDL_TEXTINPUT && isFocused_Widget(w)) {
603 pushUndo_InputWidget_(d);
604 deleteMarked_InputWidget_(d);
281 const iString *uni = collectNewCStr_String(ev->text.text); 605 const iString *uni = collectNewCStr_String(ev->text.text);
282 iConstForEach(String, i, uni) { 606 iConstForEach(String, i, uni) {
283 insertChar_InputWidget_(d, i.value); 607 insertChar_InputWidget_(d, i.value);
@@ -287,27 +611,29 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) {
287 return processEvent_Widget(w, ev); 611 return processEvent_Widget(w, ev);
288} 612}
289 613
290static const iChar sensitiveChar_ = 0x25cf; /* black circle */ 614static iBool isWhite_(const iString *str) {
615 iConstForEach(String, i, str) {
616 if (!isSpace_Char(i.value)) {
617 return iFalse;
618 }
619 }
620 return iTrue;
621}
291 622
292static void draw_InputWidget_(const iInputWidget *d) { 623static void draw_InputWidget_(const iInputWidget *d) {
293 const iWidget *w = constAs_Widget(d); 624 const iWidget *w = constAs_Widget(d);
294 const uint32_t time = frameTime_Window(get_Window()); 625 const uint32_t time = frameTime_Window(get_Window());
295 const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); 626 iRect bounds = adjusted_Rect(bounds_Widget(w), padding_(), neg_I2(padding_()));
296 iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); 627 iBool isHint = iFalse;
297 const iBool isFocused = isFocused_Widget(w); 628 const iBool isFocused = isFocused_Widget(w);
298 const iBool isHover = isHover_Widget(w) && 629 const iBool isHover = isHover_Widget(w) &&
299 contains_Widget(w, mouseCoord_Window(get_Window())); 630 contains_Widget(w, mouseCoord_Window(get_Window()));
300 iPaint p; 631 iPaint p;
301 init_Paint(&p); 632 init_Paint(&p);
302 iString text; 633 iString *text = visText_InputWidget_(d);
303 if (!d->isSensitive) { 634 if (isWhite_(text) && !isEmpty_String(&d->hint)) {
304 initUnicodeN_String(&text, constData_Array(&d->text), size_Array(&d->text)); 635 set_String(text, &d->hint);
305 } 636 isHint = iTrue;
306 else {
307 init_String(&text);
308 for (size_t i = 0; i < size_Array(&d->text); ++i) {
309 appendChar_String(&text, sensitiveChar_);
310 }
311 } 637 }
312 fillRect_Paint( 638 fillRect_Paint(
313 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId); 639 &p, bounds, isFocused ? uiInputBackgroundFocused_ColorId : uiInputBackground_ColorId);
@@ -317,34 +643,27 @@ static void draw_InputWidget_(const iInputWidget *d) {
317 isFocused ? uiInputFrameFocused_ColorId 643 isFocused ? uiInputFrameFocused_ColorId
318 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId); 644 : isHover ? uiInputFrameHover_ColorId : uiInputFrame_ColorId);
319 setClip_Paint(&p, bounds); 645 setClip_Paint(&p, bounds);
320 shrink_Rect(&bounds, init_I2(gap_UI * (flags_Widget(w) & tight_WidgetFlag ? 1 : 2), 0)); 646 const iInt2 textOrigin = textOrigin_InputWidget_(d, cstr_String(text));
321 const iInt2 emSize = advance_Text(d->font, "M"); 647 if (isFocused && !isEmpty_Range(&d->mark)) {
322 const int textWidth = advance_Text(d->font, cstr_String(&text)).x; 648 /* Draw the selected range. */
323 const int cursorX = advanceN_Text(d->font, cstr_String(&text), d->cursor).x; 649 const int m1 = advanceN_Text(d->font, cstr_String(text), d->mark.start).x;
324 int xOff = 0; 650 const int m2 = advanceN_Text(d->font, cstr_String(text), d->mark.end).x;
325 if (d->maxLen == 0) { 651 fillRect_Paint(&p,
326 if (textWidth > width_Rect(bounds) - emSize.x) { 652 (iRect){ addX_I2(textOrigin, iMin(m1, m2)),
327 xOff = width_Rect(bounds) - emSize.x - textWidth; 653 init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) },
328 } 654 uiMarked_ColorId);
329 if (cursorX + xOff < width_Rect(bounds) / 2) {
330 xOff = width_Rect(bounds) / 2 - cursorX;
331 }
332 xOff = iMin(xOff, 0);
333 } 655 }
334 const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2;
335 draw_Text(d->font, 656 draw_Text(d->font,
336 add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)), 657 textOrigin,
337 isFocused ? uiInputTextFocused_ColorId : uiInputText_ColorId, 658 isHint ? uiAnnotation_ColorId
659 : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId
660 : uiInputText_ColorId,
338 "%s", 661 "%s",
339 cstr_String(&text)); 662 cstr_String(text));
340 unsetClip_Paint(&p); 663 unsetClip_Paint(&p);
341 /* Cursor blinking. */ 664 /* Cursor blinking. */
342 if (isFocused && (time & 256)) { 665 if (isFocused && (time & 256)) {
343 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); 666 iString cur;
344 const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x,
345 yOff + top_Rect(bounds));
346 const iRect curRect = { curPos, addX_I2(emSize, 1) };
347 iString cur;
348 if (d->cursor < size_Array(&d->text)) { 667 if (d->cursor < size_Array(&d->text)) {
349 if (!d->isSensitive) { 668 if (!d->isSensitive) {
350 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1); 669 initUnicodeN_String(&cur, constAt_Array(&d->text, d->cursor), 1);
@@ -356,11 +675,14 @@ static void draw_InputWidget_(const iInputWidget *d) {
356 else { 675 else {
357 initCStr_String(&cur, " "); 676 initCStr_String(&cur, " ");
358 } 677 }
678 const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(text), d->cursor);
679 const iInt2 curPos = addX_I2(textOrigin, prefixSize.x);
680 const iRect curRect = { curPos, addX_I2(advance_Text(d->font, cstr_String(&cur)), 1) };
359 fillRect_Paint(&p, curRect, uiInputCursor_ColorId); 681 fillRect_Paint(&p, curRect, uiInputCursor_ColorId);
360 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur)); 682 draw_Text(d->font, curPos, uiInputCursorText_ColorId, cstr_String(&cur));
361 deinit_String(&cur); 683 deinit_String(&cur);
362 } 684 }
363 deinit_String(&text); 685 delete_String(text);
364} 686}
365 687
366iBeginDefineSubclass(InputWidget, Widget) 688iBeginDefineSubclass(InputWidget, Widget)