URI: 
       tentry.c - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
       tentry.c (28442B)
       ---
            1 /*
            2  * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org>
            3  *
            4  * Permission to use, copy, modify, and/or distribute this software for any
            5  * purpose with or without fee is hereby granted, provided that the above
            6  * copyright notice and this permission notice appear in all copies.
            7  *
            8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
            9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
           10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
           11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
           12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
           13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
           14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
           15  */
           16 
           17 /* FIXME: mouse actions for expanding selection (shift+click) */
           18 /* FIXME: cursors jump weirdly with bidi text
           19    (need to support strong/weak cursors in pango backend) */
           20 /* FIXME: set imspot - needs to be standardized so widgets don't all do their own thing */
           21 /* FIXME: some sort of width setting (setting a pixel width would be kind of ugly) */
           22 
           23 #include <stdio.h>
           24 #include <ctype.h>
           25 #include <stdlib.h>
           26 #include <stdint.h>
           27 #include <string.h>
           28 #include <stdarg.h>
           29 
           30 #include "proto_types.h"
           31 #include "event.h"
           32 #include "memory.h"
           33 #include "color.h"
           34 #include "rect.h"
           35 #include "widget.h"
           36 #include "ltk.h"
           37 #include "util.h"
           38 #include "text.h"
           39 #include "entry.h"
           40 #include "graphics.h"
           41 #include "surface_cache.h"
           42 #include "theme.h"
           43 #include "array.h"
           44 #include "keys.h"
           45 #include "cmd.h"
           46 
           47 #define MAX_ENTRY_BORDER_WIDTH 100
           48 #define MAX_ENTRY_PADDING 500
           49 
           50 static void ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
           51 static ltk_entry *ltk_entry_create(ltk_window *window,
           52     const char *id, char *text);
           53 static void ltk_entry_destroy(ltk_widget *self, int shallow);
           54 static void ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s);
           55 
           56 static int ltk_entry_key_press(ltk_widget *self, ltk_key_event *event);
           57 static int ltk_entry_key_release(ltk_widget *self, ltk_key_event *event);
           58 static int ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event);
           59 static int ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event);
           60 static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
           61 static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
           62 static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
           63 static void ltk_entry_cmd_return(ltk_widget *self, char *text, size_t len);
           64 
           65 /* FIXME: also allow binding key release, not just press */
           66 typedef void (*cb_func)(ltk_entry *, ltk_key_event *);
           67 
           68 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
           69 
           70 static void cursor_to_beginning(ltk_entry *entry, ltk_key_event *event);
           71 static void cursor_to_end(ltk_entry *entry, ltk_key_event *event);
           72 static void cursor_left(ltk_entry *entry, ltk_key_event *event);
           73 static void cursor_right(ltk_entry *entry, ltk_key_event *event);
           74 static void expand_selection_left(ltk_entry *entry, ltk_key_event *event);
           75 static void expand_selection_right(ltk_entry *entry, ltk_key_event *event);
           76 static void selection_to_primary(ltk_entry *entry, ltk_key_event *event);
           77 static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event);
           78 static void switch_selection_side(ltk_entry *entry, ltk_key_event *event);
           79 static void paste_primary(ltk_entry *entry, ltk_key_event *event);
           80 static void paste_clipboard(ltk_entry *entry, ltk_key_event *event);
           81 static void select_all(ltk_entry *entry, ltk_key_event *event);
           82 static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event);
           83 static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event);
           84 static void edit_external(ltk_entry *entry, ltk_key_event *event);
           85 static void recalc_ideal_size(ltk_entry *entry);
           86 static void ensure_cursor_shown(ltk_entry *entry);
           87 static void insert_text(ltk_entry *entry, char *text, size_t len, int move_cursor);
           88 
           89 struct key_cb {
           90         char *text;
           91         cb_func func;
           92 };
           93 
           94 static struct key_cb cb_map[] = {
           95         {"cursor-left", &cursor_left},
           96         {"cursor-right", &cursor_right},
           97         {"cursor-to-beginning", &cursor_to_beginning},
           98         {"cursor-to-end", &cursor_to_end},
           99         {"delete-char-backwards", &delete_char_backwards},
          100         {"delete-char-forwards", &delete_char_forwards},
          101         {"edit-external", &edit_external},
          102         {"expand-selection-left", &expand_selection_left},
          103         {"expand-selection-right", &expand_selection_right},
          104         {"paste-clipboard", &paste_clipboard},
          105         {"paste-primary", &paste_primary},
          106         {"select-all", &select_all},
          107         {"selection-to-clipboard", &selection_to_clipboard},
          108         {"selection-to-primary", &selection_to_primary},
          109         {"switch-selection-side", &switch_selection_side},
          110 };
          111 
          112 struct keypress_cfg {
          113         ltk_keypress_binding b;
          114         struct key_cb cb;
          115 };
          116 
          117 struct keyrelease_cfg {
          118         ltk_keyrelease_binding b;
          119         struct key_cb cb;
          120 };
          121 
          122 LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
          123 LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
          124 LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
          125 LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
          126 
          127 static ltk_array(keypress) *keypresses = NULL;
          128 static ltk_array(keyrelease) *keyreleases = NULL;
          129 
          130 GEN_CB_MAP_HELPERS(cb_map, struct key_cb, text)
          131 
          132 int
          133 ltk_entry_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
          134         if (!keypresses)
          135                 keypresses = ltk_array_create(keypress, 1);
          136         struct key_cb *cb = cb_map_get_entry(func_name, func_len);
          137         if (!cb)
          138                 return 1;
          139         struct keypress_cfg cfg = {b, *cb};
          140         ltk_array_append(keypress, keypresses, cfg);
          141         return 0;
          142 }
          143 
          144 int
          145 ltk_entry_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
          146         if (!keyreleases)
          147                 keyreleases = ltk_array_create(keyrelease, 1);
          148         struct key_cb *cb = cb_map_get_entry(func_name, func_len);
          149         if (!cb)
          150                 return 1;
          151         struct keyrelease_cfg cfg = {b, *cb};
          152         ltk_array_append(keyrelease, keyreleases, cfg);
          153         return 0;
          154 }
          155 
          156 static void
          157 destroy_keypress_cfg(struct keypress_cfg cfg) {
          158         ltk_keypress_binding_destroy(cfg.b);
          159 }
          160 
          161 void
          162 ltk_entry_cleanup(void) {
          163         ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
          164         ltk_array_destroy(keyrelease, keyreleases);
          165         keypresses = NULL;
          166         keyreleases = NULL;
          167 }
          168 
          169 static struct ltk_widget_vtable vtable = {
          170         .key_press = &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)