commit bf6d516dc0a01db18fb93ddb4f153a454b36700b
parent 994e235d9939788238dd2c66cdefb4cc4ade1bce
Author: lumidify <nobody@lumidify.org>
Date:   Thu, 16 Jun 2022 10:47:44 +0200
Clean up mouse event handling a bit; bring back hover state
Of course, everything is still hopelessly broken.
Diffstat:
| M | Makefile |  |  | 11 | ++++++----- | 
| M | src/box.c |  |  | 71 | +++++++++++++++++++---------------------------------------------------- | 
| M | src/button.c |  |  | 19 | ++++++++++++++----- | 
| M | src/grid.c |  |  | 59 | ++++++++++------------------------------------------------- | 
| M | src/label.c |  |  | 1 | + | 
| M | src/ltk.h |  |  | 2 | ++ | 
| M | src/ltkd.c |  |  | 73 | ++++++++++++++++++++++++++++++++++++------------------------------------- | 
| M | src/menu.c |  |  | 52 | +++++++++++++++++++++++++++++++++------------------- | 
| M | src/scrollbar.c |  |  | 32 | ++++++++++++++++---------------- | 
| M | src/widget.c |  |  | 128 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------- | 
| M | src/widget.h |  |  | 20 | ++++++++++++-------- | 
11 files changed, 241 insertions(+), 227 deletions(-)
diff --git a/Makefile b/Makefile
@@ -7,14 +7,15 @@ VERSION = -999-prealpha0
 # Note: The stb backend should not be used with untrusted font files.
 # FIXME: Using DEBUG here doesn't work because it somehow
 # interferes with a predefined macro, at least on OpenBSD.
-DEV = 0
+DEV = 1
+SANITIZE = 0
 USE_PANGO = 0
 
 # Note: this macro magic for debugging and pango rendering seems ugly; it should probably be changed
 
 # debug
-DEV_CFLAGS_1 = -fsanitize=address -g -Wall -Wextra -pedantic
-DEV_LDFLAGS_1 = -fsanitize=address
+DEV_CFLAGS_1 = -g -Wall -Wextra -pedantic
+SANITIZE_FLAGS_1 = -fsanitize=address
 # don't include default flags when debugging so possible
 # optimization flags don't interfere with it
 DEV_CFLAGS_0 = $(CFLAGS)
@@ -29,8 +30,8 @@ EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft`
 EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft`
 
 EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
