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