ltk

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

commit 004ac7555f2e18a6f439e80a8aca23e52f838621
parent 7598671835a7421e8415a978cc055a61b5d8588a
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+++++-------
Msrc/entry.c | 271++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Msrc/entry.h | 3+++
Msrc/text_pango.c | 21---------------------
Msrc/util.c | 21+++++++++++++++++++++
Msrc/util.h | 3+++
6 files changed, 244 insertions(+), 87 deletions(-)

diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg @@ -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] diff --git a/src/entry.c b/src/entry.c @@ -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> @@ -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; @@ -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 { @@ -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; @@ -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; @@ -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 */ @@ -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 @@ -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; @@ -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; diff --git a/src/entry.h b/src/entry.h @@ -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); diff --git a/src/text_pango.c b/src/text_pango.c @@ -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)); diff --git a/src/util.c b/src/util.c @@ -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; +} diff --git a/src/util.h b/src/util.h @@ -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_ */