diff options
author | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-08-30 14:19:11 +0300 |
---|---|---|
committer | Jaakko Keränen <jaakko.keranen@iki.fi> | 2020-08-30 14:19:11 +0300 |
commit | 3eb85b3d0fdd007ad18c3b522795b0ba3a038d1f (patch) | |
tree | ac08f58747b408950b7958ef5cb9f7d2ccf2f77e /src/ui | |
parent | 2f24e9404c2c01251826e98286dab5df7d9e5ce7 (diff) |
InputWidget: Marking, deleting, word skipping
Diffstat (limited to 'src/ui')
-rw-r--r-- | src/ui/inputwidget.c | 198 | ||||
-rw-r--r-- | src/ui/inputwidget.h | 1 | ||||
-rw-r--r-- | src/ui/window.c | 1 |
3 files changed, 169 insertions, 31 deletions
diff --git a/src/ui/inputwidget.c b/src/ui/inputwidget.c index 8e9f0423..3daecfa8 100644 --- a/src/ui/inputwidget.c +++ b/src/ui/inputwidget.c | |||
@@ -36,11 +36,14 @@ struct Impl_InputWidget { | |||
36 | enum iInputMode mode; | 36 | enum iInputMode mode; |
37 | iBool isSensitive; | 37 | iBool isSensitive; |
38 | iBool enterPressed; | 38 | iBool enterPressed; |
39 | iBool selectAllOnFocus; | ||
39 | size_t maxLen; | 40 | size_t maxLen; |
40 | iArray text; /* iChar[] */ | 41 | iArray text; /* iChar[] */ |
41 | iArray oldText; /* iChar[] */ | 42 | iArray oldText; /* iChar[] */ |
42 | iString hint; | 43 | iString hint; |
43 | size_t cursor; | 44 | size_t cursor; |
45 | size_t lastCursor; | ||
46 | iRanges mark; | ||
44 | int font; | 47 | int font; |
45 | iClick click; | 48 | iClick click; |
46 | uint32_t timer; | 49 | uint32_t timer; |
@@ -57,8 +60,10 @@ void init_InputWidget(iInputWidget *d, size_t maxLen) { | |||
57 | init_String(&d->hint); | 60 | init_String(&d->hint); |
58 | d->font = uiInput_FontId; | 61 | d->font = uiInput_FontId; |
59 | d->cursor = 0; | 62 | d->cursor = 0; |
60 | d->isSensitive = iFalse; | 63 | iZap(d->mark); |
61 | d->enterPressed = iFalse; | 64 | d->isSensitive = iFalse; |
65 | d->enterPressed = iFalse; | ||
66 | d->selectAllOnFocus = iFalse; | ||
62 | setMaxLen_InputWidget(d, maxLen); | 67 | setMaxLen_InputWidget(d, maxLen); |
63 | /* Caller must arrange the width, but the height is fixed. */ | 68 | /* Caller must arrange the width, but the height is fixed. */ |
64 | w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; | 69 | w->rect.size.y = lineHeight_Text(default_FontId) + 2 * gap_UI; |
@@ -121,10 +126,6 @@ void setTextCStr_InputWidget(iInputWidget *d, const char *cstr) { | |||
121 | delete_String(str); | 126 | delete_String(str); |
122 | } | 127 | } |
123 | 128 | ||
124 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
125 | d->cursor = iMin(pos, size_Array(&d->text)); | ||
126 | } | ||
127 | |||
128 | static uint32_t refreshTimer_(uint32_t interval, void *d) { | 129 | static uint32_t refreshTimer_(uint32_t interval, void *d) { |
129 | refresh_Widget(d); | 130 | refresh_Widget(d); |
130 | return interval; | 131 | return interval; |
@@ -149,6 +150,12 @@ void begin_InputWidget(iInputWidget *d) { | |||
149 | refresh_Widget(w); | 150 | refresh_Widget(w); |
150 | d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); | 151 | d->timer = SDL_AddTimer(REFRESH_INTERVAL, refreshTimer_, d); |
151 | d->enterPressed = iFalse; | 152 | d->enterPressed = iFalse; |
153 | if (d->selectAllOnFocus) { | ||
154 | d->mark = (iRanges){ 0, size_Array(&d->text) }; | ||
155 | } | ||
156 | else { | ||
157 | iZap(d->mark); | ||
158 | } | ||
152 | } | 159 | } |
153 | 160 | ||
154 | void end_InputWidget(iInputWidget *d, iBool accept) { | 161 | void end_InputWidget(iInputWidget *d, iBool accept) { |
@@ -188,6 +195,100 @@ static void insertChar_InputWidget_(iInputWidget *d, iChar chr) { | |||
188 | refresh_Widget(as_Widget(d)); | 195 | refresh_Widget(as_Widget(d)); |
189 | } | 196 | } |
190 | 197 | ||
198 | iLocalDef size_t cursorMax_InputWidget_(const iInputWidget *d) { | ||
199 | return iMin(size_Array(&d->text), d->maxLen - 1); | ||
200 | } | ||
201 | |||
202 | iLocalDef iBool isMarking_(void) { | ||
203 | return (SDL_GetModState() & KMOD_SHIFT) != 0; | ||
204 | } | ||
205 | |||
206 | void setCursor_InputWidget(iInputWidget *d, size_t pos) { | ||
207 | if (isEmpty_Array(&d->text)) { | ||
208 | d->cursor = 0; | ||
209 | } | ||
210 | else { | ||
211 | d->cursor = iClamp(pos, 0, cursorMax_InputWidget_(d)); | ||
212 | } | ||
213 | /* Update selection. */ | ||
214 | if (isMarking_()) { | ||
215 | if (isEmpty_Range(&d->mark)) { | ||
216 | d->mark.start = d->lastCursor; | ||
217 | d->mark.end = d->cursor; | ||
218 | } | ||
219 | else { | ||
220 | d->mark.end = d->cursor; | ||
221 | } | ||
222 | } | ||
223 | else { | ||
224 | iZap(d->mark); | ||
225 | } | ||
226 | } | ||
227 | |||
228 | void setSelectAllOnFocus_InputWidget(iInputWidget *d, iBool selectAllOnFocus) { | ||
229 | d->selectAllOnFocus = selectAllOnFocus; | ||
230 | } | ||
231 | |||
232 | static iRanges mark_InputWidget_(const iInputWidget *d) { | ||
233 | return (iRanges){ iMin(d->mark.start, d->mark.end), iMax(d->mark.start, d->mark.end) }; | ||
234 | } | ||
235 | |||
236 | static void deleteMarked_InputWidget_(iInputWidget *d) { | ||
237 | const iRanges m = mark_InputWidget_(d); | ||
238 | removeRange_Array(&d->text, m); | ||
239 | setCursor_InputWidget(d, m.start); | ||
240 | iZap(d->mark); | ||
241 | } | ||
242 | |||
243 | static iBool isWordChar_InputWidget_(const iInputWidget *d, size_t pos) { | ||
244 | const iChar ch = pos < size_Array(&d->text) ? constValue_Array(&d->text, pos, iChar) : ' '; | ||
245 | return isAlphaNumeric_Char(ch); | ||
246 | } | ||
247 | |||
248 | iLocalDef iBool movePos_InputWidget_(const iInputWidget *d, size_t *pos, int dir) { | ||
249 | if (dir < 0) { | ||
250 | if (*pos > 0) (*pos)--; else return iFalse; | ||
251 | } | ||
252 | else { | ||
253 | if (*pos < cursorMax_InputWidget_(d)) (*pos)++; else return iFalse; | ||
254 | } | ||
255 | return iTrue; | ||
256 | } | ||
257 | |||
258 | static size_t skipWord_InputWidget_(const iInputWidget *d, size_t pos, int dir) { | ||
259 | const iBool startedAtNonWord = !isWordChar_InputWidget_(d, pos); | ||
260 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
261 | return pos; | ||
262 | } | ||
263 | /* Skip any non-word characters at start position. */ | ||
264 | while (!isWordChar_InputWidget_(d, pos)) { | ||
265 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
266 | return pos; | ||
267 | } | ||
268 | } | ||
269 | if (startedAtNonWord && dir > 0) { | ||
270 | return pos; /* Found the start of a word. */ | ||
271 | } | ||
272 | /* Skip the word. */ | ||
273 | while (isWordChar_InputWidget_(d, pos)) { | ||
274 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
275 | return pos; | ||
276 | } | ||
277 | } | ||
278 | if (dir > 0) { | ||
279 | /* Skip to the beginning of the word. */ | ||
280 | while (!isWordChar_InputWidget_(d, pos)) { | ||
281 | if (!movePos_InputWidget_(d, &pos, dir)) { | ||
282 | return pos; | ||
283 | } | ||
284 | } | ||
285 | } | ||
286 | else { | ||
287 | movePos_InputWidget_(d, &pos, +1); | ||
288 | } | ||
289 | return pos; | ||
290 | } | ||
291 | |||
191 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | 292 | static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { |
192 | iWidget *w = as_Widget(d); | 293 | iWidget *w = as_Widget(d); |
193 | if (isCommand_Widget(w, ev, "focus.gained")) { | 294 | if (isCommand_Widget(w, ev, "focus.gained")) { |
@@ -207,13 +308,18 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
207 | case aborted_ClickResult: | 308 | case aborted_ClickResult: |
208 | return iTrue; | 309 | return iTrue; |
209 | case finished_ClickResult: | 310 | case finished_ClickResult: |
210 | setFocus_Widget(as_Widget(d)); | 311 | if (isFocused_Widget(w)) { |
312 | |||
313 | } | ||
314 | else { | ||
315 | setFocus_Widget(w); | ||
316 | } | ||
211 | return iTrue; | 317 | return iTrue; |
212 | } | 318 | } |
213 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { | 319 | if (ev->type == SDL_KEYUP && isFocused_Widget(w)) { |
214 | return iTrue; | 320 | return iTrue; |
215 | } | 321 | } |
216 | const size_t curMax = iMin(size_Array(&d->text), d->maxLen - 1); | 322 | const size_t curMax = cursorMax_InputWidget_(d); |
217 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { | 323 | if (ev->type == SDL_KEYDOWN && isFocused_Widget(w)) { |
218 | const int key = ev->key.keysym.sym; | 324 | const int key = ev->key.keysym.sym; |
219 | const int mods = keyMods_Sym(ev->key.keysym.mod); | 325 | const int mods = keyMods_Sym(ev->key.keysym.mod); |
@@ -231,6 +337,7 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
231 | return iTrue; | 337 | return iTrue; |
232 | } | 338 | } |
233 | } | 339 | } |
340 | d->lastCursor = d->cursor; | ||
234 | switch (key) { | 341 | switch (key) { |
235 | case SDLK_RETURN: | 342 | case SDLK_RETURN: |
236 | case SDLK_KP_ENTER: | 343 | case SDLK_KP_ENTER: |
@@ -242,9 +349,13 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
242 | setFocus_Widget(NULL); | 349 | setFocus_Widget(NULL); |
243 | return iTrue; | 350 | return iTrue; |
244 | case SDLK_BACKSPACE: | 351 | case SDLK_BACKSPACE: |
245 | if (mods & KMOD_ALT) { | 352 | if (!isEmpty_Range(&d->mark)) { |
246 | clear_Array(&d->text); | 353 | deleteMarked_InputWidget_(d); |
247 | d->cursor = 0; | 354 | } |
355 | else if (mods & KMOD_ALT) { | ||
356 | d->mark.start = d->cursor; | ||
357 | d->mark.end = skipWord_InputWidget_(d, d->cursor, -1); | ||
358 | deleteMarked_InputWidget_(d); | ||
248 | } | 359 | } |
249 | else if (d->cursor > 0) { | 360 | else if (d->cursor > 0) { |
250 | remove_Array(&d->text, --d->cursor); | 361 | remove_Array(&d->text, --d->cursor); |
@@ -254,49 +365,64 @@ static iBool processEvent_InputWidget_(iInputWidget *d, const SDL_Event *ev) { | |||
254 | case SDLK_d: | 365 | case SDLK_d: |
255 | if (mods != KMOD_CTRL) break; | 366 | if (mods != KMOD_CTRL) break; |
256 | case SDLK_DELETE: | 367 | case SDLK_DELETE: |
257 | if (d->cursor < size_Array(&d->text)) { | 368 | if (!isEmpty_Range(&d->mark)) { |
369 | deleteMarked_InputWidget_(d); | ||
370 | } | ||
371 | else if (mods & KMOD_ALT) { | ||
372 | d->mark.start = d->cursor; | ||
373 | d->mark.end = skipWord_InputWidget_(d, d->cursor, +1); | ||
374 | deleteMarked_InputWidget_(d); | ||
375 | } | ||
376 | else if (d->cursor < size_Array(&d->text)) { | ||
258 | remove_Array(&d->text, d->cursor); | 377 | remove_Array(&d->text, d->cursor); |
259 | refresh_Widget(w); | 378 | refresh_Widget(w); |
260 | } | 379 | } |
261 | return iTrue; | 380 | return iTrue; |
262 | case SDLK_k: | 381 | case SDLK_k: |
263 | if (mods == KMOD_CTRL) { | 382 | if (mods == KMOD_CTRL) { |
264 | removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); | 383 | if (!isEmpty_Range(&d->mark)) { |
265 | refresh_Widget(w); | 384 | deleteMarked_InputWidget_(d); |
385 | } | ||
386 | else { | ||
387 | removeN_Array(&d->text, d->cursor, size_Array(&d->text) - d->cursor); | ||
388 | refresh_Widget(w); | ||
389 | } | ||
266 | return iTrue; | 390 | return iTrue; |
267 | } | 391 | } |
268 | break; | 392 | break; |
269 | case SDLK_HOME: | 393 | case SDLK_HOME: |
270 | case SDLK_END: | 394 | case SDLK_END: |
271 | d->cursor = (key == SDLK_HOME ? 0 : curMax); | 395 | setCursor_InputWidget(d, key == SDLK_HOME ? 0 : curMax); |
272 | refresh_Widget(w); | 396 | refresh_Widget(w); |
273 | return iTrue; | 397 | return iTrue; |
274 | case SDLK_a: | 398 | case SDLK_a: |
275 | case SDLK_e: | 399 | case SDLK_e: |
276 | if (mods == KMOD_CTRL) { | 400 | if (!(mods & ~(KMOD_CTRL | KMOD_SHIFT))) { |
277 | d->cursor = (key == 'a' ? 0 : curMax); | 401 | setCursor_InputWidget(d, key == 'a' ? 0 : curMax); |
278 | refresh_Widget(w); | 402 | refresh_Widget(w); |
279 | return iTrue; | 403 | return iTrue; |
280 | } | 404 | } |
281 | break; | 405 | break; |
282 | case SDLK_LEFT: | 406 | case SDLK_LEFT: |
407 | case SDLK_RIGHT: { | ||
408 | const int dir = (key == SDLK_LEFT ? -1 : +1); | ||
283 | if (mods & KMOD_PRIMARY) { | 409 | if (mods & KMOD_PRIMARY) { |
284 | d->cursor = 0; | 410 | setCursor_InputWidget(d, dir < 0 ? 0 : curMax); |
285 | } | 411 | } |
286 | else if (d->cursor > 0) { | 412 | else if (mods & KMOD_ALT) { |
287 | d->cursor--; | 413 | setCursor_InputWidget(d, skipWord_InputWidget_(d, d->cursor, dir)); |
288 | } | 414 | } |
289 | refresh_Widget(w); | 415 | else if (!isMarking_() && !isEmpty_Range(&d->mark)) { |
290 | return iTrue; | 416 | const iRanges m = mark_InputWidget_(d); |
291 | case SDLK_RIGHT: | 417 | setCursor_InputWidget(d, dir < 0 ? m.start : m.end); |
292 | if (mods & KMOD_PRIMARY) { | 418 | iZap(d->mark); |
293 | d->cursor = curMax; | ||
294 | } | 419 | } |
295 | else if (d->cursor < curMax) { | 420 | else if ((dir < 0 && d->cursor > 0) || (dir > 0 && d->cursor < curMax)) { |
296 | d->cursor++; | 421 | setCursor_InputWidget(d, d->cursor + dir); |
297 | } | 422 | } |
298 | refresh_Widget(w); | 423 | refresh_Widget(w); |
299 | return iTrue; | 424 | return iTrue; |
425 | } | ||
300 | case SDLK_TAB: | 426 | case SDLK_TAB: |
301 | /* Allow focus switching. */ | 427 | /* Allow focus switching. */ |
302 | return processEvent_Widget(as_Widget(d), ev); | 428 | return processEvent_Widget(as_Widget(d), ev); |
@@ -332,10 +458,10 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
332 | const uint32_t time = frameTime_Window(get_Window()); | 458 | const uint32_t time = frameTime_Window(get_Window()); |
333 | const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); | 459 | const iInt2 padding = init_I2(gap_UI / 2, gap_UI / 2); |
334 | iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); | 460 | iRect bounds = adjusted_Rect(bounds_Widget(w), padding, neg_I2(padding)); |
461 | iBool isHint = iFalse; | ||
335 | const iBool isFocused = isFocused_Widget(w); | 462 | const iBool isFocused = isFocused_Widget(w); |
336 | const iBool isHover = isHover_Widget(w) && | 463 | const iBool isHover = isHover_Widget(w) && |
337 | contains_Widget(w, mouseCoord_Window(get_Window())); | 464 | contains_Widget(w, mouseCoord_Window(get_Window())); |
338 | iBool isHint = iFalse; | ||
339 | iPaint p; | 465 | iPaint p; |
340 | init_Paint(&p); | 466 | init_Paint(&p); |
341 | iString text; | 467 | iString text; |
@@ -375,8 +501,18 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
375 | xOff = iMin(xOff, 0); | 501 | xOff = iMin(xOff, 0); |
376 | } | 502 | } |
377 | const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; | 503 | const int yOff = (height_Rect(bounds) - lineHeight_Text(d->font)) / 2; |
504 | const iInt2 textOrigin = add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)); | ||
505 | if (isFocused && !isEmpty_Range(&d->mark)) { | ||
506 | /* Draw the selected range. */ | ||
507 | const int m1 = advanceN_Text(d->font, cstr_String(&text), d->mark.start).x; | ||
508 | const int m2 = advanceN_Text(d->font, cstr_String(&text), d->mark.end).x; | ||
509 | fillRect_Paint( | ||
510 | &p, | ||
511 | (iRect){ addX_I2(textOrigin, iMin(m1, m2)), init_I2(iAbs(m2 - m1), lineHeight_Text(d->font)) }, | ||
512 | red_ColorId); | ||
513 | } | ||
378 | draw_Text(d->font, | 514 | draw_Text(d->font, |
379 | add_I2(topLeft_Rect(bounds), init_I2(xOff, yOff)), | 515 | textOrigin, |
380 | isHint ? uiAnnotation_ColorId | 516 | isHint ? uiAnnotation_ColorId |
381 | : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId | 517 | : isFocused && !isEmpty_Array(&d->text) ? uiInputTextFocused_ColorId |
382 | : uiInputText_ColorId, | 518 | : uiInputText_ColorId, |
@@ -386,8 +522,8 @@ static void draw_InputWidget_(const iInputWidget *d) { | |||
386 | /* Cursor blinking. */ | 522 | /* Cursor blinking. */ |
387 | if (isFocused && (time & 256)) { | 523 | if (isFocused && (time & 256)) { |
388 | const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); | 524 | const iInt2 prefixSize = advanceN_Text(d->font, cstr_String(&text), d->cursor); |
389 | const iInt2 curPos = init_I2(xOff + left_Rect(bounds) + prefixSize.x, | 525 | const iInt2 curPos = addX_I2(textOrigin, prefixSize.x); /* init_I2(xOff + left_Rect(bounds) + prefixSize.x, |
390 | yOff + top_Rect(bounds)); | 526 | yOff + top_Rect(bounds));*/ |
391 | const iRect curRect = { curPos, addX_I2(emSize, 1) }; | 527 | const iRect curRect = { curPos, addX_I2(emSize, 1) }; |
392 | iString cur; | 528 | iString cur; |
393 | if (d->cursor < size_Array(&d->text)) { | 529 | if (d->cursor < size_Array(&d->text)) { |
diff --git a/src/ui/inputwidget.h b/src/ui/inputwidget.h index 01f4efc3..fed9c3d9 100644 --- a/src/ui/inputwidget.h +++ b/src/ui/inputwidget.h | |||
@@ -39,6 +39,7 @@ void setMaxLen_InputWidget (iInputWidget *, size_t maxLen); | |||
39 | void setText_InputWidget (iInputWidget *, const iString *text); | 39 | void setText_InputWidget (iInputWidget *, const iString *text); |
40 | void setTextCStr_InputWidget (iInputWidget *, const char *cstr); | 40 | void setTextCStr_InputWidget (iInputWidget *, const char *cstr); |
41 | void setCursor_InputWidget (iInputWidget *, size_t pos); | 41 | void setCursor_InputWidget (iInputWidget *, size_t pos); |
42 | void setSelectAllOnFocus_InputWidget(iInputWidget *, iBool selectAllOnFocus); | ||
42 | void begin_InputWidget (iInputWidget *); | 43 | void begin_InputWidget (iInputWidget *); |
43 | void end_InputWidget (iInputWidget *, iBool accept); | 44 | void end_InputWidget (iInputWidget *, iBool accept); |
44 | 45 | ||
diff --git a/src/ui/window.c b/src/ui/window.c index fdc60423..650bc9ee 100644 --- a/src/ui/window.c +++ b/src/ui/window.c | |||
@@ -344,6 +344,7 @@ static void setupUserInterface_Window(iWindow *d) { | |||
344 | setFont_LabelWidget(lock, defaultSymbols_FontId); | 344 | setFont_LabelWidget(lock, defaultSymbols_FontId); |
345 | updateTextCStr_LabelWidget(lock, "\U0001f512"); | 345 | updateTextCStr_LabelWidget(lock, "\U0001f512"); |
346 | iInputWidget *url = new_InputWidget(0); | 346 | iInputWidget *url = new_InputWidget(0); |
347 | setSelectAllOnFocus_InputWidget(url, iTrue); | ||
347 | setId_Widget(as_Widget(url), "url"); | 348 | setId_Widget(as_Widget(url), "url"); |
348 | setTextCStr_InputWidget(url, "gemini://"); | 349 | setTextCStr_InputWidget(url, "gemini://"); |
349 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); | 350 | addChildFlags_Widget(navBar, iClob(url), expand_WidgetFlag); |