commit b610b280bbdbda3dba8f8fc3e67961f26857da43
parent 99773bbc2bcaea288c5d8b657bdff74ae69e216a
Author: lumidify <nobody@lumidify.org>
Date:   Wed, 16 Aug 2023 22:11:10 +0200
Add double/triple-click; add explicit scroll event
Diffstat:
13 files changed, 394 insertions(+), 123 deletions(-)
diff --git a/src/array.h b/src/array.h
@@ -30,27 +30,34 @@
 #include "util.h"
 #include "memory.h"
 
-#define LTK_ARRAY_INIT_DECL_BASE(name, type, storage)							\
-typedef struct {											\
-	type *buf;											\
-	size_t buf_size;										\
-	size_t len;											\
-} ltk_array_##name;											\
-													\
-storage ltk_array_##name *ltk_array_create_##name(size_t initial_len);					\
-storage type ltk_array_pop_##name(ltk_array_##name *ar);						\
-storage void ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len);		\
-storage void ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t len);	\
-storage void ltk_array_resize_##name(ltk_array_##name *ar, size_t size);				\
-storage void ltk_array_destroy_##name(ltk_array_##name *ar);						\
-storage void ltk_array_clear_##name(ltk_array_##name *ar);						\
-storage void ltk_array_append_##name(ltk_array_##name *ar, type elem);					\
-storage void ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type));		\
-storage type ltk_array_get_safe_##name(ltk_array_##name *ar, size_t index);				\
-storage void ltk_array_set_safe_##name(ltk_array_##name *ar, size_t index, type e);
+/* FIXME: make this work on more compilers? */
+#if (defined(__GNUC__) || defined(__clang__))
+#define LTK_UNUSED_FUNC __attribute__((unused))
+#else
+#define LTK_UNUSED_FUNC
+#endif
+
+#define LTK_ARRAY_INIT_DECL_BASE(name, type, storage)									\
+typedef struct {													\
+	type *buf;													\
+	size_t buf_size;												\
+	size_t len;													\
+} ltk_array_##name;													\
+															\
+LTK_UNUSED_FUNC storage ltk_array_##name *ltk_array_create_##name(size_t initial_len);					\
+LTK_UNUSED_FUNC storage type ltk_array_pop_##name(ltk_array_##name *ar);						\
+LTK_UNUSED_FUNC storage void ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len);		\
+LTK_UNUSED_FUNC storage void ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t len);	\
+LTK_UNUSED_FUNC storage void ltk_array_resize_##name(ltk_array_##name *ar, size_t size);				\
+LTK_UNUSED_FUNC storage void ltk_array_destroy_##name(ltk_array_##name *ar);						\
+LTK_UNUSED_FUNC storage void ltk_array_clear_##name(ltk_array_##name *ar);						\
+LTK_UNUSED_FUNC storage void ltk_array_append_##name(ltk_array_##name *ar, type elem);					\
+LTK_UNUSED_FUNC storage void ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type));		\
+LTK_UNUSED_FUNC storage type ltk_array_get_safe_##name(ltk_array_##name *ar, size_t index);				\
+LTK_UNUSED_FUNC storage void ltk_array_set_safe_##name(ltk_array_##name *ar, size_t index, type e);
 
 #define LTK_ARRAY_INIT_IMPL_BASE(name, type, storage)							\
-storage ltk_array_##name *										\
+LTK_UNUSED_FUNC storage ltk_array_##name *								\
 ltk_array_create_##name(size_t initial_len) {								\
 	if (initial_len == 0)										\
 		ltk_fatal("Array length is zero\n");							\
@@ -61,7 +68,7 @@ ltk_array_create_##name(size_t initial_len) {								\
 	return ar;											\
 }													\
 													\
-storage type												\
+LTK_UNUSED_FUNC storage type										\
 ltk_array_pop_##name(ltk_array_##name *ar) {								\
 	if (ar->len == 0) 										\
 		ltk_fatal("Array empty; cannot pop.\n");						\
