tentry.c - ltk - Socket-based GUI for X11 (WIP)
HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
tentry.c (28442B)
---
1 /*
2 * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org>
3 *
4 * Permission to use, copy, modify, and/or distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17 /* FIXME: mouse actions for expanding selection (shift+click) */
18 /* FIXME: cursors jump weirdly with bidi text
19 (need to support strong/weak cursors in pango backend) */
20 /* FIXME: set imspot - needs to be standardized so widgets don't all do their own thing */
21 /* FIXME: some sort of width setting (setting a pixel width would be kind of ugly) */
22
23 #include <stdio.h>
24 #include <ctype.h>
25 #include <stdlib.h>
26 #include <stdint.h>
27 #include <string.h>
28 #include <stdarg.h>
29
30 #include "proto_types.h"
31 #include "event.h"
32 #include "memory.h"
33 #include "color.h"
34 #include "rect.h"
35 #include "widget.h"
36 #include "ltk.h"
37 #include "util.h"
38 #include "text.h"
39 #include "entry.h"
40 #include "graphics.h"
41 #include "surface_cache.h"
42 #include "theme.h"
43 #include "array.h"
44 #include "keys.h"
45 #include "cmd.h"
46
47 #define MAX_ENTRY_BORDER_WIDTH 100
48 #define MAX_ENTRY_PADDING 500
49
50 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
51 static ltk_entry *ltk_entry_create(ltk_window *window,
52 const char *id, char *text);
53 static void ltk_entry_destroy(ltk_widget *self, int shallow);
54 static void ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s);
55
56 static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event);
57 static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event);
58 static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event);
59 static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event);
60 static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
61 static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
62 static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
63 static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len);
64
65 /* FIXME: also allow binding key release, not just press */
66 typedef void (*cb_func)(ltk_entry *, ltk_key_event *);
67
68 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
69
70 static void cursor_to_beginning(ltk_entry *entry, ltk_key_event *event);
71 static void cursor_to_end(ltk_entry *entry, ltk_key_event *event);
72 static void cursor_left(ltk_entry *entry, ltk_key_event *event);
73 static void cursor_right(ltk_entry *entry, ltk_key_event *event);
74 static void expand_selection_left(ltk_entry *entry, ltk_key_event *event);
75 static void expand_selection_right(ltk_entry *entry, ltk_key_event *event);
76 static void selection_to_primary(ltk_entry *entry, ltk_key_event *event);
77 static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event);
78 static void switch_selection_side(ltk_entry *entry, ltk_key_event *event);
79 static void paste_primary(ltk_entry *entry, ltk_key_event *event);
80 static void paste_clipboard(ltk_entry *entry, ltk_key_event *event);
81 static void select_all(ltk_entry *entry, ltk_key_event *event);
82 static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event);
83 static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event);
84 static void edit_external(ltk_entry *entry, ltk_key_event *event);
85 static void recalc_ideal_size(ltk_entry *entry);
86 static void ensure_cursor_shown(ltk_entry *entry);
87 static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor);
88
89 struct key_cb {
90 char *text;
91 cb_func func;
92 };
93
94 static struct key_cb cb_map[] = {
95 {"cursor-left", &cursor_left},
96 {"cursor-right", &cursor_right},
97 {"cursor-to-beginning", &cursor_to_beginning},
98 {"cursor-to-end", &cursor_to_end},
99 {"delete-char-backwards", &delete_char_backwards},
100 {"delete-char-forwards", &delete_char_forwards},
101 {"edit-external", &edit_external},
102 {"expand-selection-left", &expand_selection_left},
103 {"expand-selection-right", &expand_selection_right},
104 {"paste-clipboard", &paste_clipboard},
105 {"paste-primary", &paste_primary},
106 {"select-all", &select_all},
107 {"selection-to-clipboard", &selection_to_clipboard},
108 {"selection-to-primary", &selection_to_primary},
109 {"switch-selection-side", &switch_selection_side},
110 };
111
112 struct keypress_cfg {
113 ltk_keypress_binding b;
114 struct key_cb cb;
115 };
116
117 struct keyrelease_cfg {
118 ltk_keyrelease_binding b;
119 struct key_cb cb;
120 };
121
122 LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
123 LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
124 LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
125 LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
126
127 static ltk_array(keypress) *keypresses = NULL;
128 static ltk_array(keyrelease) *keyreleases = NULL;
129
130 GEN_CB_MAP_HELPERS(cb_map, struct key_cb, text)
131
132 int
133 ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
134 if (!keypresses)
135 keypresses = ltk_array_create(keypress, 1);
136 struct key_cb *cb = cb_map_get_entry(func_name, func_len);
137 if (!cb)
138 return 1;
139 struct keypress_cfg cfg = {b, *cb};
140 ltk_array_append(keypress, keypresses, cfg);
141 return 0;
142 }
143
144 int
145 ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
146 if (!keyreleases)
147 keyreleases = ltk_array_create(keyrelease, 1);
148 struct key_cb *cb = cb_map_get_entry(func_name, func_len);
149 if (!cb)
150 return 1;
151 struct keyrelease_cfg cfg = {b, *cb};
152 ltk_array_append(keyrelease, keyreleases, cfg);
153 return 0;
154 }
155
156 static void
157 destroy_keypress_cfg(struct keypress_cfg cfg) {
158 ltk_keypress_binding_destroy(cfg.b);
159 }
160
161 void
162 ltk_entry_cleanup(void) {
163 ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
164 ltk_array_destroy(keyrelease, keyreleases);
165 keypresses = NULL;
166 keyreleases = NULL;
167 }
168
169 static struct ltk_widget_vtable vtable = {
170 .key_press = <k_entry_key_press,
171 .key_release = <k_entry_key_release,
172 .mouse_press = <k_entry_mouse_press,
173 .mouse_release = <k_entry_mouse_release,
174 .release = NULL,
175 .motion_notify = <k_entry_motion_notify,
176 .mouse_leave = <k_entry_mouse_leave,
177 .mouse_enter = <k_entry_mouse_enter,
178 .cmd_return = <k_entry_cmd_return,
179 .change_state = NULL,
180 .get_child_at_pos = NULL,
181 .resize = NULL,
182 .hide = NULL,
183 .draw = <k_entry_draw,
184 .destroy = <k_entry_destroy,
185 .child_size_change = NULL,
186 .remove_child = NULL,
187 .type = LTK_WIDGET_ENTRY,
188 .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD,
189 };
190
191 static struct {
192 int border_width;
193 ltk_color text_color;
194 ltk_color selection_color;
195 int pad;
196
197 ltk_color border;
198 ltk_color fill;
199
200 ltk_color border_pressed;
201 ltk_color fill_pressed;
202
203 ltk_color border_hover;
204 ltk_color fill_hover;
205
206 ltk_color border_active;
207 ltk_color fill_active;
208
209 ltk_color border_disabled;
210 ltk_color fill_disabled;
211 } theme;
212
213 /* FIXME:
214 need to distinguish between active and focused keybindings - entry binding for opening
215 in external text editor should work no matter if active or focused */
216 /* FIXME: mouse press also needs to set focused */
217 static ltk_theme_parseinfo parseinfo[] = {
218 {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0},
219 {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#339999"}, 0, 0, 0},
220 {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
221 {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0},
222 {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
223 {"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_ENTRY_BORDER_WIDTH, 0},
224 {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0},
225 {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0},
226 {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
227 {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
228 {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
229 {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_ENTRY_PADDING, 0},
230 {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
231 {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0},
232 };
233 static int parseinfo_sorted = 0;
234
235 int
236 ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value) {
237 return ltk_theme_handle_value(window, "entry", prop, value, parseinfo, LENGTH(parseinfo), &parseinfo_sorted);
238 }
239
240 int
241 ltk_entry_fill_theme_defaults(ltk_window *window) {
242 return ltk_theme_fill_defaults(window, "entry", parseinfo, LENGTH(parseinfo));
243 }
244
245 void
246 ltk_entry_uninitialize_theme(ltk_window *window) {
247 ltk_theme_uninitialize(window, parseinfo, LENGTH(parseinfo));
248 }
249
250 /* FIXME: only keep text in surface to avoid large surface */
251 /* -> or maybe not even that? */
252 static void
253 ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
254 ltk_entry *entry = (ltk_entry *)self;
255 ltk_rect lrect = self->lrect;
256 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
257 if (clip_final.w <= 0 || clip_final.h <= 0)
258 return;
259 ltk_surface *s;
260 ltk_surface_cache_request_surface_size(entry->key, lrect.w, lrect.h);
261 if (!ltk_surface_cache_get_surface(entry->key, &s) || self->dirty)
262 ltk_entry_redraw_surface(entry, s);
263 ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
264 }
265
266 /* FIXME: draw cursor in different color on selection side that will be expanded */
267 static void
268 ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) {
269 ltk_rect rect = entry->widget.lrect;
270 int bw = theme.border_width;
271 ltk_color *border = NULL, *fill = NULL;
272 /* FIXME: HOVERACTIVE STATE */
273 if (entry->widget.state & LTK_DISABLED) {
274 border = &theme.border_disabled;
275 fill = &theme.fill_disabled;
276 } else if (entry->widget.state & LTK_PRESSED) {
277 border = &theme.border_pressed;
278 fill = &theme.fill_pressed;
279 } else if (entry->widget.state & LTK_ACTIVE) {
280 border = &theme.border_active;
281 fill = &theme.fill_active;
282 } else if (entry->widget.state & LTK_HOVER) {
283 border = &theme.border_hover;
284 fill = &theme.fill_hover;
285 } else {
286 border = &theme.border;
287 fill = &theme.fill;
288 }
289 rect.x = 0;
290 rect.y = 0;
291 ltk_surface_fill_rect(s, fill, rect);
292 if (bw > 0)
293 ltk_surface_draw_rect(s, border, (ltk_rect){bw / 2, bw / 2, rect.w - bw, rect.h - bw}, bw);
294
295 int text_w, text_h;
296 ltk_text_line_get_size(entry->tl, &text_w, &text_h);
297 /* FIXME: what if text_h > rect.h? */
298 int x_offset = 0;
299 if (text_w < rect.w - 2 * (bw + theme.pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL)
300 x_offset = rect.w - 2 * (bw + theme.pad) - text_w;
301 int text_x = bw + theme.pad + x_offset;
302 int text_y = (rect.h - text_h) / 2;
303 ltk_rect clip_rect = (ltk_rect){text_x, text_y, rect.w - 2 * bw - 2 * theme.pad, text_h};
304 ltk_text_line_draw_clipped(entry->tl, s, &theme.text_color, text_x - entry->cur_offset, text_y, clip_rect);
305 if ((entry->widget.state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) {
306 int x, y, w, h;
307 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
308 /* FIXME: configure line width */
309 ltk_surface_draw_rect(s, &theme.text_color, (ltk_rect){x - entry->cur_offset + text_x, y + text_y, 1, h}, 1);
310 }
311 entry->widget.dirty = 0;
312 }
313
314 static size_t
315 xy_to_pos(ltk_entry *e, int x, int y, int snap) {
316 int side = theme.border_width + theme.pad;
317 int text_w, text_h;
318 ltk_text_line_get_size(e->tl, &text_w, &text_h);
319 if (text_w < e->widget.lrect.w - 2 * side && ltk_text_line_get_softline_direction(e->tl, 0) == LTK_TEXT_RTL)
320 x -= e->widget.lrect.w - 2 * side - text_w;
321 return ltk_text_line_xy_to_pos(e->tl, x - side + e->cur_offset, y - side, snap);
322 }
323
324 static void
325 set_selection(ltk_entry *entry, size_t start, size_t end) {
326 entry->sel_start = start;
327 entry->sel_end = end;
328 ltk_text_line_clear_attrs(entry->tl);
329 if (start != end)
330 ltk_text_line_add_attr_bg(entry->tl, entry->sel_start, entry->sel_end, &theme.selection_color);
331 entry->widget.dirty = 1;
332 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
333 }
334
335 static void
336 wipe_selection(ltk_entry *entry) {
337 set_selection(entry, 0, 0);
338 }
339
340 static void
341 cursor_to_beginning(ltk_entry *entry, ltk_key_event *event) {
342 (void)event;
343 wipe_selection(entry);
344 entry->pos = 0;
345 ensure_cursor_shown(entry);
346 }
347
348 static void
349 cursor_to_end(ltk_entry *entry, ltk_key_event *event) {
350 (void)event;
351 wipe_selection(entry);
352 entry->pos = entry->len;
353 ensure_cursor_shown(entry);
354 }
355
356 static void
357 cursor_left(ltk_entry *entry, ltk_key_event *event) {
358 (void)event;
359 if (entry->sel_start != entry->sel_end)
360 entry->pos = entry->sel_start;
361 else
362 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL);
363 wipe_selection(entry);
364 ensure_cursor_shown(entry);
365 }
366
367 static void
368 cursor_right(ltk_entry *entry, ltk_key_event *event) {
369 (void)event;
370 if (entry->sel_start != entry->sel_end)
371 entry->pos = entry->sel_end;
372 else
373 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL);
374 wipe_selection(entry);
375 ensure_cursor_shown(entry);
376 }
377
378 static void
379 expand_selection(ltk_entry *entry, int dir) {
380 size_t pos = entry->pos;
381 size_t otherpos = entry->pos;
382 if (entry->sel_start != entry->sel_end) {
383 pos = entry->sel_side == 0 ? entry->sel_start : entry->sel_end;
384 otherpos = entry->sel_side == 1 ? entry->sel_start : entry->sel_end;
385 }
386 size_t new = ltk_text_line_move_cursor_visually(entry->tl, pos, dir, NULL);
387 if (new < otherpos) {
388 set_selection(entry, new, otherpos);
389 entry->sel_side = 0;
390 } else if (otherpos < new) {
391 set_selection(entry, otherpos, new);
392 entry->sel_side = 1;
393 } else {
394 entry->pos = new;
395 wipe_selection(entry);
396 }
397 selection_to_primary(entry, NULL);
398 }
399
400 /* FIXME: different programs have different behaviors when they set the selection */
401 /* FIXME: sometimes, it might be more useful to wipe the selection when sel_end == sel_start */
402 static void
403 selection_to_primary(ltk_entry *entry, ltk_key_event *event) {
404 (void)event;
405 if (entry->sel_end == entry->sel_start)
406 return;
407 txtbuf *primary = ltk_clipboard_get_primary_buffer(entry->widget.window->clipboard);
408 txtbuf_clear(primary);
409 txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
410 ltk_clipboard_set_primary_selection_owner(entry->widget.window->clipboard);
411 }
412
413 static void
414 selection_to_clipboard(ltk_entry *entry, ltk_key_event *event) {
415 (void)event;
416 if (entry->sel_end == entry->sel_start)
417 return;
418 txtbuf *clip = ltk_clipboard_get_clipboard_buffer(entry->widget.window->clipboard);
419 txtbuf_clear(clip);
420 txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
421 ltk_clipboard_set_clipboard_selection_owner(entry->widget.window->clipboard);
422 }
423 static void
424 switch_selection_side(ltk_entry *entry, ltk_key_event *event) {
425 (void)event;
426 entry->sel_side = !entry->sel_side;
427 }
428
429 static void
430 paste_primary(ltk_entry *entry, ltk_key_event *event) {
431 (void)event;
432 txtbuf *buf = ltk_clipboard_get_primary_text(entry->widget.window->clipboard);
433 if (buf)
434 insert_text(entry, buf->text, buf->len, 1);
435 }
436
437 static void
438 paste_clipboard(ltk_entry *entry, ltk_key_event *event) {
439 (void)event;
440 txtbuf *buf = ltk_clipboard_get_clipboard_text(entry->widget.window->clipboard);
441 if (buf)
442 insert_text(entry, buf->text, buf->len, 1);
443 }
444
445 static void
446 expand_selection_left(ltk_entry *entry, ltk_key_event *event) {
447 (void)event;
448 expand_selection(entry, -1);
449 }
450
451 static void
452 expand_selection_right(ltk_entry *entry, ltk_key_event *event) {
453 (void)event;
454 expand_selection(entry, 1);
455 }
456
457 static void
458 delete_text(ltk_entry *entry, size_t start, size_t end) {
459 memmove(entry->text + start, entry->text + end, entry->len - end);
460 entry->len -= end - start;
461 entry->text[entry->len] = '\0';
462 entry->pos = start;
463 wipe_selection(entry);
464 ltk_text_line_set_text(entry->tl, entry->text, 0);
465 recalc_ideal_size(entry);
466 ensure_cursor_shown(entry);
467 entry->widget.dirty = 1;
468 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
469 }
470
471 static void
472 delete_char_backwards(ltk_entry *entry, ltk_key_event *event) {
473 (void)event;
474 if (entry->sel_start != entry->sel_end) {
475 delete_text(entry, entry->sel_start, entry->sel_end);
476 } else {
477 size_t new = prev_utf8(entry->text, entry->pos);
478 delete_text(entry, new, entry->pos);
479 }
480 }
481
482 static void
483 delete_char_forwards(ltk_entry *entry, ltk_key_event *event) {
484 (void)event;
485 if (entry->sel_start != entry->sel_end) {
486 delete_text(entry, entry->sel_start, entry->sel_end);
487 } else {
488 size_t new = next_utf8(entry->text, entry->len, entry->pos);
489 delete_text(entry, entry->pos, new);
490 }
491 }
492
493 static void
494 select_all(ltk_entry *entry, ltk_key_event *event) {
495 (void)event;
496 set_selection(entry, 0, entry->len);
497 if (entry->len)
498 selection_to_primary(entry, NULL);
499 entry->sel_side = 0;
500 }
501
502 static void
503 recalc_ideal_size(ltk_entry *entry) {
504 /* FIXME: need to react to resize and adjust cur_offset */
505 int w, h;
506 ltk_text_line_get_size(entry->tl, &w, &h);
507 unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad;
508 unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad;
509 if (ideal_w != entry->widget.ideal_w || ideal_h != entry->widget.ideal_h) {
510 entry->widget.ideal_w = ideal_w;
511 entry->widget.ideal_h = ideal_h;
512 if (entry->widget.parent && entry->widget.parent->vtable->child_size_change)
513 entry->widget.parent->vtable->child_size_change(entry->widget.parent, &entry->widget);
514 }
515 }
516
517 static void
518 ensure_cursor_shown(ltk_entry *entry) {
519 int x, y, w, h;
520 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
521 /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change,
522 and then the stuff on the next few lines is done afterwards */
523 /* FIXME: adjustable cursor width */
524 int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad;
525 if (x + 1 > text_w + entry->cur_offset) {
526 entry->cur_offset = x - text_w + 1;
527 entry->widget.dirty = 1;
528 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
529 } else if (x < entry->cur_offset) {
530 entry->cur_offset = x;
531 entry->widget.dirty = 1;
532 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
533 }
534 }
535
536 /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */
537 static void
538 insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) {
539 size_t num = 0;
540 /* FIXME: this is ugly and there are probably a lot of other
541 cases that need to be handled */
542 /* FIXME: Just ignoring newlines is weird, but what other option is there? */
543 for (size_t i = 0; i < len; i++) {
544 if (text[i] == '\n' || text[i] == '\r')
545 num++;
546 }
547 size_t reallen = len - num;
548 size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start));
549 if (new_alloc != entry->alloc) {
550 entry->text = ltk_realloc(entry->text, new_alloc);
551 entry->alloc = new_alloc;
552 }
553 /* FIXME: also need to reset selecting status once mouse selections are supported */
554 if (entry->sel_start != entry->sel_end) {
555 entry->pos = entry->sel_start;
556 memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end);
557 entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start);
558 wipe_selection(entry);
559 } else {
560 memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos);
561 entry->len += reallen;
562 }
563 for (size_t i = 0, j = entry->pos; i < len; i++) {
564 if (text[i] != '\n' && text[i] != '\r')
565 entry->text[j++] = text[i];
566 }
567 if (move_cursor)
568 entry->pos += reallen;
569 entry->text[entry->len] = '\0';
570 ltk_text_line_set_text(entry->tl, entry->text, 0);
571 recalc_ideal_size(entry);
572 ensure_cursor_shown(entry);
573 entry->widget.dirty = 1;
574 ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
575 }
576
577 static void
578 ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len) {
579 ltk_entry *e = (ltk_entry *)self;
580 wipe_selection(e);
581 e->len = e->pos = 0;
582 insert_text(e, text, len, 0);
583 }
584
585 static void
586 edit_external(ltk_entry *entry, ltk_key_event *event) {
587 (void)event;
588 ltk_config *config = ltk_config_get();
589 /* FIXME: allow arguments to key mappings - this would allow to have different key mappings
590 for different editors instead of just one command */
591 if (!config->general.line_editor) {
592 ltk_warn("Unable to run external editing command: line editor not configured\n");
593 } else {
594 /* FIXME: somehow show that there was an error if this returns 1? */
595 /* FIXME: change interface to not require length of cmd */
596 ltk_window_call_cmd(entry->widget.window, &entry->widget, config->general.line_editor, strlen(config->general.line_editor), entry->text, entry->len);
597 }
598 }
599
600 static int
601 ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
602 ltk_entry *entry = (ltk_entry *)self;
603 ltk_keypress_binding b;
604 for (size_t i = 0; i < ltk_array_length(keypresses); i++) {
605 b = ltk_array_get(keypresses, i).b;
606 /* FIXME: change naming (rawtext, text, mapped...) */
607 /* FIXME: a bit weird to mask out shift, but if that isn't done, it
608 would need to be included for all mappings with capital letters */
609 if ((b.mods == event->modmask && b.sym != LTK_KEY_NONE && b.sym == event->sym) ||
610 (b.mods == (event->modmask & ~LTK_MOD_SHIFT) &&
611 ((b.text && event->mapped && !strcmp(b.text, event->mapped)) ||
612 (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) {
613 ltk_array_get(keypresses, i).cb.func(entry, event);
614 entry->widget.dirty = 1;
615 ltk_window_invalidate_widget_rect(self->window, self);
616 return 1;
617 }
618 }
619 if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) {
620 /* FIXME: properly handle everything */
621 if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
622 return 0;
623 insert_text(entry, event->text, strlen(event->text), 1);
624 return 1;
625 }
626 return 0;
627 }
628
629 static int
630 ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) {
631 (void)self; (void)event;
632 return 0;
633 }
634
635 static int
636 ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) {
637 ltk_entry *e = (ltk_entry *)self;
638 int side = theme.border_width + theme.pad;
639 if (event->x < side || event->x > self->lrect.w - side ||
640 event->y < side || event->y > self->lrect.h - side) {
641 return 0;
642 }
643 if (event->button == LTK_BUTTONL) {
644 if (event->type == LTK_3BUTTONPRESS_EVENT) {
645 select_all(e, NULL);
646 } else if (event->type == LTK_2BUTTONPRESS_EVENT) {
647 /* FIXME: use proper unicode stuff */
648 /* Note: If pango is used to determine what a word is, maybe at least
649 allow a config option to revert to the naive behavior - I hate it
650 when word boundaries stop at punctuation because it's really
651 annoying to select URLs, etc. then. */
652 e->pos = xy_to_pos(e, event->x, event->y, 0);
653 size_t cur = e->pos;
654 size_t left = 0, right = 0;
655 if (isspace(e->text[e->pos])) {
656 while (cur-- > 0) {
657 if (!isspace(e->text[cur])) {
658 left = cur + 1;
659 break;
660 }
661 }
662 for (cur = e->pos + 1; cur < e->len; cur++) {
663 if (!isspace(e->text[cur])) {
664 right = cur;
665 break;
666 } else if (cur == e->len - 1) {
667 right = cur + 1;
668 }
669 }
670 } else {
671 while (cur-- > 0) {
672 if (isspace(e->text[cur])) {
673 left = cur + 1;
674 break;
675 }
676 }
677 for (cur = e->pos + 1; cur < e->len; cur++) {
678 if (isspace(e->text[cur])) {
679 right = cur;
680 break;
681 } else if (cur == e->len - 1) {
682 right = cur + 1;
683 }
684 }
685 }
686 set_selection(e, left, right);
687 e->sel_side = 0;
688 } else if (event->type == LTK_BUTTONPRESS_EVENT) {
689 e->pos = xy_to_pos(e, event->x, event->y, 1);
690 set_selection(e, e->pos, e->pos);
691 e->selecting = 1;
692 e->sel_side = 0;
693 }
694 } else if (event->button == LTK_BUTTONM) {
695 /* FIXME: configure if this should change the position or paste at the current position
696 (see behavior in ledit) */
697 wipe_selection(e);
698 e->pos = xy_to_pos(e, event->x, event->y, 1);
699 paste_primary(e, NULL);
700 }
701 return 0;
702 }
703
704 static int
705 ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) {
706 ltk_entry *e = (ltk_entry *)self;
707 if (event->button == LTK_BUTTONL) {
708 e->selecting = 0;
709 selection_to_primary(e, NULL);
710 }
711 return 0;
712 }
713
714 static int
715 ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) {
716 ltk_entry *e = (ltk_entry *)self;
717 if (e->selecting) {
718 /* this occurs when something like deletion happens while text
719 is being selected (FIXME: a bit weird) */
720 if (e->sel_start == e->sel_end && e->pos != e->sel_start)
721 e->sel_start = e->sel_end = e->pos;
722 size_t new = xy_to_pos(e, event->x, event->y, 1);
723 size_t otherpos = e->sel_side == 1 ? e->sel_start : e->sel_end;
724 e->pos = new;
725 /* this takes care of moving the shown text when the mouse is
726 dragged to the right or left of the entry box */
727 ensure_cursor_shown(e);
728 if (new <= otherpos) {
729 set_selection(e, new, otherpos);
730 e->sel_side = 0;
731 } else if (otherpos < new) {
732 set_selection(e, otherpos, new);
733 e->sel_side = 1;
734 }
735 }
736 return 0;
737 }
738
739 /* FIXME: set cursor */
740 static int
741 ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
742 (void)self; (void)event;
743 return 0;
744 }
745
746 static int
747 ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
748 (void)self; (void)event;
749 return 0;
750 }
751
752 static ltk_entry *
753 ltk_entry_create(ltk_window *window, const char *id, char *text) {
754 ltk_entry *entry = ltk_malloc(sizeof(ltk_entry));
755
756 uint16_t font_size = window->theme->font_size;
757 entry->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1);
758 int text_w, text_h;
759 ltk_text_line_get_size(entry->tl, &text_w, &text_h);
760 ltk_fill_widget_defaults(&entry->widget, id, window, &vtable, entry->widget.ideal_w, entry->widget.ideal_h);
761 entry->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2;
762 entry->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2;
763 entry->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, entry->widget.ideal_w, entry->widget.ideal_h);
764 entry->cur_offset = 0;
765 entry->text = ltk_strdup(text);
766 entry->len = strlen(text);
767 entry->alloc = entry->len + 1;
768 entry->pos = entry->sel_start = entry->sel_end = 0;
769 entry->sel_side = 0;
770 entry->selecting = 0;
771 entry->widget.dirty = 1;
772
773 return entry;
774 }
775
776 static void
777 ltk_entry_destroy(ltk_widget *self, int shallow) {
778 (void)shallow;
779 ltk_entry *entry = (ltk_entry *)self;
780 if (!entry) {
781 ltk_warn("Tried to destroy NULL entry.\n");
782 return;
783 }
784 ltk_free(entry->text);
785 ltk_surface_cache_release_key(entry->key);
786 ltk_text_line_destroy(entry->tl);
787 ltk_free(entry);
788 }
789
790 /* FIXME: make text optional, command set-text */
791 /* entry <entry id> create <text> */
792 static int
793 ltk_entry_cmd_create(
794 ltk_window *window,
795 ltk_entry *entry_unneeded,
796 ltk_cmd_token *tokens,
797 size_t num_tokens,
798 ltk_error *err) {
799 (void)entry_unneeded;
800 ltk_cmdarg_parseinfo cmd[] = {
801 {.type = CMDARG_IGNORE, .optional = 0},
802 {.type = CMDARG_STRING, .optional = 0},
803 {.type = CMDARG_IGNORE, .optional = 0},
804 {.type = CMDARG_STRING, .optional = 0},
805 };
806 if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
807 return 1;
808 if (!ltk_widget_id_free(cmd[1].val.str)) {
809 err->type = ERR_WIDGET_ID_IN_USE;
810 err->arg = 1;
811 return 1;
812 }
813 ltk_entry *entry = ltk_entry_create(window, cmd[1].val.str, cmd[3].val.str);
814 ltk_set_widget((ltk_widget *)entry, cmd[1].val.str);
815
816 return 0;
817 }
818
819 static struct entry_cmd {
820 char *name;
821 int (*func)(ltk_window *, ltk_entry *, ltk_cmd_token *, size_t, ltk_error *);
822 int needs_all;
823 } entry_cmds[] = {
824 {"create", <k_entry_cmd_create, 1},
825 };
826
827 GEN_CMD_HELPERS(ltk_entry_cmd, LTK_WIDGET_ENTRY, ltk_entry, entry_cmds, struct entry_cmd)