URI: 
       tMake line entry slightly more usable (but only slightly) - 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
       ---
   DIR commit 004ac7555f2e18a6f439e80a8aca23e52f838621
   DIR parent 7598671835a7421e8415a978cc055a61b5d8588a
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Thu, 17 Aug 2023 22:47:04 +0200
       
       Make line entry slightly more usable (but only slightly)
       
       Diffstat:
         M .ltk/ltk.cfg                        |      12 +++++-------
         M src/entry.c                         |     271 ++++++++++++++++++++++++-------
         M src/entry.h                         |       3 +++
         M src/text_pango.c                    |      21 ---------------------
         M src/util.c                          |      21 +++++++++++++++++++++
         M src/util.h                          |       3 +++
       
       6 files changed, 244 insertions(+), 87 deletions(-)
       ---
   DIR diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
       t@@ -36,13 +36,11 @@ bind-keypress cursor-to-beginning sym home
        bind-keypress cursor-to-end sym end
        bind-keypress cursor-left sym left
        bind-keypress cursor-right sym right
       -#bind-keypress delete-backwards sym backspace
       -#bind-keypress delete-forwards sym delete
       -#bind-keypress cursor-left sym left
       -#bind-keypress cursor-right sym right
       -#bind-keypress selection-left sym left mods shift
       -#bind-keypress selection-right sym right mods shift
       -#bind-keypress select-all text a mods ctrl
       +bind-keypress select-all text a mods ctrl
       +bind-keypress delete-char-backwards sym backspace
       +bind-keypress delete-char-forwards sym delete
       +bind-keypress expand-selection-left sym left mods shift
       +bind-keypress expand-selection-right sym right mods shift
        
        # default mapping (just to silence warnings)
        [key-mapping]
   DIR diff --git a/src/entry.c b/src/entry.c
       t@@ -14,6 +14,9 @@
         * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
         */
        
       +/* FIXME: support RTL text! */
       +/* FIXME: mouse actions, copy-paste, allow opening text in external program */
       +
        #include <stdio.h>
        #include <stdlib.h>
        #include <stdint.h>
       t@@ -53,16 +56,22 @@ static int ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event);
        static int ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event);
        static int ltk_entry_mouse_leave(ltk_widget *self, ltk_motion_event *event);
        
       -/* FIXME: give entire key event, not just text */
        /* FIXME: also allow binding key release, not just press */
       -typedef void (*cb_func)(ltk_entry *, char *, size_t);
       +typedef void (*cb_func)(ltk_entry *, ltk_key_event *);
        
        /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
        
       -static void cursor_to_beginning(ltk_entry *entry, char *text, size_t len);
       -static void cursor_to_end(ltk_entry *entry, char *text, size_t len);
       -static void cursor_left(ltk_entry *entry, char *text, size_t len);
       -static void cursor_right(ltk_entry *entry, char *text, size_t len);
       +static void cursor_to_beginning(ltk_entry *entry, ltk_key_event *event);
       +static void cursor_to_end(ltk_entry *entry, ltk_key_event *event);
       +static void cursor_left(ltk_entry *entry, ltk_key_event *event);
       +static void cursor_right(ltk_entry *entry, ltk_key_event *event);
       +static void expand_selection_left(ltk_entry *entry, ltk_key_event *event);
       +static void expand_selection_right(ltk_entry *entry, ltk_key_event *event);
       +static void select_all(ltk_entry *entry, ltk_key_event *event);
       +static void delete_char_backwards(ltk_entry *entry, ltk_key_event *event);
       +static void delete_char_forwards(ltk_entry *entry, ltk_key_event *event);
       +static void recalc_ideal_size(ltk_entry *entry);
       +static void ensure_cursor_shown(ltk_entry *entry);
        
        struct key_cb {
                char *text;
       t@@ -74,6 +83,11 @@ static struct key_cb cb_map[] = {
                {"cursor-right", &cursor_right},
                {"cursor-to-beginning", &cursor_to_beginning},
                {"cursor-to-end", &cursor_to_end},
       +        {"delete-char-backwards", &delete_char_backwards},
       +        {"delete-char-forwards", &delete_char_forwards},
       +        {"expand-selection-left", &expand_selection_left},
       +        {"expand-selection-right", &expand_selection_right},
       +        {"select-all", &select_all},
        };
        
        struct keypress_cfg {
       t@@ -157,6 +171,7 @@ static struct ltk_widget_vtable vtable = {
        static struct {
                int border_width;
                ltk_color text_color;
       +        ltk_color selection_color;
                int pad;
        
                ltk_color border;
       t@@ -193,6 +208,7 @@ static ltk_theme_parseinfo parseinfo[] = {
                {"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
                {"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_ENTRY_PADDING, 0},
                {"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"selection-color", THEME_COLOR, {.color = &theme.selection_color}, {.color = "#000000"}, 0, 0, 0},
        };
        static int parseinfo_sorted = 0;
        
       t@@ -262,7 +278,7 @@ ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) {
                int text_y = (rect.h - text_h) / 2;
                ltk_rect clip_rect = (ltk_rect){text_x, text_y, rect.w - 2 * bw - 2 * theme.pad, text_h};
                ltk_text_line_draw_clipped(entry->tl, s, &theme.text_color, text_x - entry->cur_offset, text_y, clip_rect);
       -        if (entry->widget.state & LTK_FOCUSED) {
       +        if ((entry->widget.state & LTK_FOCUSED) && entry->sel_start == entry->sel_end) {
                        int x, y, w, h;
                        ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
                        /* FIXME: configure line width */
       t@@ -271,34 +287,197 @@ ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) {
                entry->widget.dirty = 0;
        }
        
       -/* FIXME: these don't need len, but they do need rawtext as well */
        static void
       -cursor_to_beginning(ltk_entry *entry, char *text, size_t len) {
       -        (void)text;
       -        (void)len;
       +set_selection(ltk_entry *entry, size_t start, size_t end) {
       +        entry->sel_start = start;
       +        entry->sel_end = end;
       +        ltk_text_line_clear_attrs(entry->tl);
       +        if (start != end)
       +                ltk_text_line_add_attr_bg(entry->tl, entry->sel_start, entry->sel_end, &theme.selection_color);
       +        entry->widget.dirty = 1;
       +        ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
       +}
       +
       +static void
       +wipe_selection(ltk_entry *entry) {
       +        set_selection(entry, 0, 0);
       +}
       +
       +static void
       +cursor_to_beginning(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        wipe_selection(entry);
                entry->pos = 0;
       +        ensure_cursor_shown(entry);
        }
        
        static void
       -cursor_to_end(ltk_entry *entry, char *text, size_t len) {
       -        (void)text;
       -        (void)len;
       +cursor_to_end(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        wipe_selection(entry);
                entry->pos = entry->len;
       +        ensure_cursor_shown(entry);
       +}
       +
       +static void
       +cursor_left(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        if (entry->sel_start != entry->sel_end)
       +                entry->pos = entry->sel_start;
       +        else
       +                entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL);
       +        wipe_selection(entry);
       +        ensure_cursor_shown(entry);
       +}
       +
       +static void
       +cursor_right(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        if (entry->sel_start != entry->sel_end)
       +                entry->pos = entry->sel_end;
       +        else
       +                entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL);
       +        wipe_selection(entry);
       +        ensure_cursor_shown(entry);
       +}
       +
       +static void
       +expand_selection(ltk_entry *entry, int dir) {
       +        size_t pos = entry->pos;
       +        size_t otherpos = entry->pos;
       +        if (entry->sel_start != entry->sel_end) {
       +                pos = entry->sel_side == 0 ? entry->sel_start : entry->sel_end;
       +                otherpos = entry->sel_side == 1 ? entry->sel_start : entry->sel_end;
       +        }
       +        size_t new = ltk_text_line_move_cursor_visually(entry->tl, pos, dir, NULL);
       +        if (new < otherpos) {
       +                set_selection(entry, new, otherpos);
       +                entry->sel_side = 0;
       +        } else if (otherpos < new) {
       +                set_selection(entry, otherpos, new);
       +                entry->sel_side = 1;
       +        } else {
       +                entry->pos = new;
       +                wipe_selection(entry);
       +        }
       +}
       +
       +static void
       +expand_selection_left(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        expand_selection(entry, -1);
       +}
       +
       +static void
       +expand_selection_right(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        expand_selection(entry, 1);
        }
        
       -/* FIXME: actually move shown text */
        static void
       -cursor_left(ltk_entry *entry, char *text, size_t len) {
       -        (void)text;
       -        (void)len;
       -        entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, -1, NULL);
       +delete_text(ltk_entry *entry, size_t start, size_t end) {
       +        memmove(entry->text + start, entry->text + end, entry->len - end);
       +        entry->len -= end - start;
       +        entry->text[entry->len] = '\0';
       +        entry->pos = start;
       +        wipe_selection(entry);
       +        ltk_text_line_set_text(entry->tl, entry->text, 0);
       +        recalc_ideal_size(entry);
       +        ensure_cursor_shown(entry);
       +        entry->widget.dirty = 1;
       +        ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
        }
        
        static void
       -cursor_right(ltk_entry *entry, char *text, size_t len) {
       -        (void)text;
       -        (void)len;
       -        entry->pos = ltk_text_line_move_cursor_visually(entry->tl, entry->pos, 1, NULL);
       +delete_char_backwards(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        if (entry->sel_start != entry->sel_end) {
       +                delete_text(entry, entry->sel_start, entry->sel_end);
       +        } else {
       +                size_t new = prev_utf8(entry->text, entry->pos);
       +                delete_text(entry, new, entry->pos);
       +        }
       +}
       +
       +static void
       +delete_char_forwards(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        if (entry->sel_start != entry->sel_end) {
       +                delete_text(entry, entry->sel_start, entry->sel_end);
       +        } else {
       +                size_t new = next_utf8(entry->text, entry->len, entry->pos);
       +                delete_text(entry, entry->pos, new);
       +        }
       +}
       +
       +static void
       +select_all(ltk_entry *entry, ltk_key_event *event) {
       +        (void)event;
       +        set_selection(entry, 0, entry->len);
       +        entry->sel_side = 0;
       +}
       +
       +static void
       +recalc_ideal_size(ltk_entry *entry) {
       +        /* FIXME: need to react to resize and adjust cur_offset */
       +        int w, h;
       +        ltk_text_line_get_size(entry->tl, &w, &h);
       +        unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad;
       +        unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad;
       +        if (ideal_w != entry->widget.ideal_w || ideal_h != entry->widget.ideal_h) {
       +                entry->widget.ideal_w = ideal_w;
       +                entry->widget.ideal_h = ideal_h;
       +                if (entry->widget.parent && entry->widget.parent->vtable->child_size_change)
       +                        entry->widget.parent->vtable->child_size_change(entry->widget.parent, &entry->widget);
       +        }
       +}
       +
       +static void
       +ensure_cursor_shown(ltk_entry *entry) {
       +        int x, y, w, h;
       +        ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
       +        /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change,
       +           and then the stuff on the next few lines is done afterwards */
       +        /* FIXME: adjustable cursor width */
       +        int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad;
       +        if (x + 1 > text_w + entry->cur_offset) {
       +                entry->cur_offset = x - text_w + 1;
       +                entry->widget.dirty = 1;
       +                ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
       +        } else if (x < entry->cur_offset) {
       +                entry->cur_offset = x;
       +                entry->widget.dirty = 1;
       +                ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
       +        }
       +}
       +
       +/* FIXME: maybe make this a regular key binding with wildcard text like in ledit? */
       +static void
       +insert_text(ltk_entry *entry, char *text, size_t len) {
       +        /* FIXME: ignore newlines, etc. */
       +        size_t new_alloc = ideal_array_size(entry->alloc, entry->len + len + 1 - (entry->sel_end - entry->sel_start));
       +        if (new_alloc != entry->alloc) {
       +                entry->text = ltk_realloc(entry->text, new_alloc);
       +                entry->alloc = new_alloc;
       +        }
       +        /* FIXME: also need to reset selecting status once mouse selections are supported */
       +        if (entry->sel_start != entry->sel_end) {
       +                entry->pos = entry->sel_start;
       +                memmove(entry->text + entry->pos + len, entry->text + entry->sel_end, entry->len - entry->sel_end);
       +                entry->len = entry->len + len - (entry->sel_end - entry->sel_start);
       +                wipe_selection(entry);
       +        } else {
       +                memmove(entry->text + entry->pos + len, entry->text + entry->pos, entry->len - entry->pos);
       +                entry->len += len;
       +        }
       +        memmove(entry->text + entry->pos, text, len);
       +        entry->pos += len;
       +        entry->text[entry->len] = '\0';
       +        ltk_text_line_set_text(entry->tl, entry->text, 0);
       +        recalc_ideal_size(entry);
       +        ensure_cursor_shown(entry);
       +        entry->widget.dirty = 1;
       +        ltk_window_invalidate_widget_rect(entry->widget.window, &entry->widget);
        }
        
        static int
       t@@ -307,51 +486,24 @@ ltk_entry_key_press(ltk_widget *self, ltk_key_event *event) {
                ltk_keypress_binding b;
                for (size_t i = 0; i < ltk_array_length(keypresses); i++) {
                        b = ltk_array_get(keypresses, i).b;
       -                /* FIXME: do this properly */
       -                if (b.sym == event->sym) {
       -                        ltk_array_get(keypresses, i).cb.func(entry, event->text, 0);
       +                /* FIXME: change naming (rawtext, text, mapped...) */
       +                /* FIXME: a bit weird to mask out shift, but if that isn't done, it
       +                   would need to be included for all mappings with capital letters */
       +                if ((b.mods == event->modmask && b.sym == event->sym) ||
       +                    (b.mods == (event->modmask & ~LTK_MOD_SHIFT) &&
       +                     ((b.text && event->mapped && !strcmp(b.text, event->mapped)) ||
       +                      (b.rawtext && event->text && !strcmp(b.rawtext, event->text))))) {
       +                        ltk_array_get(keypresses, i).cb.func(entry, event);
                                entry->widget.dirty = 1;
                                ltk_window_invalidate_widget_rect(self->window, self);
                                return 1;
                        }
                }
       -        /* FIXME: rawtext? */
                if (event->text) {
                        /* FIXME: properly handle everything */
                        if (event->text[0] == '\n' || event->text[0] == '\r' || event->text[0] == 0x1b)
                                return 0;
       -                size_t len = strlen(event->text);
       -                if (entry->alloc < entry->len + len + 1) {
       -                        size_t new_alloc = ideal_array_size(entry->alloc, entry->len + len + 1);
       -                        entry->text = ltk_realloc(entry->text, new_alloc);
       -                        entry->alloc = new_alloc;
       -                }
       -                memmove(entry->text + entry->pos + len, entry->text + entry->pos, entry->len - entry->pos);
       -                memmove(entry->text + entry->pos, event->text, len);
       -                entry->len += len;
       -                entry->pos += len;
       -                entry->text[entry->len] = '\0';
       -                ltk_text_line_set_text(entry->tl, entry->text, 0);
       -                /* FIXME: need to react to resize and adjust cur_offset */
       -                int x, y, w, h;
       -                ltk_text_line_get_size(entry->tl, &w, &h);
       -                unsigned int ideal_h = h + 2 * theme.border_width + 2 * theme.pad;
       -                unsigned int ideal_w = w + 2 * theme.border_width + 2 * theme.pad;
       -                if (ideal_w != self->ideal_w || ideal_h != self->ideal_h) {
       -                        self->ideal_w = ideal_w;
       -                        self->ideal_h = ideal_h;
       -                        if (self->parent && self->parent->vtable->child_size_change)
       -                                self->parent->vtable->child_size_change(self->parent, self);
       -                }
       -                ltk_text_line_pos_to_rect(entry->tl, entry->pos, &x, &y, &w, &h);
       -                /* FIXME: test if anything weird can happen since resize is called by parent->child_size_change,
       -                   and then the stuff on the next few lines is done afterwards */
       -                /* FIXME: adjustable cursor width */
       -                int text_w = entry->widget.lrect.w - 2 * theme.border_width - 2 * theme.pad;
       -                if (x - entry->cur_offset + 1 > text_w)
       -                        entry->cur_offset = x - text_w + 1;
       -                entry->widget.dirty = 1;
       -                ltk_window_invalidate_widget_rect(self->window, self);
       +                insert_text(entry, event->text, strlen(event->text));
                        return 1;
                }
                return 0;
       t@@ -410,6 +562,7 @@ ltk_entry_create(ltk_window *window, const char *id, char *text) {
                entry->len = strlen(text);
                entry->alloc = entry->len + 1;
                entry->pos = entry->sel_start = entry->sel_end = 0;
       +        entry->sel_side = 0;
                entry->widget.dirty = 1;
        
                return entry;
   DIR diff --git a/src/entry.h b/src/entry.h
       t@@ -29,6 +29,9 @@ typedef struct {
                char *text;
                size_t len, alloc, pos, sel_start, sel_end;
                int cur_offset;
       +        /* 0 when side of selection that is expanded by cursor keys
       +           is left, 1 when it is right */
       +        int sel_side;
        } ltk_entry;
        
        int ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value);
   DIR diff --git a/src/text_pango.c b/src/text_pango.c
       t@@ -52,27 +52,6 @@ struct ltk_text_context {
                char *default_font;
        };
        
       -size_t
       -prev_utf8(char *text, size_t index) {
       -        if (index == 0)
       -                return 0;
       -        size_t i = index - 1;
       -        /* find valid utf8 char - this probably needs to be improved */
       -        while (i > 0 && ((text[i] & 0xC0) == 0x80))
       -                i--;
       -        return i;
       -}
       -
       -size_t
       -next_utf8(char *text, size_t len, size_t index) {
       -        if (index >= len)
       -                return len;
       -        size_t i = index + 1;
       -        while (i < len && ((text[i] & 0xC0) == 0x80))
       -                i++;
       -        return i;
       -}
       -
        ltk_text_context *
        ltk_text_context_create(ltk_renderdata *data, char *default_font) {
                ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context));
   DIR diff --git a/src/util.c b/src/util.c
       t@@ -162,3 +162,24 @@ str_array_equal(const char *terminated, const char *array, size_t len) {
                }
                return 0;
        }
       +
       +size_t
       +prev_utf8(char *text, size_t index) {
       +        if (index == 0)
       +                return 0;
       +        size_t i = index - 1;
       +        /* find valid utf8 char - this probably needs to be improved */
       +        while (i > 0 && ((text[i] & 0xC0) == 0x80))
       +                i--;
       +        return i;
       +}
       +
       +size_t
       +next_utf8(char *text, size_t len, size_t index) {
       +        if (index >= len)
       +                return len;
       +        size_t i = index + 1;
       +        while (i < len && ((text[i] & 0xC0) == 0x80))
       +                i++;
       +        return i;
       +}
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -48,6 +48,9 @@ void ltk_warn(const char *format, ...);
        /* Note: this doesn't work if array contains '\0'. */
        int str_array_equal(const char *terminated, const char *array, size_t len);
        
       +size_t prev_utf8(char *text, size_t index);
       +size_t next_utf8(char *text, size_t len, size_t index);
       +
        #define LENGTH(X) (sizeof(X) / sizeof(X[0]))
        
        #endif /* _LTK_UTIL_H_ */