diff options
Diffstat (limited to 'src/ui/inputwidget.c')
-rw-r--r-- | src/ui/inputwidget.c | 452 |
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 | |||
3 | Redistribution and use in source and binary forms, with or without | ||
4 | modification, are permitted provided that the following conditions are met: | ||
5 | |||
6 | 1. Redistributions of source code must retain the above copyright notice, this | ||
7 | list of conditions and the following disclaimer. | ||
8 | 2. 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 | |||
12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||
13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||
14 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | ||
16 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||
17 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||
18 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | ||
19 | ANY 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 | ||
21 | SOFTWARE, 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 | ||
10 | static const int REFRESH_INTERVAL = 256; | 32 | static const int refreshInterval_InputWidget_ = 256; |
33 | static const size_t maxUndo_InputWidget_ = 64; | ||
34 | |||
35 | iDeclareType(InputUndo) | ||
36 | |||
37 | struct Impl_InputUndo { | ||
38 | iArray text; | ||
39 | size_t cursor; | ||
40 | }; | ||
41 | |||
42 | static void init_InputUndo_(iInputUndo *d, const iArray *text, size_t cursor) { | ||
43 | initCopy_Array(&d->text, text); | ||
44 | d->cursor = cursor; | ||
45 | } | ||
46 | |||
47 | static void deinit_InputUndo_(iInputUndo *d) { | ||
48 | deinit_Array(&d->text); | ||
49 | } | ||
11 | 50 | ||
12 | struct Impl_InputWidget { | 51 | struct 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 | ||
26 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) | 71 | iDefineObjectConstructionArgs(InputWidget, (size_t maxLen), maxLen) |
27 | 72 | ||
73 | static void clearUndo_InputWidget_(iInputWidget *d) { | ||
74 | iForEach(Array, i, &d->undoStack) { | ||
75 | deinit_InputUndo_(i.value); | ||
76 | } | ||
77 | clear_Array(&d->undoStack); | ||
78 | } | ||
79 | |||
28 | void init_InputWidget(iInputWidget *d, size_t maxLen) { | 80 | void 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 | ||
46 | void deinit_InputWidget(iInputWidget *d) { | 104 | void 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 | ||
115 | static 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 | |||
125 | static 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 | |||
54 | void setMode_InputWidget(iInputWidget *d, enum iInputMode mode) { | 138 | void 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 | ||
165 | void setHint_InputWidget(iInputWidget *d, const char *hintText) { | ||
166 | setCStr_String(&d->hint, hintText); | ||
167 | } | ||
168 | |||
81 | void setText_InputWidget(iInputWidget *d, const iString *text) { | 169 | void 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 | ||
95 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
96 | d->cursor = iMin(pos, size_Array(&d->text)); | ||
97 | } | ||
98 | |||
99 | static uint32_t refreshTimer_(uint32_t interval, void *d) { | 184 | static 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 | ||
125 | void end_InputWidget(iInputWidget *d, iBool accept) { | 216 | void 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 | ||
253 | iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) { | ||
254 | return iMin(size_Array(&d->text), d->maxLen - 1); | ||
255 | } | ||
256 | |||
257 | iLocalDef iBool isMarking_(void) { | ||
258 | return (SDL_GetModState() & KMOD_SHIFT) != 0; | ||
259 | } | ||
260 | |||
261 | void 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 | |||
283 | void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { | ||
284 | d->selectAllOnFocus = selectAllOnFocus; | ||
285 | } | ||
286 | |||
287 | static 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 | |||
291 | static 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 | |||
302 | static 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 | |||
307 | iLocalDef 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 | |||
317 | static 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 | |||
351 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | ||
352 | |||
353 | static 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 | |||
367 | iLocalDef iInt2 padding_(void) { | ||
368 | return init_I2(gap_UI / 2, gap_UI / 2); | ||
369 | } | ||
370 | |||
371 | static 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 | |||
392 | static 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 | |||
162 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 415 | static 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 | ||
290 | static const iChar sensitiveChar_ = 0x25cf; /* black circle */ | 614 | static 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 | ||
292 | static void draw_InputWidget_(const iInputWidget *d) { | 623 | static 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 | ||
366 | iBeginDefineSubclass(InputWidget, Widget) | 688 | iBeginDefineSubclass(InputWidget, Widget) |