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