ltk

Socket-based GUI for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

entry.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 = &ltk_entry_key_press,
    171 	.key_release = &ltk_entry_key_release,
    172 	.mouse_press = &ltk_entry_mouse_press,
    173 	.mouse_release = &ltk_entry_mouse_release,
    174 	.release = NULL,
    175 	.motion_notify = &ltk_entry_motion_notify,
    176 	.mouse_leave = &ltk_entry_mouse_leave,
    177 	.mouse_enter = &ltk_entry_mouse_enter,
    178 	.cmd_return = &ltk_entry_cmd_return,
    179 	.change_state = NULL,
    180 	.get_child_at_pos = NULL,
    181 	.resize = NULL,
    182 	.hide = NULL,
    183 	.draw = &ltk_entry_draw,
    184 	.destroy = &ltk_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", &ltk_entry_cmd_create, 1},
    825 };
    826 
    827 GEN_CMD_HELPERS(ltk_entry_cmd, LTK_WIDGET_ENTRY, ltk_entry, entry_cmds, struct entry_cmd)