@@ -70,7 +77,7 @@ ltk_array_pop_##name(ltk_array_##name *ar) {								\
 }													\
 													\
 /* FIXME: having this function in the public interface is ugly */					\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len) {				\
 	if (index > ar->len) 										\
 		ltk_fatal("Array index out of bounds\n");						\
@@ -82,7 +89,7 @@ ltk_array_prepare_gap_##name(ltk_array_##name *ar, size_t index, size_t len) {		
 	    (ar->len - len - index) * sizeof(type));							\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t len) {			\
 	ltk_array_prepare_gap_##name(ar, index, len);							\
 	for (size_t i = 0; i < len; i++) {								\
@@ -90,20 +97,20 @@ ltk_array_insert_##name(ltk_array_##name *ar, size_t index, type *elem, size_t l
 	}												\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_append_##name(ltk_array_##name *ar, type elem) {						\
 	if (ar->len == ar->buf_size)									\
 		ltk_array_resize_##name(ar, ar->len + 1);						\
 	ar->buf[ar->len++] = elem;									\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_clear_##name(ltk_array_##name *ar) {								\
 	ar->len = 0;											\
 	ltk_array_resize_##name(ar, 1);									\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_resize_##name(ltk_array_##name *ar, size_t len) {						\
 	size_t new_size = ideal_array_size(ar->buf_size, len);						\
 	if (new_size != ar->buf_size) {									\
@@ -113,7 +120,7 @@ ltk_array_resize_##name(ltk_array_##name *ar, size_t len) {						\
 	}                                                                               		\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_destroy_##name(ltk_array_##name *ar) {							\
 	if (!ar)											\
 		return;											\
@@ -121,7 +128,7 @@ ltk_array_destroy_##name(ltk_array_##name *ar) {							\
 	ltk_free(ar);											\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type)) {			\
 	if (!ar)											\
 		return;											\
@@ -131,14 +138,14 @@ ltk_array_destroy_deep_##name(ltk_array_##name *ar, void (*destroy_func)(type)) 
 	ltk_array_destroy_##name(ar);									\
 }													\
 													\
-storage type												\
+LTK_UNUSED_FUNC storage type										\
 ltk_array_get_safe_##name(ltk_array_##name *ar, size_t index) {						\
 	if (index >= ar->len)										\
 		ltk_fatal("Index out of bounds.\n");							\
 	return ar->buf[index];										\
 }													\
 													\
-storage void												\
+LTK_UNUSED_FUNC storage void										\
 ltk_array_set_safe_##name(ltk_array_##name *ar, size_t index, type e) {					\
 	if (index >= ar->len)										\
 		ltk_fatal("Index out of bounds.\n");							\
diff --git a/src/box.c b/src/box.c
@@ -40,7 +40,7 @@ static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, uns
 static int ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err);
 /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow, ltk_error *err); */
 static void ltk_box_scroll(ltk_widget *self);
-static int ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event);
+static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
 static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
 static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
 
@@ -65,7 +65,8 @@ static struct ltk_widget_vtable vtable = {
 	.remove_child = <k_box_remove,
 	.key_press = NULL,
 	.key_release = NULL,
-	.mouse_press = <k_box_mouse_press,
+	.mouse_press = NULL,
+	.mouse_scroll = <k_box_mouse_scroll,
 	.mouse_release = NULL,
 	.motion_notify = NULL,
 	.get_child_at_pos = <k_box_get_child_at_pos,
@@ -471,19 +472,18 @@ ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
 }
 
 static int
-ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event) {
+ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
 	ltk_box *box = (ltk_box *)self;
-	/* FIXME: combine multiple events into one for efficiency */
-	if (event->button == LTK_BUTTON4 || event->button == LTK_BUTTON5) {
+	if (event->dy) {
+		/* FIXME: horizontal scrolling, etc. */
 		/* FIXME: configure scrollstep */
-		int delta = event->button == LTK_BUTTON4 ? -15 : 15;
+		int delta = event->dy * -15;
 		ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
 		ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
 		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		return 1;
-	} else {
-		return 0;
 	}
+	return 0;
 }
 
 /* box <box id> add <widget id> [sticky] */
diff --git a/src/entry.c b/src/entry.c
@@ -53,6 +53,8 @@ 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);
 
 /* FIXME: configure mouse actions, e.g. select-word-under-pointer, move-cursor-to-pointer */
