commit 8ba1124273be1373b9afbfcefbf2a4df2d6fec24
parent 5bcc196ebfd966a0d6479164d02e4a05d10b38fa
Author: lumidify <nobody@lumidify.org>
Date:   Thu, 24 Aug 2023 23:18:32 +0200
Add mouse support to text entry
Diffstat:
5 files changed, 109 insertions(+), 5 deletions(-)
diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
@@ -43,6 +43,7 @@ bind-keypress expand-selection-left sym left mods shift
 bind-keypress expand-selection-right sym right mods shift
 bind-keypress selection-to-clipboard text c mods ctrl
 bind-keypress paste-clipboard text v mods ctrl
+bind-keypress switch-selection-side text o mods alt
 
 # default mapping (just to silence warnings)
 [key-mapping]
diff --git a/src/entry.c b/src/entry.c
@@ -15,9 +15,10 @@
  */
 
 /* FIXME: support RTL text! */
-/* FIXME: mouse actions, copy-paste, allow opening text in external program */
+/* FIXME: allow opening text in external program */
 
 #include <stdio.h>
+#include <ctype.h>
 #include <stdlib.h>
 #include <stdint.h>
 #include <string.h>
@@ -69,6 +70,7 @@ 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 selection_to_primary(ltk_entry *entry, ltk_key_event *event);
 static void selection_to_clipboard(ltk_entry *entry, ltk_key_event *event);
+static void switch_selection_side(ltk_entry *entry, ltk_key_event *event);
 static void paste_primary(ltk_entry *entry, ltk_key_event *event);
 static void paste_clipboard(ltk_entry *entry, ltk_key_event *event);
 static void select_all(ltk_entry *entry, ltk_key_event *event);
@@ -97,6 +99,7 @@ static struct key_cb cb_map[] = {
 	{"select-all", &select_all},
 	{"selection-to-clipboard", &selection_to_clipboard},
 	{"selection-to-primary", &selection_to_primary},
+	{"switch-selection-side", &switch_selection_side},
 };
 
 struct keypress_cfg {
@@ -252,6 +255,7 @@ ltk_entry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect 
 	ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
 }
 
