ltk

GUI toolkit for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/ltk.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
Log | Files | Refs | README | LICENSE

entry.c (26787B)


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