diff --git a/src/event.h b/src/event.h
@@ -12,6 +12,12 @@ typedef struct {
 typedef struct {
 	ltk_event_type type;
 	int x, y;
+	int dx, dy;
+} ltk_scroll_event;
+
+typedef struct {
+	ltk_event_type type;
+	int x, y;
 } ltk_motion_event;
 
 typedef struct {
@@ -43,6 +49,7 @@ typedef struct {
 typedef union {
 	ltk_event_type type;
 	ltk_button_event button;
+	ltk_scroll_event scroll;
 	ltk_motion_event motion;
 	ltk_key_event key;
 	ltk_configure_event configure;
@@ -52,10 +59,9 @@ typedef union {
 
 #include "ltk.h"
 
-int ltk_events_pending(ltk_renderdata *renderdata);
 void ltk_events_cleanup(void);
 /* WARNING: Text returned in key and keyboard events must be copied before calling this function again! */
-void ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event);
+int ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event);
 void ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event);
 
 #endif /* LTK_EVENT_H */
diff --git a/src/event_xlib.c b/src/event_xlib.c
@@ -13,11 +13,35 @@
 static char *text = NULL;
 static size_t text_alloc = 0;
 static char *cur_kbd = NULL;
-
-int
-ltk_events_pending(ltk_renderdata *renderdata) {
-	return XPending(renderdata->dpy);
-}
+/* FIXME: support more buttons?
+   -> What even makes sense here? Mice can support a bunch
+   of buttons, but what is sensible here? Just adding
+   support for higher button numbers would cause problems
+   when adding other backends (e.g. SDL) that might do
+   things completely differently. */
+/* FIXME: support touch events? */
+/* times of last button press/release,
+   used to implement double/triple-click */
+static Time last_button_press[] = {0, 0, 0};
+static Time last_button_release[] = {0, 0, 0};
+/* positions of last press/release so double/triple-click is
+   only generated when the position is near enough */
+static struct point {
+	int x;
+	int y;
+} press_pos[] = {{0, 0}, {0, 0}, {0, 0}};
+static struct point release_pos[] = {{0, 0}, {0, 0}, {0, 0}};
+/* stores whether the last button press already was
+   a double-click to decide if a triple-click should
+   be generated (same for release) */
+static int was_2press[] = {0, 0, 0};
+static int was_2release[] = {0, 0, 0};
+/* Used to store special next event - currently just
+   used to implement double/triple-click because the
+   actual double/triple-click/release event is
+   generated in addition to the regular press/release */
+static int next_event_valid = 0;
+static ltk_button_event next_event;
 
 void
 ltk_events_cleanup(void) {
@@ -34,10 +58,6 @@ get_button(unsigned int button) {
 	case Button1: return LTK_BUTTONL;
 	case Button2: return LTK_BUTTONM;
 	case Button3: return LTK_BUTTONR;
-	case 4: return LTK_BUTTON4;
-	case 5: return LTK_BUTTON5;
-	case 6: return LTK_BUTTON6;
-	case 7: return LTK_BUTTON7;
 	default: return LTK_BUTTONL; /* FIXME: what to do here? */
 	}
 }
@@ -154,35 +174,138 @@ ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event) {
 	XkbFreeKeyboard(desc, XkbAllComponentsMask, True);
 }
 
-void
-ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
+#define DISTSQ(x0, y0, x1, y1) (((x1) - (x0)) * ((x1) - (x0)) + ((y1) - (y0)) * ((y1) - (y0)))
+/* return value 0 means valid event returned,
+   1 means no events pending,
+   2 means event discarded (need to call again) */
+static int
+next_event_base(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
+	if (next_event_valid) {
+		next_event_valid = 0;
+		*event = (ltk_event){.button = next_event};
+		return 0;
+	}
 	XEvent xevent;
+	if (!XPending(renderdata->dpy))
+		return 1;
 	XNextEvent(renderdata->dpy, &xevent);
 	if (renderdata->xkb_supported && xevent.type == renderdata->xkb_event_type) {
 		ltk_generate_keyboard_event(renderdata, event);
-		return;
+		return 0;
 	}
 	*event = (ltk_event){.type = LTK_UNKNOWN_EVENT};
 	if (XFilterEvent(&xevent, None))
-		return;
+		return 2;
+	int button = 0;
 	switch (xevent.type) {
 	case ButtonPress:
-		*event = (ltk_event){.button = {
-			.type = LTK_BUTTONPRESS_EVENT,
-			.button = get_button(xevent.xbutton.button),
-			.x = xevent.xbutton.x,
-			.y = xevent.xbutton.y
-		}};
+		button = xevent.xbutton.button;
+		/* FIXME: are the buttons really always defined as exactly these values? */
+		if (button >= 1 && button <= 3) {
+			if (xevent.xbutton.time - last_button_press[button] <= DOUBLECLICK_TIME &&
+			    DISTSQ(press_pos[button].x, press_pos[button].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
+				if (was_2press[button]) {
+					/* reset so normal press is sent again next time */
+					was_2press[button] = 0;
+					last_button_press[button] = 0;
+					next_event = (ltk_button_event){
+						.type = LTK_3BUTTONPRESS_EVENT,
+						.button = get_button(button),
+						.x = xevent.xbutton.x,
+						.y = xevent.xbutton.y
+					};
+				} else {
+					was_2press[button] = 1;
+					last_button_press[button] = xevent.xbutton.time;
+					next_event = (ltk_button_event){
+						.type = LTK_2BUTTONPRESS_EVENT,
+						.button = get_button(button),
+						.x = xevent.xbutton.x,
+						.y = xevent.xbutton.y
+					};
+				}
+				next_event_valid = 1;
+			} else {
+				last_button_press[button] = xevent.xbutton.time;
+			}
+			*event = (ltk_event){.button = {
+				.type = LTK_BUTTONPRESS_EVENT,
+				.button = get_button(button),
+				.x = xevent.xbutton.x,
+				.y = xevent.xbutton.y
+			}};
+			press_pos[button].x = xevent.xbutton.x;
+			press_pos[button].y = xevent.xbutton.y;
+		} else if (button >= 4 && button <= 7) {
+			/* FIXME: compress multiple scroll events into one */
+			*event = (ltk_event){.scroll = {
+				.type = LTK_SCROLL_EVENT,
+				.x = xevent.xbutton.x,
+				.y = xevent.xbutton.y,
+				.dx = 0,
+				.dy = 0
+			}};
+			switch (button) {
+			case 4:
+				event->scroll.dy = 1;
+				break;
+			case 5:
+				event->scroll.dy = -1;
+				break;
+			case 6:
+				event->scroll.dx = -1;
+				break;
+			case 7:
+				event->scroll.dx = 1;
+				break;
+			}
+		} else {
+			return 2;
+		}
 		break;
 	case ButtonRelease:
-		*event = (ltk_event){.button = {
-			.type = LTK_BUTTONRELEASE_EVENT,
-			.button = get_button(xevent.xbutton.button),
-			.x = xevent.xbutton.x,
-			.y = xevent.xbutton.y
-		}};
+		button = xevent.xbutton.button;
+		if (button >= 1 && button <= 3) {
+			if (xevent.xbutton.time - last_button_release[button] <= DOUBLECLICK_TIME &&
+			    DISTSQ(release_pos[button].x, release_pos[button].y, xevent.xbutton.x, xevent.xbutton.y) <= DOUBLECLICK_DISTSQ) {
+				if (was_2release[button]) {
+					/* reset so normal release is sent again next time */
+					was_2release[button] = 0;
+					last_button_release[button] = 0;
+					next_event = (ltk_button_event){
+						.type = LTK_3BUTTONRELEASE_EVENT,
+						.button = get_button(button),
+						.x = xevent.xbutton.x,
+						.y = xevent.xbutton.y
+					};
+				} else {
+					was_2release[button] = 1;
+					last_button_release[button] = xevent.xbutton.time;
+					next_event = (ltk_button_event){
+						.type = LTK_2BUTTONRELEASE_EVENT,
+						.button = get_button(button),
+						.x = xevent.xbutton.x,
+						.y = xevent.xbutton.y
+					};
+				}
+				next_event_valid = 1;
+			} else {
+				last_button_release[button] = xevent.xbutton.time;
+			}
+			*event = (ltk_event){.button = {
+				.type = LTK_BUTTONRELEASE_EVENT,
+				.button = get_button(button),
+				.x = xevent.xbutton.x,
+				.y = xevent.xbutton.y
+			}};
+			release_pos[button].x = xevent.xbutton.x;
+			release_pos[button].y = xevent.xbutton.y;
+		} else {
+			return 2;
+		}
 		break;
 	case MotionNotify:
+		/* FIXME: compress motion events */
 		*event = (ltk_event){.motion = {
 			.type = LTK_MOTION_EVENT,
 			.x = xevent.xmotion.x,
@@ -216,13 +339,27 @@ ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) 
 				.w = xevent.xexpose.width,
 				.h = xevent.xexpose.height
 			}};
+		} else {
+			return 2;
 		}
 		break;
 	case ClientMessage:
 		if ((Atom)xevent.xclient.data.l[0] == renderdata->wm_delete_msg)
 			*event = (ltk_event){.type = LTK_WINDOWCLOSE_EVENT};
+		else
+			return 2;
 		break;
 	default:
 		break;
 	}
+	return 0;
+}
+
+int
+ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
+	int ret = 0;
+	while ((ret = next_event_base(renderdata, lang_index, event)) == 2) {
+		/* NOP */
+	}
+	return ret;
 }
