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