entry.c - ltk - GUI toolkit 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)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
entry.c (26602B)
---
1 /*
2 * Copyright (c) 2022-2026 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 <ctype.h>
24 #include <stdint.h>
25 #include <string.h>
26
27 #include "entry.h"
28 #include "array.h"
29 #include "clipboard.h"
30 #include "color.h"
31 #include "event.h"
32 #include "eventdefs.h"
33 #include "graphics.h"
34 #include "ltk.h"
35 #include "memory.h"
36 #include "rect.h"
37 #include "text.h"
38 #include "txtbuf.h"
39 #include "util.h"
40 #include "widget.h"
41 #include "config.h"
42 #include "widget_internal.h"
43
44 #define MAX_ENTRY_BORDER_WIDTH 10000
45 #define MAX_ENTRY_PADDING 50000
46
47 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
48 static void ltk_entry_destroy(ltk_widget *self, int shallow);
49 static void recalc_ideal_size(ltk_entry *entry);
50 static void ltk_entry_recalc_ideal_size(ltk_widget *self);
51
52 static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event);
53 static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event);
54 static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event);
55 static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event);
56 static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
57 static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
58 static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
59 static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len);
60
61 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
62
63 static int cursor_to_beginning(ltk_widget *self, ltk_key_event *event);
64 static int cursor_to_end(ltk_widget *self, ltk_key_event *event);
65 static int cursor_left(ltk_widget *self, ltk_key_event *event);
66 static int cursor_right(ltk_widget *self, ltk_key_event *event);
67 static int expand_selection_left(ltk_widget *self, ltk_key_event *event);
68 static int expand_selection_right(ltk_widget *self, ltk_key_event *event);
69 static int selection_to_primary(ltk_widget *self, ltk_key_event *event);
70 static int selection_to_clipboard(ltk_widget *self, ltk_key_event *event);
71 static int switch_selection_side(ltk_widget *self, ltk_key_event *event);
72 static int paste_primary(ltk_widget *self, ltk_key_event *event);
73 static int paste_clipboard(ltk_widget *self, ltk_key_event *event);
74 static int select_all(ltk_widget *self, ltk_key_event *event);
75 static int delete_char_backwards(ltk_widget *self, ltk_key_event *event);
76 static int delete_char_forwards(ltk_widget *self, ltk_key_event *event);
77 static int edit_external(ltk_widget *self, ltk_key_event *event);
78
79 static void ensure_cursor_shown(ltk_entry *entry);
80 static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor);
81
82 static ltk_keybinding_cb cb_map[] = {
83 {"cursor-left", &cursor_left},
84 {"cursor-right", &cursor_right},
85 {"cursor-to-beginning", &cursor_to_beginning},
86 {"cursor-to-end", &cursor_to_end},
87 {"delete-char-backwards", &delete_char_backwards},
88 {"delete-char-forwards", &delete_char_forwards},
89 {"edit-external", &edit_external},
90 {"expand-selection-left", &expand_selection_left},
91 {"expand-selection-right", &expand_selection_right},
92 {"paste-clipboard", &paste_clipboard},
93 {"paste-primary", &paste_primary},
94 {"select-all", &select_all},
95 {"selection-to-clipboard", &selection_to_clipboard},
96 {"selection-to-primary", &selection_to_primary},
97 {"switch-selection-side", &switch_selection_side},
98 };
99
100 /* FIXME: also support keyreleases */
101 static ltk_array(keypress) *keypresses = NULL;
102
103 void
104 ltk_entry_get_keybinding_parseinfo(
105 ltk_keybinding_cb **press_cbs_ret, size_t *press_len_ret,
106 ltk_keybinding_cb **release_cbs_ret, size_t *release_len_ret,
107 ltk_array(keypress) **presses_ret, ltk_array(keyrelease) **releases_ret
108 ) {
109 *press_cbs_ret = cb_map;
110 *press_len_ret = LENGTH(cb_map);
111 *release_cbs_ret = NULL;
112 *release_len_ret = 0;
113 if (!keypresses)
114 keypresses = ltk_array_create(keypress, 1);
115 *presses_ret = keypresses;
116 *releases_ret = NULL;
117 }
118
119 void
120 ltk_entry_cleanup(void) {
121 ltk_keypress_bindings_destroy(keypresses);
122 keypresses = NULL;
123 }
124
125 static struct ltk_widget_vtable vtable = {
126 .key_press = <k_entry_key_press,
127 .key_release = <k_entry_key_release,
128 .mouse_press = <k_entry_mouse_press,
129 .mouse_release = <k_entry_mouse_release,
130 .release = NULL,
131 .motion_notify = <k_entry_motion_notify,
132 .mouse_leave = <k_entry_mouse_leave,
133 .mouse_enter = <k_entry_mouse_enter,
134 .cmd_return = <k_entry_cmd_return,
135 .change_state = NULL,
136 .get_child_at_pos = NULL,
137 .resize = NULL,
138 .hide = NULL,
139 .draw = <k_entry_draw,
140 .destroy = <k_entry_destroy,
141 .child_size_change = NULL,
142 .remove_child = NULL,
143 .recalc_ideal_size = <k_entry_recalc_ideal_size,
144 .type = LTK_WIDGET_ENTRY,
145 .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_NEEDS_KEYBOARD,
146 .invalid_signal = LTK_ENTRY_SIGNAL_INVALID,
147 };
148
149 static struct {
150 ltk_color *text_color;
151 ltk_color *selection_color;
152
153 ltk_color *border;
154 ltk_color *fill;
155
156 ltk_color *border_pressed;
157 ltk_color *fill_pressed;
158
159 ltk_color *border_hover;
160 ltk_color *fill_hover;
161
162 ltk_color *border_active;
163 ltk_color *fill_active;
164
165 ltk_color *border_disabled;
166 ltk_color *fill_disabled;
167
168 char *font;
169 ltk_size border_width;
170 ltk_size pad;
171 ltk_size font_size;
172 } theme;
173
174 /* FIXME:
175 need to distinguish between active and focused keybindings - entry binding for opening
176 in external text editor should work no matter if active or focused */
177 /* FIXME: mouse press also needs to set focused */
178 static ltk_theme_parseinfo parseinfo[] = {
179 {"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0},
180 {"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#339999"}, 0, 0, 0},
181 {"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
182 {"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#339999"}, 0, 0, 0},
183 {"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
184 {"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0},
185 {"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#113355"}, 0, 0, 0},
186 {"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
187 {"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
188 {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
189 {"border-width", THEME_SIZE, {.size = &theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_BORDER_WIDTH, 0},
190 {"pad", THEME_SIZE, {.size = &theme.pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_ENTRY_PADDING, 0},
191 {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
192 {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0},
193 {"font-size", THEME_SIZE, {.size = &theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
194 {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
195 };
196
197 void
198 ltk_entry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
199 *p = parseinfo;
200 *len = LENGTH(parseinfo);
201 }
202
203 /* FIXME: draw cursor in different color on selection side that will be expanded */
204 static void
205 ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
206 ltk_entry *entry = LTK_CAST_ENTRY(self);
207 ltk_rect lrect = self->lrect;
208 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
209 if (clip_final.w <= 0 || clip_final.h <= 0)
210 return;
211
212 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi);
213 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
214 ltk_color *border = NULL, *fill = NULL;
215 /* FIXME: HOVERACTIVE STATE */
216 if (self->state & LTK_DISABLED) {
217 border = theme.border_disabled;
218 fill = theme.fill_disabled;
219 } else if (self->state & LTK_PRESSED) {
220 border = theme.border_pressed;
221 fill = theme.fill_pressed;
222 } else if (self->state & LTK_ACTIVE) {
223 border = theme.border_active;
224 fill = theme.fill_active;
225 } else if (self->state & LTK_HOVER) {
226 border = theme.border_hover;
227 fill = theme.fill_hover;
228 } else {
229 border = theme.border;
230 fill = theme.fill;
231 }
232 ltk_rect draw_rect = {x, y, lrect.w, lrect.h};
233 ltk_rect draw_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
234 ltk_surface_fill_rect(draw_surf, fill, draw_clip);
235 if (bw > 0) {
236 ltk_surface_draw_border_clipped(
237 draw_surf, border, draw_rect, bw, LTK_BORDER_ALL, draw_clip
238 );
239 }
240
241 int text_w, text_h;
242 ltk_text_line_get_size(entry->tl, &text_w, &text_h);
243 /* FIXME: what if text_h > rect.h? */
244 int x_offset = 0;
245 if (text_w < lrect.w - 2 * (bw + pad) && ltk_text_line_get_softline_direction(entry->tl, 0) == LTK_TEXT_RTL)
246 x_offset = lrect.w - 2 * (bw + pad) - text_w;
247 int text_x = x + bw + pad + x_offset;
248 int text_y = y + (lrect.h - text_h) / 2;
249 ltk_rect clip_rect = (ltk_rect){text_x, text_y, lrect.w - 2 * bw - 2 * pad, text_h};
250 ltk_text_line_draw_clipped(entry->tl, draw_surf, theme.text_color, text_x - entry->cur_offset, text_y, clip_rect);
251 if ((self->state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) {
252 int cx, cy, cw, ch;
253 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &cx, &cy, &cw, &ch);
254 ltk_rect line_rect = {cx - entry->cur_offset + text_x, cy + text_y, 1, ch};
255 /* FIXME: configure line width */
256 ltk_surface_fill_rect(
257 draw_surf, theme.text_color,
258 ltk_rect_intersect(draw_clip, line_rect)
259 );
260 }
261 self->dirty = 0;
262 }
263
264 static size_t
265 xy_to_pos(ltk_entry *e, int x, int y, int snap) {
266 int bw = ltk_size_to_pixel(theme.border_width, LTK_CAST_WIDGET(e)->last_dpi);
267 int pad = ltk_size_to_pixel(theme.pad, LTK_CAST_WIDGET(e)->last_dpi);
268 int side = bw + pad;
269 int text_w, text_h;
270 ltk_text_line_get_size(e->tl, &text_w, &text_h);
271 if (text_w < e->widget.lrect.w - 2 * side && ltk_text_line_get_softline_direction(e->tl, 0) == LTK_TEXT_RTL)
272 x -= e->widget.lrect.w - 2 * side - text_w;
273 return ltk_text_line_xy_to_pos(e->tl, x - side + e->cur_offset, y - side, snap);
274 }
275
276 static void
277 set_selection(ltk_entry *entry, size_t start, size_t end) {
278 ltk_widget *self = LTK_CAST_WIDGET(entry);
279 entry->sel_start = start;
280 entry->sel_end = end;
281 ltk_text_line_clear_attrs(entry->tl);
282 if (start != end)
283 ltk_text_line_add_attr_bg(entry->tl, entry->sel_start, entry->sel_end, theme.selection_color);
284 self->dirty = 1;
285 ltk_window_invalidate_widget_rect(self->window, self->id);
286 }
287
288 static void
289 wipe_selection(ltk_entry *entry) {
290 set_selection(entry, 0, 0);
291 }
292
293 static int
294 cursor_to_beginning(ltk_widget *self, ltk_key_event *event) {
295 (void)event;
296 ltk_entry *entry = LTK_CAST_ENTRY(self);
297 wipe_selection(entry);
298 entry->pos = 0;
299 ensure_cursor_shown(entry);
300 return 1;
301 }
302
303 static int
304 cursor_to_end(ltk_widget *self, ltk_key_event *event) {
305 (void)event;
306 ltk_entry *entry = LTK_CAST_ENTRY(self);
307 wipe_selection(entry);
308 entry->pos = entry->len;
309 ensure_cursor_shown(entry);
310 return 1;
311 }
312
313 static int
314 cursor_left(ltk_widget *self, ltk_key_event *event) {
315 (void)event;
316 ltk_entry *entry = LTK_CAST_ENTRY(self);
317 if (entry->sel_start != entry->sel_end)
318 entry->pos = entry->sel_start;
319 else
320 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL);
321 wipe_selection(entry);
322 ensure_cursor_shown(entry);
323 return 1;
324 }
325
326 static int
327 cursor_right(ltk_widget *self, ltk_key_event *event) {
328 (void)event;
329 ltk_entry *entry = LTK_CAST_ENTRY(self);
330 if (entry->sel_start != entry->sel_end)
331 entry->pos = entry->sel_end;
332 else
333 entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL);
334 wipe_selection(entry);
335 ensure_cursor_shown(entry);
336 return 1;
337 }
338
339 static void
340 expand_selection(ltk_entry *entry, int dir) {
341 size_t pos = entry->pos;
342 size_t otherpos = entry->pos;
343 if (entry->sel_start != entry->sel_end) {
344 pos = entry->sel_side == 0 ? entry->sel_start : entry->sel_end;
345 otherpos = entry->sel_side == 1 ? entry->sel_start : entry->sel_end;
346 }
347 size_t new = ltk_text_line_move_cursor_visually(entry->tl, pos, dir, NULL);
348 if (new < otherpos) {
349 set_selection(entry, new, otherpos);
350 entry->sel_side = 0;
351 } else if (otherpos < new) {
352 set_selection(entry, otherpos, new);
353 entry->sel_side = 1;
354 } else {
355 entry->pos = new;
356 wipe_selection(entry);
357 }
358 selection_to_primary(LTK_CAST_WIDGET(entry), NULL);
359 }
360
361 /* FIXME: different programs have different behaviors when they set the selection */
362 /* FIXME: sometimes, it might be more useful to wipe the selection when sel_end == sel_start */
363 static int
364 selection_to_primary(ltk_widget *self, ltk_key_event *event) {
365 (void)event;
366 ltk_entry *entry = LTK_CAST_ENTRY(self);
367 if (entry->sel_end == entry->sel_start)
368 return 1;
369 txtbuf *primary = ltk_clipboard_get_primary_buffer(ltk_get_clipboard());
370 txtbuf_clear(primary);
371 txtbuf_appendn(primary, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
372 ltk_clipboard_set_primary_selection_owner(ltk_get_clipboard());
373 return 1;
374 }
375
376 static int
377 selection_to_clipboard(ltk_widget *self, ltk_key_event *event) {
378 (void)event;
379 ltk_entry *entry = LTK_CAST_ENTRY(self);
380 if (entry->sel_end == entry->sel_start)
381 return 1;
382 txtbuf *clip = ltk_clipboard_get_clipboard_buffer(ltk_get_clipboard());
383 txtbuf_clear(clip);
384 txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
385 ltk_clipboard_set_clipboard_selection_owner(ltk_get_clipboard());
386 return 1;
387 }
388
389 static int
390 switch_selection_side(ltk_widget *self, ltk_key_event *event) {
391 (void)event;
392 ltk_entry *entry = LTK_CAST_ENTRY(self);
393 entry->sel_side = !entry->sel_side;
394 return 1;
395 }
396
397 static int
398 paste_primary(ltk_widget *self, ltk_key_event *event) {
399 (void)event;
400 ltk_entry *entry = LTK_CAST_ENTRY(self);
401 txtbuf *buf = ltk_clipboard_get_primary_text(ltk_get_clipboard());
402 if (buf)
403 insert_text(entry, buf->text, buf->len, 1);
404 return 1;
405 }
406
407 static int
408 paste_clipboard(ltk_widget *self, ltk_key_event *event) {
409 (void)event;
410 ltk_entry *entry = LTK_CAST_ENTRY(self);
411 txtbuf *buf = ltk_clipboard_get_clipboard_text(ltk_get_clipboard());
412 if (buf)
413 insert_text(entry, buf->text, buf->len, 1);
414 return 1;
415 }
416
417 static int
418 expand_selection_left(ltk_widget *self, ltk_key_event *event) {
419 (void)event;
420 ltk_entry *entry = LTK_CAST_ENTRY(self);
421 expand_selection(entry, -1);
422 return 1;
423 }
424
425 static int
426 expand_selection_right(ltk_widget *self, ltk_key_event *event) {
427 (void)event;
428 ltk_entry *entry = LTK_CAST_ENTRY(self);
429 expand_selection(entry, 1);
430 return 1;
431 }
432
433 static void
434 delete_text(ltk_entry *entry, size_t start, size_t end) {
435 ltk_widget *self = LTK_CAST_WIDGET(entry);
436 memmove(entry->text + start, entry->text + end, entry->len - end);
437 entry->len -= end - start;
438 entry->text[entry->len] = '\0';
439 entry->pos = start;
440 wipe_selection(entry);
441 ltk_text_line_set_text(entry->tl, entry->text, 0);
442 recalc_ideal_size(entry);
443 ensure_cursor_shown(entry);
444 self->dirty = 1;
445 ltk_window_invalidate_widget_rect(self->window, self->id);
446 }
447
448 static int
449 delete_char_backwards(ltk_widget *self, ltk_key_event *event) {
450 (void)event;
451 ltk_entry *entry = LTK_CAST_ENTRY(self);
452 if (entry->sel_start != entry->sel_end) {
453 delete_text(entry, entry->sel_start, entry->sel_end);
454 } else {
455 size_t new = prev_utf8(entry->text, entry->pos);
456 delete_text(entry, new, entry->pos);
457 }
458 return 1;
459 }
460
461 static int
462 delete_char_forwards(ltk_widget *self, ltk_key_event *event) {
463 (void)event;
464 ltk_entry *entry = LTK_CAST_ENTRY(self);
465 if (entry->sel_start != entry->sel_end) {
466 delete_text(entry, entry->sel_start, entry->sel_end);
467 } else {
468 size_t new = next_utf8(entry->text, entry->len, entry->pos);
469 delete_text(entry, entry->pos, new);
470 }
471 return 1;
472 }
473
474 static int
475 select_all(ltk_widget *self, ltk_key_event *event) {
476 (void)event;
477 ltk_entry *entry = LTK_CAST_ENTRY(self);
478 set_selection(entry, 0, entry->len);
479 if (entry->len)
480 selection_to_primary(LTK_CAST_WIDGET(entry), NULL);
481 entry->sel_side = 0;
482 return 1;
483 }
484
485 static void
486 recalc_ideal_size_with_notification(ltk_entry *entry) {
487 ltk_widget *self = LTK_CAST_WIDGET(entry);
488 /* FIXME: need to react to resize and adjust cur_offset */
489 unsigned int old_w = self->ideal_w;
490 unsigned int old_h = self->ideal_h;
491 recalc_ideal_size(entry);
492 if (old_w != self->ideal_w || old_h != self->ideal_h) {
493 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
494 if (parent && parent->vtable->child_size_change)
495 parent->vtable->child_size_change(parent, self->id);
496 }
497 }
498
499 static void
500 ensure_cursor_shown(ltk_entry *entry) {
501 ltk_widget *self = LTK_CAST_WIDGET(entry);
502 int x, y, w, h;
503 ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
504 /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change,
505 and then the stuff on the next few lines is done afterwards */
506 /* FIXME: adjustable cursor width */
507 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi);
508 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
509 int text_w = self->lrect.w - 2 * bw - 2 * pad;
510 if (x + 1 > text_w + entry->cur_offset) {
511 entry->cur_offset = x - text_w + 1;
512 self->dirty = 1;
513 ltk_window_invalidate_widget_rect(self->window, self->id);
514 } else if (x < entry->cur_offset) {
515 entry->cur_offset = x;
516 self->dirty = 1;
517 ltk_window_invalidate_widget_rect(self->window, self->id);
518 }
519 }
520
521 /* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */
522 static void
523 insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor) {
524 ltk_widget *self = LTK_CAST_WIDGET(entry);
525 size_t num = 0;
526 /* FIXME: this is ugly and there are probably a lot of other
527 cases that need to be handled */
528 /* FIXME: Just ignoring newlines is weird, but what other option is there? */
529 for (size_t i = 0; i < len; i++) {
530 if (text[i] == '\n' || text[i] == '\r')
531 num++;
532 }
533 size_t reallen = len - num;
534 size_t new_alloc = ideal_array_size(entry->alloc, entry->len + reallen + 1 - (entry->sel_end - entry->sel_start));
535 if (new_alloc != entry->alloc) {
536 entry->text = ltk_realloc(entry->text, new_alloc);
537 entry->alloc = new_alloc;
538 }
539 /* FIXME: also need to reset selecting status once mouse selections are supported */
540 if (entry->sel_start != entry->sel_end) {
541 entry->pos = entry->sel_start;
542 memmove(entry->text + entry->pos + reallen, entry->text + entry->sel_end, entry->len - entry->sel_end);
543 entry->len = entry->len + reallen - (entry->sel_end - entry->sel_start);
544 wipe_selection(entry);
545 } else {
546 memmove(entry->text + entry->pos + reallen, entry->text + entry->pos, entry->len - entry->pos);
547 entry->len += reallen;
548 }
549 for (size_t i = 0, j = entry->pos; i < len; i++) {
550 if (text[i] != '\n' && text[i] != '\r')
551 entry->text[j++] = text[i];
552 }
553 if (move_cursor)
554 entry->pos += reallen;
555 entry->text[entry->len] = '\0';
556 ltk_text_line_set_text(entry->tl, entry->text, 0);
557 recalc_ideal_size_with_notification(entry);
558 ensure_cursor_shown(entry);
559 self->dirty = 1;
560 ltk_window_invalidate_widget_rect(self->window, self->id);
561 }
562
563 static void
564 ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len) {
565 ltk_entry *e = LTK_CAST_ENTRY(self);
566 wipe_selection(e);
567 e->len = e->pos = 0;
568 insert_text(e, text, len, 0);
569 }
570
571 static int
572 edit_external(ltk_widget *self, ltk_key_event *event) {
573 (void)event;
574 ltk_entry *entry = LTK_CAST_ENTRY(self);
575 ltk_general_config *config = ltk_config_get_general();
576 /* FIXME: allow arguments to key mappings - this would allow to have different key mappings
577 for different editors instead of just one command */
578 if (!config->line_editor) {
579 ltk_warn("Unable to run external editing command: line editor not configured\n");
580 } else {
581 /* FIXME: somehow show that there was an error if this returns 1? */
582 /* FIXME: change interface to not require length of cmd */
583 ltk_call_cmd(self->id, config->line_editor, entry->text, entry->len);
584 }
585 return 1;
586 }
587
588 static int
589 ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
590 ltk_entry *entry = LTK_CAST_ENTRY(self);
591 if (ltk_widget_handle_keypress_bindings(self, event, keypresses, 0)) {
592 self->dirty = 1;
593 ltk_window_invalidate_widget_rect(self->window, self->id);
594 return 1;
595 }
596 if (event->text && (event->modmask & (LTK_MOD_CTRL | LTK_MOD_ALT | LTK_MOD_SUPER)) == 0) {
597 /* FIXME: properly handle everything */
598 if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
599 return 0;
600 insert_text(entry, event->text, strlen(event->text), 1);
601 return 1;
602 }
603 return 0;
604 }
605
606 static int
607 ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) {
608 (void)self; (void)event;
609 return 0;
610 }
611
612 static int
613 ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) {
614 ltk_entry *e = LTK_CAST_ENTRY(self);
615 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi);
616 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
617 int side = bw + pad;
618 if (event->x < side || event->x > self->lrect.w - side ||
619 event->y < side || event->y > self->lrect.h - side) {
620 return 0;
621 }
622 if (event->button == LTK_BUTTONL) {
623 if (event->type == LTK_3BUTTONPRESS_EVENT) {
624 select_all(LTK_CAST_WIDGET(e), NULL);
625 } else if (event->type == LTK_2BUTTONPRESS_EVENT) {
626 /* FIXME: use proper unicode stuff */
627 /* Note: If pango is used to determine what a word is, maybe at least
628 allow a config option to revert to the naive behavior - I hate it
629 when word boundaries stop at punctuation because it's really
630 annoying to select URLs, etc. then. */
631 e->pos = xy_to_pos(e, event->x, event->y, 0);
632 size_t cur = e->pos;
633 size_t left = 0, right = 0;
634 if (isspace(e->text[e->pos])) {
635 while (cur-- > 0) {
636 if (!isspace(e->text[cur])) {
637 left = cur + 1;
638 break;
639 }
640 }
641 for (cur = e->pos + 1; cur < e->len; cur++) {
642 if (!isspace(e->text[cur])) {
643 right = cur;
644 break;
645 } else if (cur == e->len - 1) {
646 right = cur + 1;
647 }
648 }
649 } else {
650 while (cur-- > 0) {
651 if (isspace(e->text[cur])) {
652 left = cur + 1;
653 break;
654 }
655 }
656 for (cur = e->pos + 1; cur < e->len; cur++) {
657 if (isspace(e->text[cur])) {
658 right = cur;
659 break;
660 } else if (cur == e->len - 1) {
661 right = cur + 1;
662 }
663 }
664 }
665 set_selection(e, left, right);
666 e->sel_side = 0;
667 } else if (event->type == LTK_BUTTONPRESS_EVENT) {
668 e->pos = xy_to_pos(e, event->x, event->y, 1);
669 set_selection(e, e->pos, e->pos);
670 e->selecting = 1;
671 e->sel_side = 0;
672 }
673 } else if (event->button == LTK_BUTTONM) {
674 /* FIXME: configure if this should change the position or paste at the current position
675 (see behavior in ledit) */
676 wipe_selection(e);
677 e->pos = xy_to_pos(e, event->x, event->y, 1);
678 paste_primary(LTK_CAST_WIDGET(e), NULL);
679 }
680 return 0;
681 }
682
683 static int
684 ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) {
685 ltk_entry *e = LTK_CAST_ENTRY(self);
686 if (event->button == LTK_BUTTONL) {
687 e->selecting = 0;
688 selection_to_primary(LTK_CAST_WIDGET(e), NULL);
689 }
690 return 0;
691 }
692
693 static int
694 ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) {
695 ltk_entry *e = LTK_CAST_ENTRY(self);
696 if (e->selecting) {
697 /* this occurs when something like deletion happens while text
698 is being selected (FIXME: a bit weird) */
699 if (e->sel_start == e->sel_end && e->pos != e->sel_start)
700 e->sel_start = e->sel_end = e->pos;
701 size_t new = xy_to_pos(e, event->x, event->y, 1);
702 size_t otherpos = e->sel_side == 1 ? e->sel_start : e->sel_end;
703 e->pos = new;
704 /* this takes care of moving the shown text when the mouse is
705 dragged to the right or left of the entry box */
706 ensure_cursor_shown(e);
707 if (new <= otherpos) {
708 set_selection(e, new, otherpos);
709 e->sel_side = 0;
710 } else if (otherpos < new) {
711 set_selection(e, otherpos, new);
712 e->sel_side = 1;
713 }
714 }
715 return 0;
716 }
717
718 /* FIXME: set cursor */
719 static int
720 ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
721 (void)self; (void)event;
722 return 0;
723 }
724
725 static int
726 ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
727 (void)self; (void)event;
728 return 0;
729 }
730
731 static void
732 recalc_ideal_size(ltk_entry *entry) {
733 ltk_widget *self = LTK_CAST_WIDGET(entry);
734 int text_w, text_h;
735 ltk_text_line_get_size(entry->tl, &text_w, &text_h);
736 int bw = ltk_size_to_pixel(theme.border_width, self->last_dpi);
737 int pad = ltk_size_to_pixel(theme.pad, self->last_dpi);
738 self->ideal_w = text_w + bw * 2 + pad * 2;
739 self->ideal_h = text_h + bw * 2 + pad * 2;
740 }
741
742 static void
743 ltk_entry_recalc_ideal_size(ltk_widget *self) {
744 ltk_entry *entry = LTK_CAST_ENTRY(self);
745 int font_size = ltk_size_to_pixel(theme.font_size, self->last_dpi);
746 ltk_text_line_set_font_size(entry->tl, font_size);
747 recalc_ideal_size(entry);
748 }
749
750 ltk_widget_id
751 ltk_entry_create(ltk_widget_id windowid, const char *text) {
752 ltk_entry *entry = ltk_malloc(sizeof(ltk_entry));
753 ltk_widget *self = LTK_CAST_WIDGET(entry);
754 ltk_widget_id id = ltk_initialize_widget(self, windowid, &vtable, 0, 0);
755
756 entry->tl = ltk_text_line_create_const_text_default(
757 theme.font, ltk_size_to_pixel(theme.font_size, self->last_dpi), text, -1
758 );
759 recalc_ideal_size(entry);
760
761 entry->cur_offset = 0;
762 entry->text = ltk_strdup(text);
763 entry->len = strlen(text);
764 entry->alloc = entry->len + 1;
765 entry->pos = entry->sel_start = entry->sel_end = 0;
766 entry->sel_side = 0;
767 entry->selecting = 0;
768 self->dirty = 1;
769
770 return id;
771 }
772
773 static void
774 ltk_entry_destroy(ltk_widget *self, int shallow) {
775 (void)shallow;
776 ltk_entry *entry = LTK_CAST_ENTRY(self);
777 ltk_free(entry->text);
778 ltk_text_line_destroy(entry->tl);
779 ltk_free(entry);
780 }