diff --git a/src/eventdefs.h b/src/eventdefs.h
@@ -1,10 +1,23 @@
 #ifndef LTK_EVENTDEFS_H
 #define LTK_EVENTDEFS_H
 
+/* FIXME: add to config */
+#define DOUBLECLICK_TIME 250
+/* square of distance to make calculation simpler */
+#define DOUBLECLICK_DISTSQ 25
+/* FIXME: reduce amount of scroll/motion events */
+
 typedef enum {
 	LTK_UNKNOWN_EVENT, /* FIXME: a bit weird */
 	LTK_BUTTONPRESS_EVENT,
 	LTK_BUTTONRELEASE_EVENT,
+	/* double-click/release */
+	LTK_2BUTTONPRESS_EVENT,
+	LTK_2BUTTONRELEASE_EVENT,
+	/* triple-click/release */
+	LTK_3BUTTONPRESS_EVENT,
+	LTK_3BUTTONRELEASE_EVENT,
+	LTK_SCROLL_EVENT,
 	LTK_MOTION_EVENT,
 	LTK_KEYPRESS_EVENT,
 	LTK_KEYRELEASE_EVENT,
@@ -20,11 +33,6 @@ typedef enum {
 	LTK_BUTTONL,
 	LTK_BUTTONM,
 	LTK_BUTTONR,
-	/* FIXME: dedicated scroll event */
-	LTK_BUTTON4,
-	LTK_BUTTON5,
-	LTK_BUTTON6,
-	LTK_BUTTON7
 } ltk_button_type;
 
 /* FIXME: just steal the definitions from X when using Xlib so no conversion is necessary? */
diff --git a/src/grid.c b/src/grid.c
@@ -72,6 +72,7 @@ static struct ltk_widget_vtable vtable = {
 	.child_size_change = <k_grid_child_size_change,
 	.remove_child = <k_grid_ungrid,
 	.mouse_press = NULL,
+	.mouse_scroll = NULL,
 	.mouse_release = NULL,
 	.motion_notify = NULL,
 	.get_child_at_pos = <k_grid_get_child_at_pos,
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -450,10 +450,8 @@ ltk_mainloop(ltk_window *window) {
 		/* value of tv doesn't really matter anymore here because the
 		   necessary framerate-limiting delay is already done */
 		wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv);
-		while (ltk_events_pending(window->renderdata)) {
-			ltk_next_event(window->renderdata, window->cur_kbd, &event);
+		while (!ltk_next_event(window->renderdata, window->cur_kbd, &event))
 			ltk_handle_event(window, &event);
-		}
 
 		if (rretval > 0 || (sock_write_available && wretval > 0)) {
 			if (FD_ISSET(sock_state.listenfd, &rfds)) {
@@ -1222,9 +1220,16 @@ ltk_handle_event(ltk_window *window, ltk_event *event) {
 		ltk_window_key_release_event(window, &event->key);
 		break;
 	case LTK_BUTTONPRESS_EVENT:
+	case LTK_2BUTTONPRESS_EVENT:
+	case LTK_3BUTTONPRESS_EVENT:
 		ltk_window_mouse_press_event(window, &event->button);
 		break;
+	case LTK_SCROLL_EVENT:
+		ltk_window_mouse_scroll_event(window, &event->scroll);
+		break;
 	case LTK_BUTTONRELEASE_EVENT:
+	case LTK_2BUTTONRELEASE_EVENT:
+	case LTK_3BUTTONRELEASE_EVENT:
 		ltk_window_mouse_release_event(window, &event->button);
 		break;
 	case LTK_MOTION_EVENT:
diff --git a/src/menu.c b/src/menu.c
@@ -16,6 +16,10 @@
 
 /* NOTE: The implementation of menus and menu entries is a collection of ugly hacks. */
 
+/* FIXME: parent is pressed when scroll arrows pressed */
+/* -> this is because the pressed handling checks if the widget is activatable, then goes to the parent,
+   but the child isn't geometrically in the parent here, so that's weird */
+
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdint.h>
@@ -94,7 +98,7 @@ static void ltk_menu_scroll_callback(void *data);
 static void stop_scrolling(ltk_menu *menu);
 static ltk_widget *ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y);
 static int set_scroll_timer(ltk_menu *menu, int x, int y);
-static int ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event);
+static int ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
 static void ltk_menu_hide(ltk_widget *self);
 static void popup_active_menu(ltk_menuentry *e);
 static void unpopup_active_entry(ltk_menuentry *e);
@@ -136,7 +140,8 @@ static ltk_widget *ltk_menuentry_get_child(ltk_widget *self);
 static struct ltk_widget_vtable vtable = {
 	.key_press = NULL,
 	.key_release = NULL,
-	.mouse_press = <k_menu_mouse_press,
+	.mouse_press = NULL,
+	.mouse_scroll = <k_menu_mouse_scroll,
 	.motion_notify = <k_menu_motion_notify,
 	.mouse_release = NULL,
 	.mouse_enter = <k_menu_mouse_enter,
@@ -646,7 +651,8 @@ ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
 /* FIXME: make sure timers are always destroyed when widget is destroyed */
 static int
 set_scroll_timer(ltk_menu *menu, int x, int y) {
-	if (!ltk_collide_rect(menu->widget.lrect, x, y))
+	/* this check probably isn't necessary, but whatever */
+	if (x < 0 || y < 0 || x >= menu->widget.lrect.w || y >= menu->widget.lrect.h)
 		return 0;
 	int t = 0, b = 0, l = 0,r = 0;
 	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
@@ -694,30 +700,20 @@ ltk_menuentry_release(ltk_widget *self) {
 }
 
 static int
-ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event) {
+ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
 	ltk_menu *menu = (ltk_menu *)self;
-	/* FIXME: configure scroll step */
 	ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
-	switch (event->button) {
-	case LTK_BUTTON4:
-		ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
-		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
-		break;
-	case LTK_BUTTON5:
-		ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
-		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
-		break;
-	case LTK_BUTTON6:
-		ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
-		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
-		break;
-	case LTK_BUTTON7:
-		ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
-		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
-		break;
-	default:
-		return 0;
-	}
+	/* FIXME: configure scroll step */
+	/* FIXME: fix the interface for ltk_menu_scroll */
+	if (event->dx > 0)
+		ltk_menu_scroll(menu, 0, 0, 0, 1, event->dx * 10);
+	else if (event->dx < 0)
+		ltk_menu_scroll(menu, 0, 0, 1, 0, -event->dx * 10);
+	if (event->dy > 0)
+		ltk_menu_scroll(menu, 1, 0, 0, 0, event->dy * 10);
+	else if (event->dy < 0)
+		ltk_menu_scroll(menu, 0, 1, 0, 0, -event->dy * 10);
+	ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 	return 1;
 }
 
diff --git a/src/proto_types.h b/src/proto_types.h
@@ -24,23 +24,34 @@
 
 /* P == protocol; W == widget */
 
-#define LTK_PEVENT_MOUSEPRESS    0
-#define LTK_PEVENT_MOUSERELEASE  1
-#define LTK_PEVENT_MOUSEMOTION   2
-#define LTK_PEVENT_KEYPRESS      3
-#define LTK_PEVENT_KEYRELEASE    4
-#define LTK_PEVENT_CONFIGURE     5
-#define LTK_PEVENT_STATECHANGE   6
-
-#define LTK_PEVENTMASK_NONE          (UINT32_C(0))
-#define LTK_PEVENTMASK_MOUSEPRESS    (UINT32_C(1) << LTK_PEVENT_MOUSEPRESS)
-#define LTK_PEVENTMASK_MOUSERELEASE  (UINT32_C(1) << LTK_PEVENT_MOUSERELEASE)
-#define LTK_PEVENTMASK_MOUSEMOTION   (UINT32_C(1) << LTK_PEVENT_MOUSEMOTION)
-#define LTK_PEVENTMASK_KEYPRESS      (UINT32_C(1) << LTK_PEVENT_KEYPRESS)
-#define LTK_PEVENTMASK_KEYRELEASE    (UINT32_C(1) << LTK_PEVENT_KEYRELEASE)
-#define LTK_PEVENTMASK_CONFIGURE     (UINT32_C(1) << LTK_PEVENT_CONFIGURE)
-#define LTK_PEVENTMASK_EXPOSE        (UINT32_C(1) << LTK_PEVENT_EXPOSE)
-#define LTK_PEVENTMASK_STATECHANGE   (UINT32_C(1) << LTK_PEVENT_STATECHANGE)
+#define LTK_PEVENT_MOUSEPRESS     0
+#define LTK_PEVENT_2MOUSEPRESS    1
+#define LTK_PEVENT_3MOUSEPRESS    2
+#define LTK_PEVENT_MOUSERELEASE   3
+#define LTK_PEVENT_2MOUSERELEASE  4
+#define LTK_PEVENT_3MOUSERELEASE  5
+#define LTK_PEVENT_MOUSEMOTION    6
+#define LTK_PEVENT_MOUSESCROLL    7
+#define LTK_PEVENT_KEYPRESS       8
+#define LTK_PEVENT_KEYRELEASE     9
+#define LTK_PEVENT_CONFIGURE      10
+#define LTK_PEVENT_STATECHANGE    11
+
+/* FIXME: standardize names - internally, buttonpress is used, here it's mousepress... */
+#define LTK_PEVENTMASK_NONE           (UINT32_C(0))
+#define LTK_PEVENTMASK_MOUSEPRESS     (UINT32_C(1) << LTK_PEVENT_MOUSEPRESS)
+#define LTK_PEVENTMASK_2MOUSEPRESS    (UINT32_C(1) << LTK_PEVENT_2MOUSEPRESS)
+#define LTK_PEVENTMASK_3MOUSEPRESS    (UINT32_C(1) << LTK_PEVENT_3MOUSEPRESS)
+#define LTK_PEVENTMASK_MOUSERELEASE   (UINT32_C(1) << LTK_PEVENT_MOUSERELEASE)
+#define LTK_PEVENTMASK_2MOUSERELEASE  (UINT32_C(1) << LTK_PEVENT_2MOUSERELEASE)
+#define LTK_PEVENTMASK_3MOUSERELEASE  (UINT32_C(1) << LTK_PEVENT_3MOUSERELEASE)
+#define LTK_PEVENTMASK_MOUSEMOTION    (UINT32_C(1) << LTK_PEVENT_MOUSEMOTION)
+#define LTK_PEVENTMASK_KEYPRESS       (UINT32_C(1) << LTK_PEVENT_KEYPRESS)
+#define LTK_PEVENTMASK_KEYRELEASE     (UINT32_C(1) << LTK_PEVENT_KEYRELEASE)
+#define LTK_PEVENTMASK_CONFIGURE      (UINT32_C(1) << LTK_PEVENT_CONFIGURE)
+#define LTK_PEVENTMASK_EXPOSE         (UINT32_C(1) << LTK_PEVENT_EXPOSE)
+#define LTK_PEVENTMASK_STATECHANGE    (UINT32_C(1) << LTK_PEVENT_STATECHANGE)
+#define LTK_PEVENTMASK_MOUSESCROLL    (UINT32_C(1) << LTK_PEVENT_MOUSESCROLL)
 
 #define LTK_PWEVENT_MENUENTRY_PRESS     0
 #define LTK_PWEVENTMASK_MENUENTRY_NONE  (UINT32_C(0))
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -165,7 +165,7 @@ static int
 ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	ltk_scrollbar *sc = (ltk_scrollbar *)self;
 	int max_pos;
-	if (event->button != LTK_BUTTONL)
+	if (event->button != LTK_BUTTONL || event->type != LTK_BUTTONPRESS_EVENT)
 		return 0;
 	int ex = event->x, ey = event->y;
 	ltk_rect handle_rect = handle_get_rect(sc);
diff --git a/src/widget.c b/src/widget.c
@@ -469,14 +469,47 @@ is_parent(ltk_widget *parent, ltk_widget *child) {
 
 /* FIXME: fix global and local coordinates! */
 static int
-queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
+queue_mouse_event(ltk_widget *widget, ltk_event_type type, int x, int y) {
+	uint32_t mask;
+	char *typename;
+	switch (type) {
+	case LTK_MOTION_EVENT:
+		mask = LTK_PEVENTMASK_MOUSEMOTION;
+		typename = "mousemotion";
+		break;
+	case LTK_2BUTTONPRESS_EVENT:
+		mask = LTK_PEVENTMASK_2MOUSEPRESS;
+		typename = "2mousepress";
+		break;
+	case LTK_3BUTTONPRESS_EVENT:
+		mask = LTK_PEVENTMASK_3MOUSEPRESS;
+		typename = "3mousepress";
+		break;
+	case LTK_BUTTONRELEASE_EVENT:
+		mask = LTK_PEVENTMASK_MOUSERELEASE;
+		typename = "mouserelease";
+		break;
+	case LTK_2BUTTONRELEASE_EVENT:
+		mask = LTK_PEVENTMASK_2MOUSERELEASE;
+		typename = "2mouserelease";
+		break;
+	case LTK_3BUTTONRELEASE_EVENT:
+		mask = LTK_PEVENTMASK_3MOUSERELEASE;
+		typename = "3mouserelease";
+		break;
+	case LTK_BUTTONPRESS_EVENT:
+	default:
+		mask = LTK_PEVENTMASK_MOUSEPRESS;
+		typename = "mousepress";
+		break;
+	}
 	int lock_client = -1;
 	for (size_t i = 0; i < widget->masks_num; i++) {
 		if (widget->event_masks[i].lmask & mask) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "eventl %s widget %s %d %d %d %d\n",
-			    widget->id, type, x, y, x, y
+			    widget->id, typename, x, y, x, y
 			    /* x - widget->rect.x, y - widget->rect.y */
 			);
 			lock_client = widget->event_masks[i].client;
@@ -484,7 +517,37 @@ queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "event %s widget %s %d %d %d %d\n",
-			    widget->id, type, x, y, x, y
+			    widget->id, typename, x, y, x, y
+			    /* x - widget->rect.x, y - widget->rect.y */
+			);
+		}
+	}
+	if (lock_client >= 0) {
+		if (ltk_handle_lock_client(widget->window, lock_client))
+			return 1;
+	}
+	return 0;
+}
+
+/* FIXME: global/local coords (like above) */
+static int
+queue_scroll_event(ltk_widget *widget, int x, int y, int dx, int dy) {
+	uint32_t mask = LTK_PEVENTMASK_MOUSESCROLL;
+	int lock_client = -1;
+	for (size_t i = 0; i < widget->masks_num; i++) {
+		if (widget->event_masks[i].lmask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "eventl %s widget %s %d %d %d %d %d %d\n",
+			    widget->id, "mousescroll", x, y, x, y, dx, dy
+			    /* x - widget->rect.x, y - widget->rect.y */
+			);
+			lock_client = widget->event_masks[i].client;
+		} else if (widget->event_masks[i].mask & mask) {
+			ltk_queue_sock_write_fmt(
+			    widget->event_masks[i].client,
+			    "event %s widget %s %d %d %d %d %d %d\n",
+			    widget->id, "mousescroll", x, y, x, y, dx, dy
 			    /* x - widget->rect.x, y - widget->rect.y */
 			);
 		}
@@ -1011,6 +1074,8 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 	if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
 		ltk_window_unregister_all_popups(window);
 
+	/* FIXME: popups don't always have their children geometrically contained within parents,
+	   so this won't work properly in all cases */
 	int first = 1;
 	while (cur_widget) {
 		int handled = 0;
@@ -1020,13 +1085,13 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 		if (cur_widget->state != LTK_DISABLED) {
 			/* FIXME: figure out whether this makes sense - currently, all widgets (unless disabled)
 			   get mouse press, but they are only set to pressed if they are activatable */
-			if (queue_mouse_event(cur_widget, "mousepress", LTK_PEVENTMASK_MOUSEPRESS, event->x, event->y))
+			if (queue_mouse_event(cur_widget, event->type, event->x, event->y))
 				handled = 1;
 			else if (cur_widget->vtable->mouse_press)
 				handled = cur_widget->vtable->mouse_press(cur_widget, event);
 			/* set first non-disabled widget to pressed widget */
 			/* FIXME: use config values for all_activatable */
-			if (first && event->button == LTK_BUTTONL && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+			if (first && event->button == LTK_BUTTONL && event->type == LTK_BUTTONPRESS_EVENT && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
 				ltk_window_set_pressed_widget(window, cur_widget, 0);
 				first = 0;
 			}
@@ -1039,6 +1104,35 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 }
 
 void
+ltk_window_mouse_scroll_event(ltk_window *window, ltk_scroll_event *event) {
+	/* FIXME: should it first be sent to pressed widget? */
+	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	if (!widget)
+		widget = window->root_widget;
+	if (!widget)
+		return;
+	int orig_x = event->x, orig_y = event->y;
+	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
+	/* FIXME: same issue with popups like in mouse_press above */
+	while (cur_widget) {
+		int handled = 0;
+		ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+		event->x = local.x;
+		event->y = local.y;
+		if (cur_widget->state != LTK_DISABLED) {
+			if (queue_scroll_event(cur_widget, event->x, event->y, event->dx, event->dy))
+				handled = 1;
+			else if (cur_widget->vtable->mouse_scroll)
+				handled = cur_widget->vtable->mouse_scroll(cur_widget, event);
+		}
+		if (!handled)
+			cur_widget = cur_widget->parent;
+		else
+			break;
+	}
+}
+
+void
 ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
 	ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = x, .y = y};
 	ltk_window_motion_notify_event(window, &e);
@@ -1048,17 +1142,18 @@ void
 ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event) {
 	ltk_widget *widget = window->pressed_widget;
 	int orig_x = event->x, orig_y = event->y;
+	/* FIXME: why does this only take pressed widget and popups into account? */
 	if (!widget) {
 		widget = get_hover_popup(window, event->x, event->y);
 		widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
 	}
 	/* FIXME: loop up to top of hierarchy if not handled */
-	if (widget && queue_mouse_event(widget, "mouserelease", LTK_PEVENTMASK_MOUSERELEASE, event->x, event->y)) {
+	if (widget && queue_mouse_event(widget, event->type, event->x, event->y)) {
 		/* NOP */
 	} else if (widget && widget->vtable->mouse_release) {
 		widget->vtable->mouse_release(widget, event);
 	}
-	if (event->button == LTK_BUTTONL) {
+	if (event->button == LTK_BUTTONL && event->type == LTK_BUTTONRELEASE_EVENT) {
 		int release = 0;
 		if (window->pressed_widget) {
 			ltk_rect prect = window->pressed_widget->lrect;
@@ -1104,7 +1199,7 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 		event->x = local.x;
 		event->y = local.y;
 		if (cur_widget->state != LTK_DISABLED) {
-			if (queue_mouse_event(cur_widget, "mousemotion", LTK_PEVENTMASK_MOUSEMOTION, event->x, event->y))
+			if (queue_mouse_event(cur_widget, LTK_MOTION_EVENT, event->x, event->y))
 				handled = 1;
 			else if (cur_widget->vtable->motion_notify)
 				handled = cur_widget->vtable->motion_notify(cur_widget, event);
diff --git a/src/widget.h b/src/widget.h
@@ -119,8 +119,10 @@ struct ltk_widget {
 struct ltk_widget_vtable {
 	int (*key_press)(struct ltk_widget *, ltk_key_event *);
 	int (*key_release)(struct ltk_widget *, ltk_key_event *);
+	/* press/release also receive double/triple-click/release */
 	int (*mouse_press)(struct ltk_widget *, ltk_button_event *);
 	int (*mouse_release)(struct ltk_widget *, ltk_button_event *);
+	int (*mouse_scroll)(struct ltk_widget *, ltk_scroll_event *);
 	int (*motion_notify)(struct ltk_widget *, ltk_motion_event *);
 	int (*mouse_leave)(struct ltk_widget *, ltk_motion_event *);
 	int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
@@ -168,6 +170,7 @@ void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
 void ltk_window_key_press_event(ltk_window *window, ltk_key_event *event);
 void ltk_window_key_release_event(ltk_window *window, ltk_key_event *event);
 void ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event);
+void ltk_window_mouse_scroll_event(ltk_window *window, ltk_scroll_event *event);
 void ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event);
 void ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event);
 void ltk_window_fake_motion_event(ltk_window *window, int x, int y);