+/* FIXME: draw cursor in different color on selection side that will be expanded */
 static void
 ltk_entry_redraw_surface(ltk_entry *entry, ltk_surface *s) {
 	ltk_rect rect = entry->widget.lrect;
@@ -373,6 +377,7 @@ expand_selection(ltk_entry *entry, int dir) {
 }
 
 /* FIXME: different programs have different behaviors when they set the selection */
+/* FIXME: sometimes, it might be more useful to wipe the selection when sel_end == sel_start */
 static void
 selection_to_primary(ltk_entry *entry, ltk_key_event *event) {
 	(void)event;
@@ -394,6 +399,11 @@ selection_to_clipboard(ltk_entry *entry, ltk_key_event *event) {
 	txtbuf_appendn(clip, entry->text + entry->sel_start, entry->sel_end - entry->sel_start);
 	ltk_clipboard_set_clipboard_selection_owner(entry->widget.window->clipboard);
 }
+static void
+switch_selection_side(ltk_entry *entry, ltk_key_event *event) {
+	(void)event;
+	entry->sel_side = !entry->sel_side;
+}
 
 static void
 paste_primary(ltk_entry *entry, ltk_key_event *event) {
@@ -579,22 +589,110 @@ ltk_entry_key_release(ltk_widget *self, ltk_key_event *event) {
 
 static int
 ltk_entry_mouse_press(ltk_widget *self, ltk_button_event *event) {
-	(void)self; (void)event;
+	ltk_entry *e = (ltk_entry *)self;
+	int side = theme.border_width + theme.pad;
+	if (event->x < side || event->x > self->lrect.w - side ||
+	    event->y < side || event->y > self->lrect.h - side) {
+		return 0;
+	}
+	if (event->button == LTK_BUTTONL) {
+		if (event->type == LTK_3BUTTONPRESS_EVENT) {
+			select_all(e, NULL);
+		} else if (event->type == LTK_2BUTTONPRESS_EVENT) {
+			/* FIXME: use proper unicode stuff */
+			/* Note: If pango is used to determine what a word is, maybe at least
+			   allow a config option to revert to the naive behavior - I hate it
+			   when word boundaries stop at punctuation because it's really
+			   annoying to select URLs, etc. then. */
+			e->pos = ltk_text_line_xy_to_pos(e->tl, event->x - side + e->cur_offset, event->y - side, 0);
+			size_t cur = e->pos;
+			size_t left = 0, right = 0;
+			if (isspace(e->text[e->pos])) {
+				while (cur-- > 0) {
+					if (!isspace(e->text[cur])) {
+						left = cur + 1;
+						break;
+					}
+				}
+				for (cur = e->pos + 1; cur < e->len; cur++) {
+					if (!isspace(e->text[cur])) {
+						right = cur;
+						break;
+					} else if (cur == e->len - 1) {
+						right = cur + 1;
+					}
+				}
+			} else {
+				while (cur-- > 0) {
+					if (isspace(e->text[cur])) {
+						left = cur + 1;
+						break;
+					}
+				}
+				for (cur = e->pos + 1; cur < e->len; cur++) {
+					if (isspace(e->text[cur])) {
+						right = cur;
+						break;
+					} else if (cur == e->len - 1) {
+						right = cur + 1;
+					}
+				}
+			}
+			set_selection(e, left, right);
+			e->sel_side = 0;
+		} else if (event->type == LTK_BUTTONPRESS_EVENT) {
+			e->pos = ltk_text_line_xy_to_pos(e->tl, event->x - side + e->cur_offset, event->y - side, 1);
+			set_selection(e, e->pos, e->pos);
+			e->selecting = 1;
+			e->sel_side = 0;
+		}
+	} else if (event->button == LTK_BUTTONM) {
+		/* FIXME: configure if this should change the position or paste at the current position
+		   (see behavior in ledit) */
+		wipe_selection(e);
+		e->pos = ltk_text_line_xy_to_pos(e->tl, event->x - side + e->cur_offset, event->y - side, 1);
+		paste_primary(e, NULL);
+	}
 	return 0;
 }
 
 static int
 ltk_entry_mouse_release(ltk_widget *self, ltk_button_event *event) {
-	(void)self; (void)event;
+	ltk_entry *e = (ltk_entry *)self;
+	if (event->button == LTK_BUTTONL) {
+		e->selecting = 0;
+		selection_to_primary(e, NULL);
+	}
 	return 0;
 }
 
 static int
 ltk_entry_motion_notify(ltk_widget *self, ltk_motion_event *event) {
-	(void)self; (void)event;
+	ltk_entry *e = (ltk_entry *)self;
+	if (e->selecting) {
+		/* this occurs when something like deletion happens while text
+		   is being selected (FIXME: a bit weird) */
+		if (e->sel_start == e->sel_end && e->pos != e->sel_start)
+			e->sel_start = e->sel_end = e->pos;
+		int side = theme.border_width + theme.pad;
+		size_t new = ltk_text_line_xy_to_pos(e->tl, event->x - side + e->cur_offset, event->y - side, 1);
+		size_t otherpos = e->sel_side == 1 ? e->sel_start : e->sel_end;
+		e->pos = new;
+		/* this takes care of moving the shown text when the mouse is
+		   dragged to the right or left of the entry box */
+		ensure_cursor_shown(e);
+		if (new <= otherpos) {
+			set_selection(e, new, otherpos);
+			e->sel_side = 0;
+		} else if (otherpos < new) {
+			set_selection(e, otherpos, new);
+			e->sel_side = 1;
+		}
+	}
 	return 0;
 }
 
+/* FIXME: set cursor */
 static int
 ltk_entry_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
 	(void)self; (void)event;
@@ -625,6 +723,7 @@ ltk_entry_create(ltk_window *window, const char *id, char *text) {
 	entry->alloc = entry->len + 1;
 	entry->pos = entry->sel_start = entry->sel_end = 0;
 	entry->sel_side = 0;
+	entry->selecting = 0;
 	entry->widget.dirty = 1;
 
 	return entry;
diff --git a/src/entry.h b/src/entry.h
@@ -31,7 +31,8 @@ typedef struct {
 	int cur_offset;
 	/* 0 when side of selection that is expanded by cursor keys
 	   is left, 1 when it is right */
-	int sel_side;
+	char sel_side;
+	char selecting;
 } ltk_entry;
 
 int ltk_entry_ini_handler(ltk_window *window, const char *prop, const char *value);
diff --git a/src/event_xlib.c b/src/event_xlib.c
@@ -209,6 +209,7 @@ next_event_base(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_ind
 				next_event_valid = 1;
 			} else {
 				last_button_press[button] = xevent.xbutton.time;
+				was_2press[button] = 0;
 			}
 			*event = (ltk_event){.button = {
 				.type = LTK_BUTTONPRESS_EVENT,
@@ -273,6 +274,7 @@ next_event_base(ltk_renderdata *renderdata, ltk_clipboard *clip, size_t lang_ind
 				next_event_valid = 1;
 			} else {
 				last_button_release[button] = xevent.xbutton.time;
+				was_2release[button] = 0;
 			}
 			*event = (ltk_event){.button = {
 				.type = LTK_BUTTONRELEASE_EVENT,
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -1173,6 +1173,7 @@ ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
 		tmp = tmp->parent;
 	}
 	if (old) {
+		old->state &= ~LTK_FOCUSED;
 		ltk_widget *cur = old;
 		while (cur) {
 			if (cur == common_parent)