-EXTRA_CFLAGS = $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
-EXTRA_LDFLAGS = $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
+EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
+EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
 
 LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
 LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext`
diff --git a/src/box.c b/src/box.c
@@ -40,10 +40,8 @@ static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, uns
 static int ltk_box_remove(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **errstr);
 /* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow, char **errstr); */
 static void ltk_box_scroll(ltk_widget *self);
-static int ltk_box_mouse_event(ltk_box *box, int x, int y, ltk_event *event, void (*handler)(ltk_widget *, ltk_event *));
-static int ltk_box_mouse_press(ltk_widget *self, ltk_event *event);
-static int ltk_box_mouse_release(ltk_widget *self, ltk_event *event);
-static int ltk_box_motion_notify(ltk_widget *self, ltk_event *event);
+static int ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event);
+static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
 
 static struct ltk_widget_vtable vtable = {
 	.change_state = NULL,
@@ -56,8 +54,9 @@ static struct ltk_widget_vtable vtable = {
 	.key_press = NULL,
 	.key_release = NULL,
 	.mouse_press = <k_box_mouse_press,
-	.mouse_release = <k_box_mouse_release,
-	.motion_notify = <k_box_motion_notify,
+	.mouse_release = NULL,
+	.motion_notify = NULL,
+	.get_child_at_pos = <k_box_get_child_at_pos,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
 	.needs_redraw = 0,
@@ -321,64 +320,32 @@ ltk_box_scroll(ltk_widget *self) {
 	ltk_window_invalidate_rect(box->widget.window, box->widget.rect);
 }
 
-static int
-ltk_box_mouse_event(ltk_box *box, int x, int y, ltk_event *event, void (*handler)(ltk_widget *, ltk_event *)) {
-	ltk_widget *widget;
-
-	if (ltk_collide_rect(box->sc->widget.rect, x, y)) {
-		handler((ltk_widget *)box->sc, event);
-		return 0;
-	}
-
-	/* FIXME: check only the currently visible items */
+static ltk_widget *
+ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
+	ltk_box *box = (ltk_box *)self;
+	if (ltk_collide_rect(box->sc->widget.rect, x, y))
+		return (ltk_widget *)box->sc;
 	for (size_t i = 0; i < box->num_widgets; i++) {
-		widget = box->widgets[i];
-		if (ltk_collide_rect(widget->rect, x, y)) {
-			handler(widget, event);
-			return 0;
-		}
+		if (ltk_collide_rect(box->widgets[i]->rect, x, y))
+			return box->widgets[i];
 	}
-	return 0;
+	return NULL;
 }
 
 static int
-ltk_box_mouse_press(ltk_widget *self, ltk_event *event) {
+ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	ltk_box *box = (ltk_box *)self;
 	/* FIXME: combine multiple events into one for efficiency */
-	/* FIXME: fix this whole state handling */
-	if (event->button.button == LTK_BUTTON4 || event->button.button == LTK_BUTTON5) {
-		ltk_widget *widget;
-		int default_handler = 1;
-		for (size_t i = 0; i < box->num_widgets; i++) {
-			widget = box->widgets[i];
-			if (ltk_collide_rect(widget->rect, event->button.x, event->button.y)) {
-				if (widget->vtable->mouse_press)
-					default_handler = widget->vtable->mouse_press(widget, event);
-			}
-		}
+	if (event->button == LTK_BUTTON4 || event->button == LTK_BUTTON5) {
 		/* FIXME: configure scrollstep */
-		if (default_handler) {
-			int delta = event->button.button == LTK_BUTTON4 ? -15 : 15;
-			ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
-		}
-		return 0;
+		int delta = event->button == LTK_BUTTON4 ? -15 : 15;
+		ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
+		return 1;
 	} else {
-		return ltk_box_mouse_event(box, event->button.x, event->button.y, event, <k_widget_mouse_press_event);
+		return 0;
 	}
 }
 
-static int
-ltk_box_mouse_release(ltk_widget *self, ltk_event *event) {
-	ltk_box *box = (ltk_box *)self;
-	return ltk_box_mouse_event(box, event->button.x, event->button.y, event, <k_widget_mouse_release_event);
-}
-
-static int
-ltk_box_motion_notify(ltk_widget *self, ltk_event *event) {
-	ltk_box *box = (ltk_box *)self;
-	return ltk_box_mouse_event(box, event->motion.x, event->motion.y, event, <k_widget_motion_notify_event);
-}
-
 /* box <box id> add <widget id> [sticky] */
 static int
 ltk_box_cmd_add(
diff --git a/src/button.c b/src/button.c
@@ -37,7 +37,7 @@
 #define MAX_BUTTON_PADDING 500
 
 static void ltk_button_draw(ltk_widget *self, ltk_rect clip);
-static int ltk_button_mouse_release(ltk_widget *self, ltk_event *event);
+static int ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event);
 static ltk_button *ltk_button_create(ltk_window *window,
     const char *id, char *text);
 static void ltk_button_destroy(ltk_widget *self, int shallow);
@@ -53,6 +53,7 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
 	.change_state = <k_button_change_state,
+	.get_child_at_pos = NULL,
 	.resize = NULL,
 	.hide = NULL,
 	.draw = <k_button_draw,
@@ -75,6 +76,9 @@ static struct {
 	ltk_color border_pressed;
 	ltk_color fill_pressed;
 
+	ltk_color border_hover;
+	ltk_color fill_hover;
+
 	ltk_color border_active;
 	ltk_color fill_active;
 
@@ -84,12 +88,14 @@ static struct {
 
 static ltk_theme_parseinfo parseinfo[] = {
 	{"border", THEME_COLOR, {.color = &theme.border}, {.color = "#339999"}, 0, 0, 0},
+	{"border-hover", THEME_COLOR, {.color = &theme.border_hover}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"border-active", THEME_COLOR, {.color = &theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"border-disabled", THEME_COLOR, {.color = &theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"border-pressed", THEME_COLOR, {.color = &theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
 	{"border-width", THEME_INT, {.i = &theme.border_width}, {.i = 2}, 0, MAX_BUTTON_BORDER_WIDTH, 0},
 	{"fill", THEME_COLOR, {.color = &theme.fill}, {.color = "#113355"}, 0, 0, 0},
-	{"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
+	{"fill-hover", THEME_COLOR, {.color = &theme.fill_hover}, {.color = "#738194"}, 0, 0, 0},
+	{"fill-active", THEME_COLOR, {.color = &theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
 	{"fill-disabled", THEME_COLOR, {.color = &theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
 	{"fill-pressed", THEME_COLOR, {.color = &theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
 	{"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_BUTTON_PADDING, 0},
@@ -133,6 +139,10 @@ ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
 		border = &theme.border;
 		fill = &theme.fill;
 		break;
+	case LTK_HOVER:
+		border = &theme.border_hover;
+		fill = &theme.fill_hover;
+		break;
 	case LTK_PRESSED:
 		border = &theme.border_pressed;
 		fill = &theme.fill_pressed;
@@ -167,11 +177,10 @@ ltk_button_change_state(ltk_widget *self) {
 	self->dirty = 1;
 }
 
-/* FIXME: only when pressed button was actually this one */
 static int
-ltk_button_mouse_release(ltk_widget *self, ltk_event *event) {
+ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event) {
 	ltk_button *button = (ltk_button *)self;
-	if (event->button.button == LTK_BUTTONL) {
+	if (self->state == LTK_PRESSED && event->button == LTK_BUTTONL) {
 		ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
 		return 1;
 	}
diff --git a/src/grid.c b/src/grid.c
@@ -50,9 +50,7 @@ static int ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
 static int ltk_grid_ungrid(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **errstr);
 static int ltk_grid_find_nearest_column(ltk_grid *grid, int x);
 static int ltk_grid_find_nearest_row(ltk_grid *grid, int y);
-static int ltk_grid_mouse_press(ltk_widget *self, ltk_event *event);
-static int ltk_grid_mouse_release(ltk_widget *self, ltk_event *event);
-static int ltk_grid_motion_notify(ltk_widget *self, ltk_event *event);
+static ltk_widget *ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y);
 
 static struct ltk_widget_vtable vtable = {
 	.draw = <k_grid_draw,
@@ -62,9 +60,10 @@ static struct ltk_widget_vtable vtable = {
 	.change_state = NULL,
 	.child_size_change = <k_grid_child_size_change,
 	.remove_child = <k_grid_ungrid,
-	.mouse_press = <k_grid_mouse_press,
-	.mouse_release = <k_grid_mouse_release,
-	.motion_notify = <k_grid_motion_notify,
+	.mouse_press = NULL,
+	.mouse_release = NULL,
+	.motion_notify = NULL,
+	.get_child_at_pos = <k_grid_get_child_at_pos,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
 	.key_press = NULL,
@@ -383,55 +382,17 @@ ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
 	return -1;
 }
 
-static int
-ltk_grid_mouse_press(ltk_widget *self, ltk_event *event) {
-	ltk_grid *grid = (ltk_grid *)self;
-	int x = event->button.x;
-	int y = event->button.y;
-	int row = ltk_grid_find_nearest_row(grid, y);
-	int column = ltk_grid_find_nearest_column(grid, x);
-	if (row == -1 || column == -1)
-		return 0;
-	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
-	if (ptr && ltk_collide_rect(ptr->rect, x, y)) {
-		ltk_widget_mouse_press_event(ptr, event);
-		return 0;
-	}
-	return 0;
-}
-
-static int
-ltk_grid_mouse_release(ltk_widget *self, ltk_event *event) {
-	ltk_grid *grid = (ltk_grid *)self;
-	int x = event->button.x;
-	int y = event->button.y;
-	int row = ltk_grid_find_nearest_row(grid, y);
-	int column = ltk_grid_find_nearest_column(grid, x);
-	if (row == -1 || column == -1)
-		return 0;
-	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
-	if (ptr && ltk_collide_rect(ptr->rect, x, y)) {
-		ltk_widget_mouse_release_event(ptr, event);
-		return 0;
-	}
-	return 0;
-}
-
-static int
-ltk_grid_motion_notify(ltk_widget *self, ltk_event *event) {
+static ltk_widget *
+ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y) {
 	ltk_grid *grid = (ltk_grid *)self;
-	int x = event->motion.x;
-	int y = event->motion.y;
 	int row = ltk_grid_find_nearest_row(grid, y);
 	int column = ltk_grid_find_nearest_column(grid, x);
 	if (row == -1 || column == -1)
 		return 0;
 	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
-	if (ptr && ltk_collide_rect(ptr->rect, x, y)) {
-		ltk_widget_motion_notify_event(ptr, event);
-		return 0;
-	}
-	return 0;
+	if (ptr && ltk_collide_rect(ptr->rect, x, y))
+		return ptr;
+	return NULL;
 }
 
 /* grid <grid id> add <widget id> <row> <column> <row_span> <column_span> [sticky] */
diff --git a/src/label.c b/src/label.c
@@ -49,6 +49,7 @@ static struct ltk_widget_vtable vtable = {
 	.change_state = NULL,
 	.child_size_change = NULL,
 	.remove_child = NULL,
+	.get_child_at_pos = NULL,
 	.key_press = NULL,
 	.key_release = NULL,
 	.mouse_press = NULL,
diff --git a/src/ltk.h b/src/ltk.h
@@ -59,6 +59,7 @@ struct ltk_window {
 	ltk_text_context *text_context;
 	ltk_surface *surface;
 	ltk_widget *root_widget;
+	ltk_widget *hover_widget;
 	ltk_widget *active_widget;
 	ltk_widget *pressed_widget;
 	void (*other_event) (struct ltk_window *, ltk_event *event);
@@ -89,6 +90,7 @@ struct ltk_window_theme {
 
 void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
 void ltk_queue_event(ltk_window *window, ltk_userevent_type type, const char *id, const char *data);
+void ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event);
 void ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget);
 void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget);
 void ltk_quit(ltk_window *window);
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -674,10 +674,10 @@ void
 ltk_window_unregister_all_popups(ltk_window *window) {
 	window->popups_locked = 1;
 	for (size_t i = 0; i < window->popups_num; i++) {
+		window->popups[i]->hidden = 1;
 		if (window->popups[i]->vtable->hide) {
 			window->popups[i]->vtable->hide(window->popups[i]);
 		}
-		window->popups[i]->hidden = 1;
 	}
 	window->popups_num = 0;
 	/* somewhat arbitrary, but should be enough for most cases */
@@ -784,6 +784,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	renderer_set_window_properties(window->renderdata, &window->theme->bg, &window->theme->fg, window->theme->border_width);
 
 	window->root_widget = NULL;
+	window->hover_widget = NULL;
 	window->active_widget = NULL;
 	window->pressed_widget = NULL;
 
@@ -818,6 +819,31 @@ ltk_destroy_window(ltk_window *window) {
 	ltk_free(window);
 }
 
+/* FIXME: some widgets should not be allowed to be active or pressed (e.g. containers) */
+void
+ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event) {
+	ltk_widget *old = window->hover_widget;
+	if (old == widget)
+		return;
+	if (old) {
+		/* set widget to active again if it is actually active */
+		if (old == window->active_widget)
+			old->state = LTK_ACTIVE;
+		else
+			old->state = LTK_NORMAL;
+		ltk_widget_change_state(old);
+		if (old->vtable->mouse_leave)
+			old->vtable->mouse_leave(old, event);
+	}
+	window->hover_widget = widget;
+	if (widget) {
+		widget->state = LTK_HOVER;
+		ltk_widget_change_state(widget);
+		if (widget->vtable->mouse_enter)
+			widget->vtable->mouse_enter(widget, event);
+	}
+}
+
 void
 ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
 	if (window->active_widget == widget)
@@ -833,19 +859,17 @@ ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
 	}
 }
 
-/* FIXME: Should pressed widget also be set as active widget? */
 void
 ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
 	if (window->pressed_widget == widget)
 		return;
-	if (window->active_widget && window->active_widget != widget) {
-		window->active_widget->state = LTK_NORMAL;
-		ltk_widget_change_state(window->active_widget);
-	}
-	if (window->pressed_widget) {
-		window->pressed_widget->state = LTK_ACTIVE;
-		ltk_widget_change_state(window->pressed_widget);
+	if (window->hover_widget && window->hover_widget != widget) {
+		window->hover_widget->state = LTK_NORMAL;
+		ltk_widget_change_state(window->hover_widget);
+		window->hover_widget = NULL;
 	}
+	if (window->pressed_widget)
+		ltk_window_set_active_widget(window, window->pressed_widget);
 	window->pressed_widget = widget;
 	if (widget) {
 		widget->state = LTK_PRESSED;
@@ -853,46 +877,21 @@ ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
 	}
 }
 
-static ltk_widget *
-get_hover_popup(ltk_window *window, int x, int y) {
-	for (size_t i = window->popups_num; i-- > 0;) {
-		if (ltk_collide_rect(window->popups[i]->rect, x, y))
-			return window->popups[i];
-	}
-	return NULL;
-}
-
 static void
 ltk_handle_event(ltk_window *window, ltk_event *event) {
-	ltk_widget *hover_popup;
-	ltk_widget *root_widget = window->root_widget;
 	switch (event->type) {
 	case LTK_KEYPRESS_EVENT:
 		break;
 	case LTK_KEYRELEASE_EVENT:
 		break;
 	case LTK_BUTTONPRESS_EVENT:
-		hover_popup = get_hover_popup(window, event->button.x, event->button.y);
-		if (hover_popup) {
-			ltk_widget_mouse_press_event(hover_popup, event);
-		} else if (root_widget) {
-			ltk_window_unregister_all_popups(window);
-			ltk_widget_mouse_press_event(root_widget, event);
-		}
+		ltk_window_mouse_press_event(window, &event->button);
 		break;
 	case LTK_BUTTONRELEASE_EVENT:
-		hover_popup = get_hover_popup(window, event->button.x, event->button.y);
-		if (hover_popup)
-			ltk_widget_mouse_release_event(hover_popup, event);
-		else if (root_widget)
-			ltk_widget_mouse_release_event(root_widget, event);
+		ltk_window_mouse_release_event(window, &event->button);
 		break;
 	case LTK_MOTION_EVENT:
-		hover_popup = get_hover_popup(window, event->motion.x, event->motion.y);
-		if (hover_popup)
-			ltk_widget_motion_notify_event(hover_popup, event);
-		else if (root_widget)
-			ltk_widget_motion_notify_event(root_widget, event);
+		ltk_window_motion_notify_event(window, &event->motion);
 		break;
 	default:
 		if (window->other_event)
diff --git a/src/menu.c b/src/menu.c
@@ -86,15 +86,15 @@ static void ltk_menu_scroll_callback(void *data);
 static void stop_scrolling(ltk_menu *menu);
 static size_t get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret);
 static int set_scroll_timer(ltk_menu *menu, int x, int y);
-static int ltk_menu_mouse_release(ltk_widget *self, ltk_event *event);
-static int ltk_menu_mouse_press(ltk_widget *self, ltk_event *event);
+static int ltk_menu_mouse_release(ltk_widget *self, ltk_button_event *event);
+static int ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event);
 static void ltk_menu_hide(ltk_widget *self);
 static void popup_active_menu(ltk_menu *menu, ltk_rect r);
 static void unpopup_active_entry(ltk_menu *menu);
 static void handle_hover(ltk_menu *menu, int x, int y);
-static int ltk_menu_motion_notify(ltk_widget *self, ltk_event *event);
-static int ltk_menu_mouse_enter(ltk_widget *self, ltk_event *event);
-static int ltk_menu_mouse_leave(ltk_widget *self, ltk_event *event);
+static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event);
+static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event);
+static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event);
 static ltk_menu *ltk_menu_create(ltk_window *window, const char *id, int is_submenu);
 static ltk_menuentry *insert_entry(ltk_menu *menu, size_t idx);
 static void recalc_menu_size(ltk_menu *menu);
@@ -126,6 +126,7 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_release = <k_menu_mouse_release,
 	.mouse_enter = <k_menu_mouse_enter,
 	.mouse_leave = <k_menu_mouse_leave,
+	.get_child_at_pos = NULL,
 	.resize = <k_menu_resize,
 	.change_state = <k_menu_change_state,
 	.hide = <k_menu_hide,
@@ -250,6 +251,14 @@ ltk_menu_change_state(ltk_widget *self) {
 		self->dirty = 1;
 		ltk_window_invalidate_rect(self->window, self->rect);
 	}
+	if (self->state == LTK_NORMAL && menu->active_entry < menu->num_entries) {
+		ltk_menuentry *e = &menu->entries[menu->active_entry];
+		if (!e->submenu || e->submenu->widget.hidden) {
+			menu->active_entry = SIZE_MAX;
+			self->dirty = 1;
+			ltk_window_invalidate_rect(self->window, self->rect);
+		}
+	}
 }
 
 static void
@@ -602,9 +611,9 @@ set_scroll_timer(ltk_menu *menu, int x, int y) {
 }
 
 static int
-ltk_menu_mouse_release(ltk_widget *self, ltk_event *event) {
+ltk_menu_mouse_release(ltk_widget *self, ltk_button_event *event) {
 	ltk_menu *menu = (ltk_menu *)self;
-	size_t idx = get_entry_at_point(menu, event->button.x, event->button.y, NULL);
+	size_t idx = get_entry_at_point(menu, event->x, event->y, NULL);
 	if (idx < menu->num_entries && idx == menu->pressed_entry) {
 		ltk_window_unregister_all_popups(self->window);
 		/* FIXME: give menu id and entry id */
@@ -620,13 +629,13 @@ ltk_menu_mouse_release(ltk_widget *self, ltk_event *event) {
 }
 
 static int
-ltk_menu_mouse_press(ltk_widget *self, ltk_event *event) {
+ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	ltk_menu *menu = (ltk_menu *)self;
 	size_t idx;
 	/* FIXME: configure scroll step */
-	switch (event->button.button) {
+	switch (event->button) {
 	case LTK_BUTTONL:
-		idx = get_entry_at_point(menu, event->button.x, event->button.y, NULL);
+		idx = get_entry_at_point(menu, event->x, event->y, NULL);
 		if (idx < menu->num_entries) {
 			menu->pressed_entry = idx;
 			self->dirty = 1;
@@ -634,19 +643,19 @@ ltk_menu_mouse_press(ltk_widget *self, ltk_event *event) {
 		break;
 	case LTK_BUTTON4:
 		ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
-		handle_hover(menu, event->button.x, event->button.y);
+		handle_hover(menu, event->x, event->y);
 		break;
 	case LTK_BUTTON5:
 		ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
-		handle_hover(menu, event->button.x, event->button.y);
+		handle_hover(menu, event->x, event->y);
 		break;
 	case LTK_BUTTON6:
 		ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
-		handle_hover(menu, event->button.x, event->button.y);
+		handle_hover(menu, event->x, event->y);
 		break;
 	case LTK_BUTTON7:
 		ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
-		handle_hover(menu, event->button.x, event->button.y);
+		handle_hover(menu, event->x, event->y);
 		break;
 	default:
 		break;
@@ -664,6 +673,11 @@ ltk_menu_hide(ltk_widget *self) {
 	menu->scroll_left_hover = menu->scroll_right_hover = 0;
 	ltk_window_unregister_popup(self->window, self);
 	ltk_window_invalidate_rect(self->window, self->rect);
+	/* when hiding, also update parent so it doesn't show the
+	   entry as selected anymore */
+	if (self->parent && self->parent->vtable->type == LTK_MENU) {
+		ltk_menu_change_state(self->parent);
+	}
 }
 
 /* FIXME: don't require passing rect */
@@ -805,19 +819,19 @@ handle_hover(ltk_menu *menu, int x, int y) {
 }
 
 static int
-ltk_menu_motion_notify(ltk_widget *self, ltk_event *event) {
-	handle_hover((ltk_menu *)self, event->motion.x, event->motion.y);
+ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event) {
+	handle_hover((ltk_menu *)self, event->x, event->y);
 	return 1;
 }
 
 static int
-ltk_menu_mouse_enter(ltk_widget *self, ltk_event *event) {
-	handle_hover((ltk_menu *)self, event->motion.x, event->motion.y);
+ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
+	handle_hover((ltk_menu *)self, event->x, event->y);
 	return 1;
 }
 
 static int
-ltk_menu_mouse_leave(ltk_widget *self, ltk_event *event) {
+ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
 	(void)event;
 	stop_scrolling((ltk_menu *)self);
 	return 1;
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -21,9 +21,6 @@
 #include <string.h>
 #include <stdarg.h>
 
-#include <X11/Xlib.h>
-#include <X11/Xutil.h>
-
 #include "event.h"
 #include "memory.h"
 #include "color.h"
@@ -37,8 +34,8 @@
 #define MAX_SCROLLBAR_WIDTH 100 /* completely arbitrary */
 
 static void ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip);
-static int ltk_scrollbar_mouse_press(ltk_widget *self, ltk_event *event);
-static int ltk_scrollbar_motion_notify(ltk_widget *self, ltk_event *event);
+static int ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event);
+static int ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event);
 static void ltk_scrollbar_destroy(ltk_widget *self, int shallow);
 
 static struct ltk_widget_vtable vtable = {
@@ -50,6 +47,7 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_press = <k_scrollbar_mouse_press,
 	.mouse_release = NULL,
 	.motion_notify = <k_scrollbar_motion_notify,
+	.get_child_at_pos = NULL,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
 	.child_size_change = NULL,
@@ -135,6 +133,7 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 	ltk_color *bg = NULL, *fg = NULL;
 	ltk_rect rect = scrollbar->widget.rect;
 	switch (scrollbar->widget.state) {
+	/* FIXME: proper theme for hover */
 	case LTK_NORMAL:
 		bg = &theme.bg_normal;
 		fg = &theme.fg_normal;
@@ -143,6 +142,7 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 		bg = &theme.bg_normal;
 		fg = &theme.fg_pressed;
 		break;
+	case LTK_HOVER:
 	case LTK_ACTIVE:
 		bg = &theme.bg_normal;
 		fg = &theme.fg_active;
@@ -165,12 +165,12 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 }
 
 static int
-ltk_scrollbar_mouse_press(ltk_widget *self, ltk_event *event) {
+ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	ltk_scrollbar *sc = (ltk_scrollbar *)self;
 	int max_pos;
-	if (event->button.button != LTK_BUTTONL)
+	if (event->button != LTK_BUTTONL)
 		return 0;
-	int ex = event->button.x, ey = event->button.y;
+	int ex = event->x, ey = event->y;
 	ltk_rect handle_rect = get_handle_rect(sc);
 	if (sc->orient == LTK_HORIZONTAL) {
 		if (ex < handle_rect.x || ex > handle_rect.x + handle_rect.w) {
@@ -188,8 +188,8 @@ ltk_scrollbar_mouse_press(ltk_widget *self, ltk_event *event) {
 	else if (sc->cur_pos > max_pos)
 		sc->cur_pos = max_pos;
 	sc->callback(sc->callback_data);
-	sc->last_mouse_x = event->button.x;
-	sc->last_mouse_y = event->button.y;
+	sc->last_mouse_x = event->x;
+	sc->last_mouse_y = event->y;
 	return 1;
 }
 
@@ -219,20 +219,20 @@ ltk_scrollbar_scroll(ltk_widget *self, int delta, int scaled) {
 }
 
 static int
-ltk_scrollbar_motion_notify(ltk_widget *self, ltk_event *event) {
+ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event) {
 	ltk_scrollbar *sc = (ltk_scrollbar *)self;
 	int delta;
 	if (self->state != LTK_PRESSED) {
 		return 1;
 	}
 	if (sc->orient == LTK_HORIZONTAL)
-		delta = event->button.x - sc->last_mouse_x;
+		delta = event->x - sc->last_mouse_x;
 	else
-		delta = event->button.y - sc->last_mouse_y;
+		delta = event->y - sc->last_mouse_y;
 	ltk_scrollbar_scroll(self, delta, 1);
-	sc->last_mouse_x = event->button.x;
-	sc->last_mouse_y = event->button.y;
-	return 0;
+	sc->last_mouse_x = event->x;
+	sc->last_mouse_y = event->y;
+	return 1;
 }
 
 ltk_scrollbar *
diff --git a/src/widget.c b/src/widget.c
@@ -126,55 +126,111 @@ ltk_widget_change_state(ltk_widget *widget) {
 		ltk_window_invalidate_rect(widget->window, widget->rect);
 }
 
+static ltk_widget *
+get_widget_under_pointer(ltk_widget *widget, int x, int y) {
+	ltk_widget *next = NULL;
+	while (widget && widget->vtable->get_child_at_pos) {
+		next = widget->vtable->get_child_at_pos(widget, x, y);
+		if (!next)
+			break;
+		else
+			widget = next;
+	}
+	return widget;
+}
+
+static ltk_widget *
+get_hover_popup(ltk_window *window, int x, int y) {
+	for (size_t i = window->popups_num; i-- > 0;) {
+		if (ltk_collide_rect(window->popups[i]->rect, x, y))
+			return window->popups[i];
+	}
+	return NULL;
+}
+
+/* FIXME: This is still weird. */
 void
-ltk_widget_mouse_press_event(ltk_widget *widget, ltk_event *event) {
-	if (!widget || widget->state == LTK_DISABLED)
+ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
+	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	if (!widget) {
+		ltk_window_unregister_all_popups(window);
+		widget = window->root_widget;
+	}
+	if (!widget)
 		return;
-	int default_handler = 1;
-	if (widget->vtable->mouse_press)
-		default_handler = widget->vtable->mouse_press(widget, event);
-	if (default_handler) {
-		if (event->button.button == LTK_BUTTONL)
-			ltk_window_set_pressed_widget(widget->window, widget);
+	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y);
+	int first = 1;
+	while (cur_widget) {
+		int handled = 0;
+		if (cur_widget->state != LTK_DISABLED) {
+			if (cur_widget->vtable->mouse_press)
+				handled = cur_widget->vtable->mouse_press(cur_widget, event);
+			/* set first non-disabled widget to pressed widget */
+			if (first && event->button == LTK_BUTTONL) {
+				ltk_window_set_pressed_widget(window, cur_widget);
+				first = 0;
+			}
+		}
+		if (!handled)
+			cur_widget = cur_widget->parent;
+		else
+			break;
 	}
 }
 
 void
-ltk_widget_mouse_release_event(ltk_widget *widget, ltk_event *event) {
-	if (!widget || widget->state == LTK_DISABLED)
-		return;
-	if (widget->vtable->mouse_release)
+ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event) {
+	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	if (!widget)
+		widget = window->pressed_widget;
+	if (widget && widget->vtable->mouse_release)
 		widget->vtable->mouse_release(widget, event);
-	ltk_window_set_pressed_widget(widget->window, NULL);
+	if (event->button == LTK_BUTTONL) {
+		ltk_window_set_pressed_widget(window, NULL);
+		/* send motion notify to widget under pointer */
+		ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = event->x, .y = event->y};
+		ltk_window_motion_notify_event(window, &e);
+	}
 }
 
 void
-ltk_widget_motion_notify_event(ltk_widget *widget, ltk_event *event) {
-	/* FIXME: THIS WHOLE STATE HANDLING IS STILL PARTIALLY BROKEN */
-	/* FIXME: need to bring back hover state to make enter/leave work properly */
-	/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
-	/* (especially once keyboard navigation is added) */
-	/* Also, enter/leave should probably be called for all in hierarchy */
-	int set_active = 1;
-	if (widget->window->pressed_widget && widget->window->pressed_widget->vtable->motion_notify) {
-		widget->window->pressed_widget->vtable->motion_notify(widget->window->pressed_widget, event);
-		set_active = 0;
-	} else if (widget && widget->state != LTK_DISABLED) {
-		/* FIXME: because only the bottom widget of the hierarchy is stored,
-		   this *really* does not work properly! */
-		if (widget != widget->window->active_widget) {
-			if (widget->window->active_widget && widget->window->active_widget->vtable->mouse_leave) {
-				widget->window->active_widget->vtable->mouse_leave(widget->window->active_widget, event);
-			}
-			if (widget->vtable->mouse_enter) {
-				widget->vtable->mouse_enter(widget, event);
+ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
+	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	if (!widget) {
+		widget = window->pressed_widget;
+		if (widget) {
+			if (widget->vtable->motion_notify)
+				widget->vtable->motion_notify(widget, event);
+			return;
+		}
+		widget = window->root_widget;
+	}
+	if (!widget)
+		return;
+	if (!ltk_collide_rect(widget->rect, event->x, event->y)) {
+		ltk_window_set_hover_widget(widget->window, NULL, event);
+		return;
+	}
+	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y);
+	int first = 1;
+	while (cur_widget) {
+		int handled = 0;
+		if (cur_widget->state != LTK_DISABLED) {
+			if (cur_widget->vtable->motion_notify)
+				handled = cur_widget->vtable->motion_notify(cur_widget, event);
+			/* set first non-disabled widget to hover widget */
+			/* FIXME: should enter/leave event be sent to parent
+			   when moving from/to widget nested in parent? */
+			if (first) {
+				ltk_window_set_hover_widget(window, cur_widget, event);
+				first = 0;
 			}
 		}
-		if (widget->vtable->motion_notify)
-			set_active = widget->vtable->motion_notify(widget, event);
+		if (!handled)
+			cur_widget = cur_widget->parent;
+		else
+			break;
 	}
-	if (set_active)
-		ltk_window_set_active_widget(widget->window, widget);
 }
 
 int
diff --git a/src/widget.h b/src/widget.h
@@ -38,6 +38,7 @@ typedef enum {
 
 typedef enum {
 	LTK_NORMAL,
+	LTK_HOVER,
 	LTK_PRESSED,
 	LTK_ACTIVE,
 	LTK_DISABLED
@@ -89,11 +90,11 @@ struct ltk_widget {
 struct ltk_widget_vtable {
 	void (*key_press) (struct ltk_widget *, ltk_event *);
 	void (*key_release) (struct ltk_widget *, ltk_event *);
-	int (*mouse_press) (struct ltk_widget *, ltk_event *);
-	int (*mouse_release) (struct ltk_widget *, ltk_event *);
-	int (*motion_notify) (struct ltk_widget *, ltk_event *);
-	int (*mouse_leave) (struct ltk_widget *, ltk_event *);
-	int (*mouse_enter) (struct ltk_widget *, ltk_event *);
+	int (*mouse_press) (struct ltk_widget *, ltk_button_event *);
+	int (*mouse_release) (struct ltk_widget *, ltk_button_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 *);
 
 	void (*resize) (struct ltk_widget *);
 	void (*hide) (struct ltk_widget *);
@@ -102,7 +103,9 @@ struct ltk_widget_vtable {
 	void (*destroy) (struct ltk_widget *, int);
 
 	void (*child_size_change) (struct ltk_widget *, struct ltk_widget *);
+	/* FIXME: why does this take window? */
 	int (*remove_child) (struct ltk_window *, struct ltk_widget *, struct ltk_widget *, char **);
+	struct ltk_widget *(*get_child_at_pos)(struct ltk_widget *, int x, int y);
 
 	ltk_widget_type type;
 	char needs_redraw;
@@ -114,9 +117,10 @@ int ltk_widget_destroy_cmd(struct ltk_window *window, char **tokens, size_t num_
 void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, struct ltk_window *window,
     struct ltk_widget_vtable *vtable, int w, int h);
 void ltk_widget_change_state(ltk_widget *widget);
-void ltk_widget_mouse_press_event(ltk_widget *widget, ltk_event *event);
-void ltk_widget_mouse_release_event(ltk_widget *widget, ltk_event *event);
-void ltk_widget_motion_notify_event(ltk_widget *widget, ltk_event *event);
+/* FIXME: move to separate window.h */
+void ltk_window_mouse_press_event(ltk_window *window, ltk_button_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);
 int ltk_widget_id_free(const char *id);
 ltk_widget *ltk_get_widget(const char *id, ltk_widget_type type, char **errstr);
 void ltk_set_widget(ltk_widget *widget, const char *id);