]> git.lizzy.rs Git - nothing.git/blob - src/ui/edit_field.c
Removed TODO#1219.
[nothing.git] / src / ui / edit_field.c
1 #include <stdbool.h>
2 #include <string.h>
3
4 #include "edit_field.h"
5 #include "game/camera.h"
6 #include "game/sprite_font.h"
7 #include "sdl/renderer.h"
8 #include "system/lt.h"
9 #include "system/nth_alloc.h"
10 #include "system/stacktrace.h"
11
12 #define BUFFER_CAPACITY 256
13
14 struct Edit_field
15 {
16     Lt *lt;
17     char *buffer;
18     size_t buffer_size;
19     size_t cursor;
20     Vec2f font_size;
21     Color font_color;
22 };
23
24 static void edit_field_insert_char(Edit_field *edit_field, char c);
25
26 // See: https://www.gnu.org/software/emacs/manual/html_node/emacs/Moving-Point.html
27 // For an explanation of the naming terminology for these helper methods
28 static bool is_emacs_word(char c);
29 static void forward_char(Edit_field *edit_field);
30 static void backward_char(Edit_field *edit_field);
31 static void move_beginning_of_line(Edit_field *edit_field);
32 static void move_end_of_line(Edit_field *edit_field);
33 static void forward_word(Edit_field *edit_field);
34 static void backward_word(Edit_field *edit_field);
35 static void kill_region_and_move_cursor(Edit_field *edit_field, size_t start, size_t end);
36 static void delete_char(Edit_field *edit_field);
37 static void delete_backward_char(Edit_field *edit_field);
38 static void kill_word(Edit_field *edit_field);
39 static void backward_kill_word(Edit_field *edit_field);
40 static void kill_to_end_of_line(Edit_field *edit_field);
41 static void field_buffer_cut(Edit_field *edit_field);
42 static void field_buffer_copy(Edit_field *edit_field);
43 static void field_buffer_paste(Edit_field *edit_field);
44
45 static void handle_keydown(Edit_field *edit_field, const SDL_Event *event);
46 static void handle_keydown_alt(Edit_field *edit_field, const SDL_Event *event);
47 static void handle_keydown_ctrl(Edit_field *edit_field, const SDL_Event *event);
48
49 static void edit_field_insert_char(Edit_field *edit_field, char c)
50 {
51     if (edit_field->buffer_size >= BUFFER_CAPACITY) {
52         return;
53     }
54
55     char *dest = edit_field->buffer + edit_field->cursor + 1;
56     memmove(dest, dest - 1, edit_field->buffer_size - edit_field->cursor);
57
58     edit_field->buffer[edit_field->cursor++] = c;
59     edit_field->buffer[++edit_field->buffer_size] = 0;
60 }
61
62 static inline
63 void edit_field_insert_string(Edit_field *edit_field, const char *text)
64 {
65     size_t n = strlen(text);
66     for (size_t i = 0; i < n; ++i) {
67         edit_field_insert_char(edit_field, text[i]);
68     }
69 }
70
71 // See: https://www.gnu.org/software/emacs/manual/html_node/emacs/Moving-Point.html
72 // For an explanation of the naming terminology for these helper methods
73
74 static bool is_emacs_word(char c)
75 {
76     // Word syntax table retrieved from Fundamental Mode, "C-h s"
77     // (This is not the complete syntax table)
78     return (c >= '$' && c <= '%')
79         || (c >= '0' && c <= '9')
80         || (c >= 'A' && c <= 'Z')
81         || (c >= 'a' && c <= 'z');
82 }
83
84 static void forward_char(Edit_field *edit_field)
85 {
86     // "C-f" or "<RIGHT>"
87     if (edit_field->cursor < edit_field->buffer_size) {
88         edit_field->cursor++;
89     }
90 }
91
92 static void backward_char(Edit_field *edit_field)
93 {
94     // "C-b" or "<LEFT>"
95     if (edit_field->cursor > 0) {
96         edit_field->cursor--;
97     }
98 }
99
100 static void move_beginning_of_line(Edit_field *edit_field)
101 {
102     // "C-a" or "<Home>"
103     edit_field->cursor = 0;
104 }
105
106 static void move_end_of_line(Edit_field *edit_field)
107 {
108     // "C-e" or "<End>"
109     edit_field->cursor = edit_field->buffer_size;
110 }
111
112 static void forward_word(Edit_field *edit_field)
113 {
114     // "M-f" or "C-<RIGHT>" or "M-<RIGHT>"
115     while (true) {
116         forward_char(edit_field);
117         if (edit_field->cursor >= edit_field->buffer_size) {
118             break;
119         }
120
121         char current = edit_field->buffer[edit_field->cursor];
122         char preceeding = edit_field->buffer[edit_field->cursor - 1];
123         if (!is_emacs_word(current) && is_emacs_word(preceeding)) {
124             // Reached the end of the current word
125             break;
126         }
127     }
128 }
129
130 static void backward_word(Edit_field *edit_field)
131 {
132     // "M-b" or "C-<LEFT>" or "M-<LEFT>"
133     while (true) {
134         backward_char(edit_field);
135         if (edit_field->cursor == 0) {
136             break;
137         }
138
139         char current = edit_field->buffer[edit_field->cursor];
140         char preceeding = edit_field->buffer[edit_field->cursor - 1];
141         if (is_emacs_word(current) && !is_emacs_word(preceeding)) {
142             // Reached the start of the current word
143             break;
144         }
145     }
146 }
147
148 static void kill_region_and_move_cursor(Edit_field *edit_field, size_t start, size_t end) {
149     trace_assert(end <= edit_field->buffer_size);
150
151     if (end <= start) {
152         // Nothing to delete
153         return;
154     }
155
156     size_t to_delete = end - start;
157     size_t to_move = edit_field->buffer_size - end;
158
159     if (to_move > 0) {
160         char *dest = edit_field->buffer + start;
161         memmove(dest, dest + to_delete, to_move);
162     }
163
164     edit_field->buffer[start + to_move] = 0;
165     edit_field->buffer_size -= to_delete;
166
167     edit_field->cursor = start;
168 }
169
170 static void delete_char(Edit_field *edit_field)
171 {
172     // "C-d" or "<Delete>"
173     if (edit_field->cursor >= edit_field->buffer_size) {
174         return;
175     }
176
177     kill_region_and_move_cursor(edit_field, edit_field->cursor, edit_field->cursor + 1);
178 }
179
180 static void delete_backward_char(Edit_field *edit_field)
181 {
182     // "<BACKSPACE>"
183     if (edit_field->cursor == 0) {
184         return;
185     }
186
187     kill_region_and_move_cursor(edit_field, edit_field->cursor - 1, edit_field->cursor);
188 }
189
190 static void kill_word(Edit_field *edit_field)
191 {
192     // "M-d" or "C-<Delete>"
193     size_t start = edit_field->cursor;
194     forward_word(edit_field);
195     size_t end = edit_field->cursor;
196
197     kill_region_and_move_cursor(edit_field, start, end);
198 }
199
200 static void backward_kill_word(Edit_field *edit_field)
201 {
202     // "M-<BACKSPACE>" or "C-<BACKSPACE>" or "M-<Delete>"
203     size_t end = edit_field->cursor;
204     backward_word(edit_field);
205     size_t start = edit_field->cursor;
206
207     kill_region_and_move_cursor(edit_field, start, end);
208 }
209
210 static void kill_to_end_of_line(Edit_field *edit_field) {
211     // "C-k"
212     kill_region_and_move_cursor(edit_field, edit_field->cursor,
213                                 edit_field->buffer_size);
214 }
215
216 static void field_buffer_cut(Edit_field *edit_field) {
217     // "C-w"
218     SDL_SetClipboardText(edit_field_as_text(edit_field));
219     edit_field_clean(edit_field);
220 }
221
222 static void field_buffer_copy(Edit_field *edit_field) {
223     // "M-w"
224     SDL_SetClipboardText(edit_field_as_text(edit_field));
225 }
226
227 static void field_buffer_paste(Edit_field *edit_field) {
228     // "C-y"
229     char *text = SDL_GetClipboardText();
230     edit_field_insert_string(edit_field, text);
231 }
232
233 static void handle_keydown(Edit_field *edit_field, const SDL_Event *event)
234 {
235     switch (event->key.keysym.sym) {
236     case SDLK_HOME: {
237         move_beginning_of_line(edit_field);
238     } break;
239
240     case SDLK_END: {
241         move_end_of_line(edit_field);
242     } break;
243
244     case SDLK_BACKSPACE: {
245         delete_backward_char(edit_field);
246     } break;
247
248     case SDLK_DELETE: {
249         delete_char(edit_field);
250     } break;
251
252     case SDLK_RIGHT: {
253         forward_char(edit_field);
254     } break;
255
256     case SDLK_LEFT: {
257         backward_char(edit_field);
258     } break;
259     }
260 }
261
262 static void handle_keydown_alt(Edit_field *edit_field, const SDL_Event *event)
263 {
264     switch (event->key.keysym.sym) {
265     case SDLK_BACKSPACE:
266     case SDLK_DELETE: {
267         backward_kill_word(edit_field);
268     } break;
269
270     case SDLK_RIGHT:
271     case SDLK_f: {
272         forward_word(edit_field);
273     } break;
274
275     case SDLK_LEFT:
276     case SDLK_b: {
277         backward_word(edit_field);
278     } break;
279
280     case SDLK_d: {
281         kill_word(edit_field);
282     } break;
283
284     // TODO(#1220): edit_field doesn't support selections for copy/cut operations
285     case SDLK_w: {
286         field_buffer_copy(edit_field);
287     } break;
288     }
289 }
290
291 static void handle_keydown_ctrl(Edit_field *edit_field, const SDL_Event *event)
292 {
293     switch (event->key.keysym.sym) {
294     case SDLK_BACKSPACE: {
295         backward_kill_word(edit_field);
296     } break;
297
298     case SDLK_DELETE: {
299         kill_word(edit_field);
300     } break;
301
302     case SDLK_RIGHT: {
303         forward_word(edit_field);
304     } break;
305
306     case SDLK_LEFT: {
307         backward_word(edit_field);
308     } break;
309
310     case SDLK_a: {
311         move_beginning_of_line(edit_field);
312     } break;
313
314     case SDLK_e: {
315         move_end_of_line(edit_field);
316     } break;
317
318     case SDLK_f: {
319         forward_char(edit_field);
320     } break;
321
322     case SDLK_b: {
323         backward_char(edit_field);
324     } break;
325
326     case SDLK_d: {
327         delete_char(edit_field);
328     } break;
329
330     case SDLK_k: {
331         kill_to_end_of_line(edit_field);
332     } break;
333
334     case SDLK_w:
335     case SDLK_x: {
336         field_buffer_cut(edit_field);
337     } break;
338
339     case SDLK_y:
340     case SDLK_v: {
341         field_buffer_paste(edit_field);
342     } break;
343
344     case SDLK_c: {
345         field_buffer_copy(edit_field);
346     } break;
347     }
348 }
349
350 Edit_field *create_edit_field(Vec2f font_size,
351                               Color font_color)
352 {
353     Lt *lt = create_lt();
354
355     Edit_field *const edit_field = PUSH_LT(lt, nth_calloc(1, sizeof(Edit_field)), free);
356     if (edit_field == NULL) {
357         RETURN_LT(lt, NULL);
358     }
359     edit_field->lt = lt;
360
361     edit_field->buffer = PUSH_LT(lt, nth_calloc(1, sizeof(char) * (BUFFER_CAPACITY + 10)), free);
362     if (edit_field->buffer == NULL) {
363         RETURN_LT(lt, NULL);
364     }
365
366     edit_field->buffer_size = 0;
367     edit_field->cursor = 0;
368     edit_field->font_size = font_size;
369     edit_field->font_color = font_color;
370
371     edit_field->buffer[edit_field->buffer_size] = 0;
372
373     return edit_field;
374 }
375
376 void destroy_edit_field(Edit_field *edit_field)
377 {
378     trace_assert(edit_field);
379     RETURN_LT0(edit_field->lt);
380 }
381
382 int edit_field_render_screen(const Edit_field *edit_field,
383                              const Camera *camera,
384                              Vec2f screen_position)
385 {
386     trace_assert(edit_field);
387     trace_assert(camera);
388
389     const float cursor_y_overflow = 10.0f;
390     const float cursor_width = 2.0f;
391
392     camera_render_text_screen(
393         camera,
394         edit_field->buffer,
395         edit_field->font_size,
396         edit_field->font_color,
397         screen_position);
398
399     /* TODO(#363): the size of the cursor does not correspond to font size */
400     if (camera_fill_rect_screen(
401             camera,
402             rect(screen_position.x + (float) edit_field->cursor * (float) FONT_CHAR_WIDTH * edit_field->font_size.x,
403                  screen_position.y - cursor_y_overflow,
404                  cursor_width,
405                  FONT_CHAR_HEIGHT * edit_field->font_size.y + cursor_y_overflow * 2.0f),
406             edit_field->font_color) < 0) {
407         return -1;
408     }
409
410     return 0;
411 }
412
413 int edit_field_render_world(const Edit_field *edit_field,
414                             const Camera *camera,
415                             Vec2f world_position)
416 {
417     trace_assert(edit_field);
418     trace_assert(camera);
419
420     const float cursor_y_overflow = 10.0f;
421     const float cursor_width = 2.0f;
422
423     if (camera_render_text(
424             camera,
425             edit_field->buffer,
426             edit_field->font_size,
427             edit_field->font_color,
428             world_position) < 0) {
429         return -1;
430     }
431
432     if (camera_fill_rect(
433             camera,
434             rect(world_position.x + (float) edit_field->cursor * (float) FONT_CHAR_WIDTH * edit_field->font_size.x,
435                  world_position.y - cursor_y_overflow,
436                  cursor_width,
437                  FONT_CHAR_HEIGHT * edit_field->font_size.y + cursor_y_overflow * 2.0f),
438             edit_field->font_color) < 0) {
439         return -1;
440     }
441
442     return 0;
443 }
444
445 int edit_field_event(Edit_field *edit_field, const SDL_Event *event)
446 {
447     trace_assert(edit_field);
448     trace_assert(event);
449
450     switch (event->type) {
451     case SDL_KEYDOWN: {
452         if (event->key.keysym.mod & KMOD_ALT) {
453             handle_keydown_alt(edit_field, event);
454         } else if (event->key.keysym.mod & KMOD_CTRL) {
455             handle_keydown_ctrl(edit_field, event);
456         } else {
457             handle_keydown(edit_field, event);
458         }
459     } break;
460
461     case SDL_TEXTINPUT: {
462         if ((SDL_GetModState() & (KMOD_CTRL | KMOD_ALT))) {
463             // Don't process text input if a modifier key is held
464             break;
465         }
466
467         size_t n = strlen(event->text.text);
468         for (size_t i = 0; i < n; ++i) {
469             edit_field_insert_char(edit_field, event->text.text[i]);
470         }
471     } break;
472     }
473
474     return 0;
475 }
476
477 const char *edit_field_as_text(const Edit_field *edit_field)
478 {
479     trace_assert(edit_field);
480     return edit_field->buffer;
481 }
482
483 void edit_field_replace(Edit_field *edit_field, const char *text)
484 {
485     trace_assert(edit_field);
486
487     edit_field_clean(edit_field);
488
489     if (text == NULL) {
490         return;
491     }
492
493     // TODO(#983): edit_field_replace should probably use memcpy
494     size_t n = strlen(text);
495     for (size_t i = 0; i < n; ++i) {
496         edit_field_insert_char(edit_field, text[i]);
497     }
498 }
499
500 void edit_field_clean(Edit_field *edit_field)
501 {
502     trace_assert(edit_field);
503
504     edit_field->cursor = 0;
505     edit_field->buffer_size = 0;
506     edit_field->buffer[0] = 0;
507 }
508
509 void edit_field_restyle(Edit_field *edit_field,
510                         Vec2f font_size,
511                         Color font_color)
512 {
513     trace_assert(edit_field);
514     edit_field->font_size = font_size;
515     edit_field->font_color = font_color;
516 }