]> git.lizzy.rs Git - nothing.git/blob - src/ui/edit_field.c
(#988) Implement C-k to kill the current line
[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     case SDLK_k: {
290         edit_field_clean(edit_field);
291     } break;
292     }
293 }
294
295 Edit_field *create_edit_field(Vec font_size,
296                               Color font_color)
297 {
298     Lt *lt = create_lt();
299
300     Edit_field *const edit_field = PUSH_LT(lt, nth_calloc(1, sizeof(Edit_field)), free);
301     if (edit_field == NULL) {
302         RETURN_LT(lt, NULL);
303     }
304     edit_field->lt = lt;
305
306     edit_field->buffer = PUSH_LT(lt, nth_calloc(1, sizeof(char) * (BUFFER_CAPACITY + 10)), free);
307     if (edit_field->buffer == NULL) {
308         RETURN_LT(lt, NULL);
309     }
310
311     edit_field->buffer_size = 0;
312     edit_field->cursor = 0;
313     edit_field->font_size = font_size;
314     edit_field->font_color = font_color;
315
316     edit_field->buffer[edit_field->buffer_size] = 0;
317
318     return edit_field;
319 }
320
321 void destroy_edit_field(Edit_field *edit_field)
322 {
323     trace_assert(edit_field);
324     RETURN_LT0(edit_field->lt);
325 }
326
327 int edit_field_render_screen(const Edit_field *edit_field,
328                              Camera *camera,
329                              Point screen_position)
330 {
331     trace_assert(edit_field);
332     trace_assert(camera);
333
334     const float cursor_y_overflow = 10.0f;
335     const float cursor_width = 2.0f;
336
337     if (camera_render_text_screen(
338             camera,
339             edit_field->buffer,
340             edit_field->font_size,
341             edit_field->font_color,
342             screen_position) < 0) {
343         return -1;
344     }
345
346     /* TODO(#363): the size of the cursor does not correspond to font size */
347     if (camera_fill_rect_screen(
348             camera,
349             rect(screen_position.x + (float) edit_field->cursor * (float) FONT_CHAR_WIDTH * edit_field->font_size.x,
350                  screen_position.y - cursor_y_overflow,
351                  cursor_width,
352                  FONT_CHAR_HEIGHT * edit_field->font_size.y + cursor_y_overflow * 2.0f),
353             edit_field->font_color) < 0) {
354         return -1;
355     }
356
357     return 0;
358 }
359
360 int edit_field_render_world(const Edit_field *edit_field,
361                             Camera *camera,
362                             Point world_position)
363 {
364     trace_assert(edit_field);
365     trace_assert(camera);
366
367     const float cursor_y_overflow = 10.0f;
368     const float cursor_width = 2.0f;
369
370     if (camera_render_text(
371             camera,
372             edit_field->buffer,
373             edit_field->font_size,
374             edit_field->font_color,
375             world_position) < 0) {
376         return -1;
377     }
378
379     if (camera_fill_rect(
380             camera,
381             rect(world_position.x + (float) edit_field->cursor * (float) FONT_CHAR_WIDTH * edit_field->font_size.x,
382                  world_position.y - cursor_y_overflow,
383                  cursor_width,
384                  FONT_CHAR_HEIGHT * edit_field->font_size.y + cursor_y_overflow * 2.0f),
385             edit_field->font_color) < 0) {
386         return -1;
387     }
388
389     return 0;
390 }
391
392 int edit_field_event(Edit_field *edit_field, const SDL_Event *event)
393 {
394     trace_assert(edit_field);
395     trace_assert(event);
396
397     switch (event->type) {
398     case SDL_KEYDOWN: {
399         if (event->key.keysym.mod & KMOD_ALT) {
400             handle_keydown_alt(edit_field, event);
401         } else if (event->key.keysym.mod & KMOD_CTRL) {
402             handle_keydown_ctrl(edit_field, event);
403         } else {
404             handle_keydown(edit_field, event);
405         }
406     } break;
407
408     case SDL_TEXTINPUT: {
409         if ((SDL_GetModState() & (KMOD_CTRL | KMOD_ALT))) {
410             // Don't process text input if a modifier key is held
411             break;
412         }
413
414         size_t n = strlen(event->text.text);
415         for (size_t i = 0; i < n; ++i) {
416             edit_field_insert_char(edit_field, event->text.text[i]);
417         }
418     } break;
419     }
420
421     return 0;
422 }
423
424 const char *edit_field_as_text(const Edit_field *edit_field)
425 {
426     trace_assert(edit_field);
427     return edit_field->buffer;
428 }
429
430 void edit_field_replace(Edit_field *edit_field, const char *text)
431 {
432     trace_assert(edit_field);
433
434     edit_field_clean(edit_field);
435
436     if (text == NULL) {
437         return;
438     }
439
440     // TODO(#983): edit_field_replace should probably use memcpy
441     size_t n = strlen(text);
442     for (size_t i = 0; i < n; ++i) {
443         edit_field_insert_char(edit_field, text[i]);
444     }
445 }
446
447 void edit_field_clean(Edit_field *edit_field)
448 {
449     trace_assert(edit_field);
450
451     edit_field->cursor = 0;
452     edit_field->buffer_size = 0;
453     edit_field->buffer[0] = 0;
454 }
455
456 void edit_field_restyle(Edit_field *edit_field,
457                         Vec font_size,
458                         Color font_color)
459 {
460     trace_assert(edit_field);
461     edit_field->font_size = font_size;
462     edit_field->font_color = font_color;
463 }