commit 480c476bee3efca10facb5ff2be6863bf112b72a
parent 8c7d6c1077f97dd4ae11a551ccf3a68fa86e21f6
Author: lumidify <nobody@lumidify.org>
Date:   Wed, 22 Jun 2022 19:27:18 +0200
Turn menu entries into regular widgets
This might be slightly more inefficient but makes a lot of things
more convenient, especially when adding keyboard navigation.
Diffstat:
| M | Makefile |  |  | 2 | +- | 
| M | src/box.c |  |  | 17 | +++++++---------- | 
| M | src/button.c |  |  | 49 | +++++++++++++++---------------------------------- | 
| M | src/graphics.h |  |  | 7 | ++----- | 
| M | src/graphics_xlib.c |  |  | 163 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- | 
| M | src/grid.c |  |  | 31 | +++++++++++++++++-------------- | 
| M | src/label.c |  |  | 6 | ------ | 
| M | src/ltkd.c |  |  | 94 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------- | 
| M | src/menu.c |  |  | 1659 | +++++++++++++++++++++++++++++++++---------------------------------------------- | 
| M | src/menu.h |  |  | 34 | ++++++++++++++++++++++++++-------- | 
| M | src/rect.h |  |  | 11 | ++++++++--- | 
| M | src/scrollbar.c |  |  | 35 | +++++++++++------------------------ | 
| M | src/widget.c |  |  | 117 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- | 
| M | src/widget.h |  |  | 62 | +++++++++++++++++++++++++++++--------------------------------- | 
| M | test2.gui |  |  | 53 | ++++++++++++++++++++++++++++++++++++----------------- | 
15 files changed, 1168 insertions(+), 1172 deletions(-)
diff --git a/Makefile b/Makefile
@@ -7,7 +7,7 @@ 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 = 1
+DEV = 0
 SANITIZE = 0
 USE_PANGO = 0
 
diff --git a/src/box.c b/src/box.c
@@ -37,7 +37,7 @@ static void ltk_recalculate_box(ltk_widget *self);
 static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
 /* FIXME: Why is sticky unsigned short? */
 static int ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short sticky, char **errstr);
-static int ltk_box_remove(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **errstr);
+static int ltk_box_remove(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_press(ltk_widget *self, ltk_button_event *event);
@@ -119,12 +119,6 @@ static void
 ltk_box_destroy(ltk_widget *self, int shallow) {
 	ltk_box *box = (ltk_box *)self;
 	ltk_widget *ptr;
-	char *errstr;
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	for (size_t i = 0; i < box->num_widgets; i++) {
 		ptr = box->widgets[i];
 		ptr->parent = NULL;
@@ -269,7 +263,7 @@ ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short
 }
 
 static int
-ltk_box_remove(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **errstr) {
+ltk_box_remove(ltk_widget *widget, ltk_widget *self, char **errstr) {
 	ltk_box *box = (ltk_box *)self;
 	int sc_w = box->sc->widget.rect.w;
 	int sc_h = box->sc->widget.rect.h;
@@ -284,9 +278,10 @@ ltk_box_remove(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **
 				memmove(box->widgets + i, box->widgets + i + 1,
 				    (box->num_widgets - i - 1) * sizeof(ltk_widget *));
 			box->num_widgets--;
-			ltk_window_invalidate_rect(window, box->widget.rect);
+			ltk_window_invalidate_rect(widget->window, box->widget.rect);
 			/* search for new ideal width/height */
 			/* FIXME: make this all a bit nicer and break the lines better */
+			/* FIXME: other part of ideal size not updated */
 			if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == box->widget.ideal_h) {
 				box->widget.ideal_h = 0;
 				for (size_t j = 0; j < box->num_widgets; j++) {
@@ -339,6 +334,7 @@ ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event) {
 		/* FIXME: configure scrollstep */
 		int delta = event->button == LTK_BUTTON4 ? -15 : 15;
 		ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
+		ltk_window_fake_motion_event(self->window, event->x, event->y);
 		return 1;
 	} else {
 		return 0;
@@ -395,6 +391,7 @@ ltk_box_cmd_remove(
     char **tokens,
     size_t num_tokens,
     char **errstr) {
+	(void)window;
 	ltk_box *box;
 	ltk_widget *widget;
 
@@ -409,7 +406,7 @@ ltk_box_cmd_remove(
 		return 1;
 	}
 
-	return ltk_box_remove(window, widget, (ltk_widget *)box, errstr);
+	return ltk_box_remove(widget, (ltk_widget *)box, errstr);
 }
 
 /* box <box id> create <orientation> */
diff --git a/src/button.c b/src/button.c
@@ -41,7 +41,6 @@ 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);
-static void ltk_button_change_state(ltk_widget *self);
 static void ltk_button_redraw_surface(ltk_button *button, ltk_surface *s);
 
 static struct ltk_widget_vtable vtable = {
@@ -52,7 +51,7 @@ static struct ltk_widget_vtable vtable = {
 	.motion_notify = NULL,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
-	.change_state = <k_button_change_state,
+	.change_state = NULL,
 	.get_child_at_pos = NULL,
 	.resize = NULL,
 	.hide = NULL,
@@ -61,7 +60,7 @@ static struct ltk_widget_vtable vtable = {
 	.child_size_change = NULL,
 	.remove_child = NULL,
 	.type = LTK_BUTTON,
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE,
+	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE | LTK_ACTIVATABLE_ALWAYS,
 };
 
 static struct {
@@ -133,29 +132,22 @@ ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
 	ltk_rect rect = button->widget.rect;
 	int bw = theme.border_width;
 	ltk_color *border = NULL, *fill = NULL;
-	switch (button->widget.state) {
-	case LTK_NORMAL:
-		border = &theme.border;
-		fill = &theme.fill;
-		break;
-	case LTK_HOVER:
-		border = &theme.border_hover;
-		fill = &theme.fill_hover;
-		break;
-	case LTK_PRESSED:
+	/* FIXME: HOVERACTIVE STATE */
+	if (button->widget.state & LTK_DISABLED) {
+		border = &theme.border_disabled;
+		fill = &theme.fill_disabled;
+	} else if (button->widget.state & LTK_PRESSED) {
 		border = &theme.border_pressed;
 		fill = &theme.fill_pressed;
-		break;
-	case LTK_ACTIVE:
+	} else if (button->widget.state & LTK_HOVER) {
+		border = &theme.border_hover;
+		fill = &theme.fill_hover;
+	} else if (button->widget.state & LTK_ACTIVE) {
 		border = &theme.border_active;
 		fill = &theme.fill_active;
-		break;
-	case LTK_DISABLED:
-		border = &theme.border_disabled;
-		fill = &theme.fill_disabled;
-		break;
-	default:
-		ltk_fatal("No style found for button!\n");
+	} else {
+		border = &theme.border;
+		fill = &theme.fill;
 	}
 	rect.x = 0;
 	rect.y = 0;
@@ -171,15 +163,10 @@ ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
 	button->widget.dirty = 0;
 }
 
-static void
-ltk_button_change_state(ltk_widget *self) {
-	self->dirty = 1;
-}
-
 static int
 ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event) {
 	ltk_button *button = (ltk_button *)self;
-	if (self->state == LTK_PRESSED && event->button == LTK_BUTTONL) {
+	if ((self->state & LTK_PRESSED) && event->button == LTK_BUTTONL && ltk_collide_rect(self->rect, event->x, event->y)) {
 		ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
 		return 1;
 	}
@@ -205,12 +192,6 @@ ltk_button_create(ltk_window *window, const char *id, char *text) {
 static void
 ltk_button_destroy(ltk_widget *self, int shallow) {
 	(void)shallow;
-	char *errstr;
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	ltk_button *button = (ltk_button *)self;
 	if (!button) {
 		ltk_warn("Tried to destroy NULL button.\n");
diff --git a/src/graphics.h b/src/graphics.h
@@ -39,11 +39,6 @@ typedef enum {
 	LTK_BORDER_ALL = 0xF
 } ltk_border_sides;
 
-/* FIXME: X only supports 16-bit numbers */
-typedef struct {
-	int x, y;
-} ltk_point;
-
 /* typedef struct ltk_surface ltk_surface; */
 
 /* FIXME: graphics context */
@@ -60,7 +55,9 @@ void ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line
 void ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect);
 /* FIXME: document properly, especiall difference to draw_rect with offsets and line_width */
 void ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides);
+void ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides);
 void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints);
+void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip);
 
 /* TODO */
 /*
diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
@@ -128,6 +128,43 @@ ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_wi
 }
 
 void
+ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides) {
+	if (line_width <= 0)
+		return;
+	XSetForeground(s->renderdata->dpy, s->renderdata->gc, c->xcolor.pixel);
+	int width;
+	ltk_rect final_rect = ltk_rect_intersect(rect, clip_rect);
+	if (border_sides & LTK_BORDER_TOP) {
+		width = rect.y - final_rect.y;
+		if (width > -line_width) {
+			width = line_width + width;
+			XFillRectangle(s->renderdata->dpy, s->d, s->renderdata->gc, final_rect.x, final_rect.y, final_rect.w, width);
+		}
+	}
+	if (border_sides & LTK_BORDER_BOTTOM) {
+		width = (final_rect.y + final_rect.h) - (rect.y + rect.h);
+		if (width > -line_width) {
+			width = line_width + width;
+			XFillRectangle(s->renderdata->dpy, s->d, s->renderdata->gc, final_rect.x, final_rect.y + final_rect.h - width, final_rect.w, width);
+		}
+	}
+	if (border_sides & LTK_BORDER_LEFT) {
+		width = rect.x - final_rect.x;
+		if (width > -line_width) {
+			width = line_width + width;
+			XFillRectangle(s->renderdata->dpy, s->d, s->renderdata->gc, final_rect.x, final_rect.y, width, final_rect.h);
+		}
+	}
+	if (border_sides & LTK_BORDER_RIGHT) {
+		width = (final_rect.x + final_rect.w) - (rect.x + rect.w);
+		if (width > -line_width) {
+			width = line_width + width;
+			XFillRectangle(s->renderdata->dpy, s->d, s->renderdata->gc, final_rect.x + final_rect.w - width, final_rect.y, width, final_rect.h);
+		}
+	}
+}
+
+void
 ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
 	XSetForeground(s->renderdata->dpy, s->renderdata->gc, c->xcolor.pixel);
 	XFillRectangle(s->renderdata->dpy, s->d, s->renderdata->gc, rect.x, rect.y, rect.w, rect.h);
@@ -135,7 +172,7 @@ ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
 
 void
 ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
-	/* FIXME: maybe make this statis since this won't be threaded anyways? */
+	/* FIXME: maybe make this static since this won't be threaded anyways? */
 	XPoint tmp_points[6]; /* to avoid extra allocations when not necessary */
 	/* FIXME: this is ugly and inefficient */
 	XPoint *final_points;
@@ -152,7 +189,127 @@ ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t
 	XSetForeground(s->renderdata->dpy, s->renderdata->gc, c->xcolor.pixel);
 	XFillPolygon(s->renderdata->dpy, s->d, s->renderdata->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
 	if (npoints > 6)
-		free(final_points);
+		ltk_free(final_points);
+}
+
+static inline void
+swap_ptr(void **ptr1, void **ptr2) {
+	void *tmp = *ptr1;
+	*ptr1 = *ptr2;
+	*ptr2 = tmp;
+}
+
+#define check_size(cond) if (!(cond)) ltk_fatal("Unable to perform polygon clipping. This is a bug, tell lumidify about it.\n")
+
+/* FIXME: this can probably be optimized */
+/* This is basically Sutherland-Hodgman, but only the special case for clipping rectangles. */
+void
+ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
+	/* FIXME: is this even more efficient? */
+	XPoint tmp_points1[12]; /* to avoid extra allocations when not necessary */
+	XPoint tmp_points2[12];
+	XPoint *points1;
+	XPoint *points2;
+	/* FIXME: be a bit smarter about this */
+	if (npoints <= 6) {
+		points1 = tmp_points1;
+		points2 = tmp_points2;
+	} else {
+		/* FIXME: I'm pretty sure there can never be more points than this
+		   since we're only clipping against a rectangle, right?
+		   If I can be sure about that, I can remove all the check_size's below. */
+		points1 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
+		points2 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
+	}
+
+	size_t num1 = npoints;
+	size_t num2 = 0;
+	for (size_t i = 0; i < npoints; i++) {
+		points1[i].x = (short)points[i].x;
+		points1[i].y = (short)points[i].y;
+	}
+
+	for (size_t i = 0; i < num1; i++) {
+		XPoint p1 = points1[i];
+		XPoint p2 = points1[(i + 1) % num1];
+		if (p1.x >= clip.x) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = p1;
+			if (p2.x < clip.x) {
+				check_size(num2 < npoints * 2);
+				points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
+			}
+		} else if (p2.x >= clip.x) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
+		}
+	}
+	num1 = num2;
+	num2 = 0;
+	swap_ptr((void**)&points1, (void**)&points2);
+
+	for (size_t i = 0; i < num1; i++) {
+		XPoint p1 = points1[i];
+		XPoint p2 = points1[(i + 1) % num1];
+		if (p1.x <= clip.x + clip.w) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = p1;
+			if (p2.x > clip.x + clip.w) {
+				check_size(num2 < npoints * 2);
+				points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
+			}
+		} else if (p2.x <= clip.x + clip.w) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
+		}
+	}
+	num1 = num2;
+	num2 = 0;
+	swap_ptr((void**)&points1, (void**)&points2);
+
+	for (size_t i = 0; i < num1; i++) {
+		XPoint p1 = points1[i];
+		XPoint p2 = points1[(i + 1) % num1];
+		if (p1.y >= clip.y) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = p1;
+			if (p2.y < clip.y) {
+				check_size(num2 < npoints * 2);
+				points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
+			}
+		} else if (p2.y >= clip.y) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
+		}
+	}
+	num1 = num2;
+	num2 = 0;
+	swap_ptr((void**)&points1, (void**)&points2);
+
+	for (size_t i = 0; i < num1; i++) {
+		XPoint p1 = points1[i];
+		XPoint p2 = points1[(i + 1) % num1];
+		if (p1.y <= clip.y + clip.h) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = p1;
+			if (p2.y > clip.y + clip.h) {
+				check_size(num2 < npoints * 2);
+				points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
+			}
+		} else if (p2.y <= clip.y + clip.h) {
+			check_size(num2 < npoints * 2);
+			points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
+		}
+	}
+
+	if (num2 > 0) {
+		XSetForeground(s->renderdata->dpy, s->renderdata->gc, c->xcolor.pixel);
+		XFillPolygon(s->renderdata->dpy, s->d, s->renderdata->gc, points2, (int)num2, Complex, CoordModeOrigin);
+	}
+	if (npoints > 6) {
+		ltk_free(points1);
+		ltk_free(points2);
+	}
 }
 
 void
@@ -284,7 +441,7 @@ renderer_destroy_window(ltk_renderdata *renderdata) {
 	XFreeGC(renderdata->dpy, renderdata->gc);
 	XDestroyWindow(renderdata->dpy, renderdata->xwindow);
 	XCloseDisplay(renderdata->dpy);
-	free(renderdata);
+	ltk_free(renderdata);
 }
 
 /* FIXME: this is a completely random collection of properties and should be
diff --git a/src/grid.c b/src/grid.c
@@ -47,7 +47,7 @@ static void ltk_recalculate_grid(ltk_widget *self);
 static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
 static int ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
     int row, int column, int row_span, int column_span, unsigned short sticky, char **errstr);
-static int ltk_grid_ungrid(ltk_window *window, ltk_widget *widget, ltk_widget *self, char **errstr);
+static int ltk_grid_ungrid(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 ltk_widget *ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y);
@@ -163,12 +163,6 @@ ltk_grid_create(ltk_window *window, const char *id, int rows, int columns) {
 static void
 ltk_grid_destroy(ltk_widget *self, int shallow) {
 	ltk_grid *grid = (ltk_grid *)self;
-	char *errstr; /* FIXME: unused */
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	ltk_widget *ptr;
 	for (int i = 0; i < grid->rows * grid->columns; i++) {
 		if (grid->widget_grid[i]) {
@@ -240,7 +234,7 @@ ltk_recalculate_grid(ltk_widget *self) {
 		currentx += grid->column_widths[i];
 	}
 	grid->column_pos[grid->columns] = currentx;
-	int orig_width, orig_height;
+	/*int orig_width, orig_height;*/
 	int end_column, end_row;
 	for (i = 0; i < grid->rows; i++) {
 		for (j = 0; j < grid->columns; j++) {
@@ -249,8 +243,8 @@ ltk_recalculate_grid(ltk_widget *self) {
 			ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
 			if (ptr->row != i || ptr->column != j)
 				continue;
-			orig_width = ptr->rect.w;
-			orig_height = ptr->rect.h;
+			/*orig_width = ptr->rect.w;
+			orig_height = ptr->rect.h;*/
 			end_row = i + ptr->row_span;
 			end_column = j + ptr->column_span;
 			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT) {
@@ -259,7 +253,15 @@ ltk_recalculate_grid(ltk_widget *self) {
 			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) {
 				ptr->rect.h = grid->row_pos[end_row] - grid->row_pos[i];
 			}
-			if (orig_width != ptr->rect.w || orig_height != ptr->rect.h)
+			/* FIXME: Figure out a better system for this - it would be nice to make it more
+			   efficient by not doing anything if nothing changed, but that doesn't work when
+			   this function was called because of a child_size_change. In that case, if a
+			   container widget is nested inside another container widget and another widget
+			   inside the nested container sends a child_size_change but the toplevel container
+			   doesn't change the size of the container, the position/size of the widget at the
+			   bottom of the hierarchy will never be updated. That's why updates are forced
+			   here even if seemingly nothing changed, but there probably is a better way. */
+			/*if (orig_width != ptr->rect.w || orig_height != ptr->rect.h)*/
 				ltk_widget_resize(ptr);
 
 			if (ptr->sticky & LTK_STICKY_RIGHT) {
@@ -342,7 +344,7 @@ 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) {
+ltk_grid_ungrid(ltk_widget *widget, ltk_widget *self, char **errstr) {
 	ltk_grid *grid = (ltk_grid *)self;
 	if (widget->parent != (ltk_widget *)grid) {
 		*errstr = "Widget isn't gridded in given grid.\n";
@@ -354,7 +356,7 @@ ltk_grid_ungrid(ltk_window *window, ltk_widget *widget, ltk_widget *self, char *
 			grid->widget_grid[i * grid->columns + j] = NULL;
 		}
 	}
-	ltk_window_invalidate_rect(window, grid->widget.rect);
+	ltk_window_invalidate_rect(widget->window, grid->widget.rect);
 
 	return 0;
 }
@@ -465,6 +467,7 @@ ltk_grid_cmd_ungrid(
     char **tokens,
     size_t num_tokens,
     char **errstr) {
+	(void)window;
 	ltk_grid *grid;
 	ltk_widget *widget;
 	if (num_tokens != 4) {
@@ -474,7 +477,7 @@ ltk_grid_cmd_ungrid(
 	grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, errstr);
 	widget = ltk_get_widget(tokens[3], LTK_WIDGET, errstr);
 	if (!grid || !widget) return 1;
-	return ltk_grid_ungrid(window, widget, (ltk_widget *)grid, errstr);
+	return ltk_grid_ungrid(widget, (ltk_widget *)grid, errstr);
 }
 
 /* grid <grid id> create <rows> <columns> */
diff --git a/src/label.c b/src/label.c
@@ -133,12 +133,6 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
 static void
 ltk_label_destroy(ltk_widget *self, int shallow) {
 	(void)shallow;
-	char *errstr;
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	ltk_label *label = (ltk_label *)self;
 	if (!label) {
 		ltk_warn("Tried to destroy NULL label.\n");
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -675,9 +675,7 @@ 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]);
-		}
+		ltk_widget_hide(window->popups[i]);
 	}
 	window->popups_num = 0;
 	/* somewhat arbitrary, but should be enough for most cases */
@@ -728,6 +726,10 @@ ltk_ini_handler(void *window, const char *widget, const char *prop, const char *
 		ltk_menu_ini_handler(window, prop, value);
 	} else if (strcmp(widget, "submenu") == 0) {
 		ltk_submenu_ini_handler(window, prop, value);
+	} else if (strcmp(widget, "menuentry") == 0) {
+		ltk_menuentry_ini_handler(window, prop, value);
+	} else if (strcmp(widget, "submenuentry") == 0) {
+		ltk_menuentry_ini_handler(window, prop, value);
 	} else {
 		return 0;
 	}
@@ -745,7 +747,9 @@ ltk_load_theme(ltk_window *window, const char *path) {
 	    ltk_label_fill_theme_defaults(window)     ||
 	    ltk_scrollbar_fill_theme_defaults(window) ||
 	    ltk_menu_fill_theme_defaults(window)      ||
-	    ltk_submenu_fill_theme_defaults(window)) {
+	    ltk_submenu_fill_theme_defaults(window)   ||
+	    ltk_menuentry_fill_theme_defaults(window) ||
+	    ltk_submenuentry_fill_theme_defaults(window)) {
 		ltk_uninitialize_theme(window);
 		ltk_fatal("Unable to load theme defaults.\n");
 	}
@@ -759,6 +763,8 @@ ltk_uninitialize_theme(ltk_window *window) {
 	ltk_scrollbar_uninitialize_theme(window);
 	ltk_menu_uninitialize_theme(window);
 	ltk_submenu_uninitialize_theme(window);
+	ltk_menuentry_uninitialize_theme(window);
+	ltk_submenuentry_uninitialize_theme(window);
 }
 
 static ltk_window *
@@ -819,43 +825,71 @@ 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);
+		ltk_widget_state old_state = old->state;
+		old->state &= ~LTK_HOVER;
+		ltk_widget_change_state(old, old_state);
 		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);
+		ltk_widget_state old_state = widget->state;
+		widget->state |= LTK_HOVER;
+		ltk_widget_change_state(widget, old_state);
+		if ((widget->vtable->flags & LTK_HOVER_IS_ACTIVE) && widget != window->active_widget)
+			ltk_window_set_active_widget(window, widget);
 	}
 }
 
 void
 ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
-	if (window->active_widget == widget)
+	if (window->active_widget == widget) {
 		return;
-	if (window->active_widget) {
-		window->active_widget->state = LTK_NORMAL;
-		ltk_widget_change_state(window->active_widget);
 	}
+	ltk_widget *old = window->active_widget;
+	/* Note: this has to be set at the beginning to
+	   avoid infinite recursion in some cases */
 	window->active_widget = widget;
+	ltk_widget *common_parent = NULL;
 	if (widget) {
-		widget->state = LTK_ACTIVE;
-		ltk_widget_change_state(widget);
+		ltk_widget *cur = widget;
+		while (cur) {
+			if (cur->state & LTK_ACTIVE) {
+				common_parent = cur;
+				break;
+			}
+			ltk_widget_state old_state = cur->state;
+			cur->state |= LTK_ACTIVE;
+			ltk_widget_change_state(cur, old_state);
+			cur = cur->parent;
+		}
+	}
+	/* FIXME: better variable names; generally make this nicer */
+	/* special case if old is parent of new active widget */
+	ltk_widget *tmp = common_parent;
+	while (tmp) {
+		if (tmp == old)
+			return;
+		tmp = tmp->parent;
+	}
+	if (old) {
+		ltk_widget *cur = old;
+		while (cur) {
+			if (cur == common_parent)
+				break;
+			ltk_widget_state old_state = cur->state;
+			cur->state &= ~LTK_ACTIVE;
+			ltk_widget_change_state(cur, old_state);
+			cur = cur->parent;
+		}
 	}
 }
 
@@ -863,17 +897,21 @@ void
 ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
 	if (window->pressed_widget == widget)
 		return;
-	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)
+	/* FIXME: won't work properly when key navigation is added and enter can be
+	   used to set a widget to pressed while the pointer is still on another
+	   widget */
+	/* -> also need generic pressed/released callbacks instead of just mouse_press/leave */
+	if (window->pressed_widget) {
+		ltk_widget_state old_state = window->pressed_widget->state;
+		window->pressed_widget->state &= ~LTK_PRESSED;
+		ltk_widget_change_state(window->pressed_widget, old_state);
 		ltk_window_set_active_widget(window, window->pressed_widget);
+	}
 	window->pressed_widget = widget;
 	if (widget) {
-		widget->state = LTK_PRESSED;
-		ltk_widget_change_state(widget);
+		ltk_widget_state old_state = widget->state;
+		widget->state |= LTK_PRESSED;
+		ltk_widget_change_state(widget, old_state);
 	}
 }
 
@@ -1151,6 +1189,8 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
 			err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
 		} else if (strcmp(tokens[0], "submenu") == 0) {
 			err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
+		} else if (strcmp(tokens[0], "menuentry") == 0) {
+			err = ltk_menuentry_cmd(window, tokens, num_tokens, &errstr);
 		} else if (strcmp(tokens[0], "set-root-widget") == 0) {
 			err = ltk_set_root_widget_cmd(window, tokens, num_tokens, &errstr);
 /*
diff --git a/src/menu.c b/src/menu.c
@@ -14,6 +14,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+/* NOTE: The implementation of menus and menu entries is a collection of ugly hacks. */
+
 #include <stdio.h>
 #include <stdlib.h>
 #include <stdint.h>
@@ -41,24 +43,30 @@
 #define MAX(a, b) ((a) > (b) ? (a) : (b))
 
 static struct theme {
-	int border_width;
 	int pad;
-	int text_pad;
+	int arrow_pad;
 	int arrow_size;
+	int border_width;
+	int compress_borders;
+
+	ltk_color border;
+	ltk_color background;
+	ltk_color scroll_background;
+	ltk_color scroll_arrow_color;
+} menu_theme, submenu_theme;
+
+static struct entry_theme {
+	int text_pad;
 	int arrow_pad;
+	int arrow_size;
+	int border_width;
 	int compress_borders;
-	int menu_border_width;
 	/* FIXME: should border_sides actually factor into
 	   size calculation? - probably useless and would
 	   just make it more complicated */
 	/* FIXME: allow different values for different states? */
 	ltk_border_sides border_sides;
 
-	ltk_color background;
-	ltk_color scroll_background;
-	ltk_color scroll_arrow_color;
-	ltk_color menu_border;
-
 	ltk_color text;
 	ltk_color border;
 	ltk_color fill;
@@ -74,95 +82,94 @@ static struct theme {
 	ltk_color text_disabled;
 	ltk_color border_disabled;
 	ltk_color fill_disabled;
-} menu_theme, submenu_theme;
+} menu_entry_theme, submenu_entry_theme;
 
 static void ltk_menu_resize(ltk_widget *self);
-static void ltk_menu_change_state(ltk_widget *self);
 static void ltk_menu_draw(ltk_widget *self, ltk_rect clip);
-static void ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s);
 static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret);
 static void ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step);
 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 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_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 void popup_active_menu(ltk_menuentry *e);
+static void unpopup_active_entry(ltk_menuentry *e);
 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);
+static void recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget);
 static void shrink_entries(ltk_menu *menu);
 static size_t get_entry_with_id(ltk_menu *menu, const char *id);
 static void ltk_menu_destroy(ltk_widget *self, int shallow);
 
-static ltk_menuentry *ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
-static ltk_menuentry *ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
-static ltk_menuentry *ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
-static ltk_menuentry *ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
-static int ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr);
-static int ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr);
-static int ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr);
-static int ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr);
-static int ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr);
-static int ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
-static int ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr);
-static int ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr);
-static int ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
-static int ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr);
-static int ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr);
+static ltk_menuentry *ltk_menuentry_create(ltk_window *window, const char *id, const char *text);
+static void ltk_menuentry_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_menuentry_destroy(ltk_widget *self, int shallow);
+static void ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state);
+static int ltk_menuentry_mouse_release(ltk_widget *self, ltk_button_event *event);
+static void ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry);
+static void ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, char **errstr);
+static void ltk_menuentry_detach_submenu(ltk_menuentry *e);
+
+static int ltk_menu_remove_child(ltk_widget *widget, ltk_widget *self, char **errstr);
+
+#define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_MENU && ((ltk_menu *)e->widget.parent)->is_submenu)
 
 static struct ltk_widget_vtable vtable = {
 	.key_press = NULL,
 	.key_release = NULL,
 	.mouse_press = <k_menu_mouse_press,
 	.motion_notify = <k_menu_motion_notify,
-	.mouse_release = <k_menu_mouse_release,
+	.mouse_release = NULL,
 	.mouse_enter = <k_menu_mouse_enter,
 	.mouse_leave = <k_menu_mouse_leave,
-	.get_child_at_pos = NULL,
+	.get_child_at_pos = <k_menu_get_child_at_pos,
 	.resize = <k_menu_resize,
-	.change_state = <k_menu_change_state,
+	.change_state = NULL,
 	.hide = <k_menu_hide,
 	.draw = <k_menu_draw,
 	.destroy = <k_menu_destroy,
+	.child_size_change = &recalc_ideal_menu_size,
+	.remove_child = <k_menu_remove_child,
+	.type = LTK_MENU,
+	.flags = LTK_NEEDS_REDRAW,
+};
+
+static struct ltk_widget_vtable entry_vtable = {
+	.key_press = NULL,
+	.key_release = NULL,
+	.mouse_press = NULL,
+	.motion_notify = NULL,
+	.mouse_release = <k_menuentry_mouse_release,
+	.mouse_enter = NULL,
+	.mouse_leave = NULL,
+	.get_child_at_pos = NULL,
+	.resize = NULL,
+	.change_state = <k_menuentry_change_state,
+	.hide = NULL,
+	.draw = <k_menuentry_draw,
+	.destroy = <k_menuentry_destroy,
 	.child_size_change = NULL,
 	.remove_child = NULL,
-	.type = LTK_MENU,
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE,
+	.type = LTK_MENUENTRY,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
 };
 
+/* FIXME: standardize menuentry vs. menu_entry */
+
 static ltk_theme_parseinfo menu_parseinfo[] = {
-	{"border-width", THEME_INT, {.i = &menu_theme.border_width}, {.i = 2}, 0, MAX_MENU_BORDER_WIDTH, 0},
 	{"pad", THEME_INT, {.i = &menu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0},
-	{"text-pad", THEME_INT, {.i = &menu_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
-	{"arrow-size", THEME_INT, {.i = &menu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
 	{"arrow-pad", THEME_INT, {.i = &menu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
+	{"arrow-size", THEME_INT, {.i = &menu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
+	{"border-width", THEME_INT, {.i = &menu_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
 	{"compress-borders", THEME_BOOL, {.b = &menu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
-	{"border-sides", THEME_BORDERSIDES, {.border = &menu_theme.border_sides}, {.border = LTK_BORDER_ALL}, 0, 0, 0},
-	{"menu-border-width", THEME_INT, {.i = &menu_theme.menu_border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
-	{"background", THEME_COLOR, {.color = &menu_theme.background}, {.color = "#555555"}, 0, 0, 0},
+	{"border", THEME_COLOR, {.color = &menu_theme.border}, {.color = "#000000"}, 0, 0, 0},
+	{"background", THEME_COLOR, {.color = &menu_theme.background}, {.color = "#000000"}, 0, 0, 0},
 	{"scroll-background", THEME_COLOR, {.color = &menu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
 	{"scroll-arrow-color", THEME_COLOR, {.color = &menu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
-	{"text", THEME_COLOR, {.color = &menu_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border", THEME_COLOR, {.color = &menu_theme.border}, {.color = "#339999"}, 0, 0, 0},
-	{"fill", THEME_COLOR, {.color = &menu_theme.fill}, {.color = "#113355"}, 0, 0, 0},
-	{"text-pressed", THEME_COLOR, {.color = &menu_theme.text_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border-pressed", THEME_COLOR, {.color = &menu_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-pressed", THEME_COLOR, {.color = &menu_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
-	{"text-active", THEME_COLOR, {.color = &menu_theme.text_active}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border-active", THEME_COLOR, {.color = &menu_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-active", THEME_COLOR, {.color = &menu_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
-	{"text-disabled", THEME_COLOR, {.color = &menu_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border-disabled", THEME_COLOR, {.color = &menu_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-disabled", THEME_COLOR, {.color = &menu_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
-	{"menu-border", THEME_COLOR, {.color = &menu_theme.menu_border}, {.color = "#000000"}, 0, 0, 0},
 };
 static int menu_parseinfo_sorted = 0;
 
@@ -181,31 +188,53 @@ ltk_menu_uninitialize_theme(ltk_window *window) {
 	ltk_theme_uninitialize(window, menu_parseinfo, LENGTH(menu_parseinfo));
 }
 
+static ltk_theme_parseinfo menu_entry_parseinfo[] = {
+	{"text-pad", THEME_INT, {.i = &menu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
+	{"arrow-pad", THEME_INT, {.i = &menu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
+	{"arrow-size", THEME_INT, {.i = &menu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
+	{"border-width", THEME_INT, {.i = &menu_entry_theme.border_width}, {.i = 2}, 0, MAX_MENU_BORDER_WIDTH, 0},
+	{"border-sides", THEME_BORDERSIDES, {.border = &menu_entry_theme.border_sides}, {.border = LTK_BORDER_ALL}, 0, 0, 0},
+	{"compress-borders", THEME_BOOL, {.b = &menu_entry_theme.compress_borders}, {.b = 1}, 0, 0, 0},
+	{"text", THEME_COLOR, {.color = &menu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border", THEME_COLOR, {.color = &menu_entry_theme.border}, {.color = "#339999"}, 0, 0, 0},
+	{"fill", THEME_COLOR, {.color = &menu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
+	{"text-pressed", THEME_COLOR, {.color = &menu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
+	{"border-pressed", THEME_COLOR, {.color = &menu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-pressed", THEME_COLOR, {.color = &menu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
+	{"text-active", THEME_COLOR, {.color = &menu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
+	{"border-active", THEME_COLOR, {.color = &menu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-active", THEME_COLOR, {.color = &menu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
+	{"text-disabled", THEME_COLOR, {.color = &menu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border-disabled", THEME_COLOR, {.color = &menu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-disabled", THEME_COLOR, {.color = &menu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
+};
+static int menu_entry_parseinfo_sorted = 0;
+
+int
+ltk_menuentry_ini_handler(ltk_window *window, const char *prop, const char *value) {
+	return ltk_theme_handle_value(window, "menu-entry", prop, value, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo), &menu_entry_parseinfo_sorted);
+}
+
+int
+ltk_menuentry_fill_theme_defaults(ltk_window *window) {
+	return ltk_theme_fill_defaults(window, "menu-entry", menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
+}
+
+void
+ltk_menuentry_uninitialize_theme(ltk_window *window) {
+	ltk_theme_uninitialize(window, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
+}
+
 static ltk_theme_parseinfo submenu_parseinfo[] = {
-	{"border-width", THEME_INT, {.i = &submenu_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
-	{"pad", THEME_INT, {.i = &submenu_theme.pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
-	{"text-pad", THEME_INT, {.i = &submenu_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
-	{"arrow-size", THEME_INT, {.i = &submenu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
+	{"pad", THEME_INT, {.i = &submenu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0},
 	{"arrow-pad", THEME_INT, {.i = &submenu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
-	{"compress-borders", THEME_BOOL, {.b = &submenu_theme.compress_borders}, {.b = 0}, 0, 0, 0},
-	{"border-sides", THEME_BORDERSIDES, {.border = &submenu_theme.border_sides}, {.border = LTK_BORDER_NONE}, 0, 0, 0},
-	{"menu-border-width", THEME_INT, {.i = &submenu_theme.menu_border_width}, {.i = 1}, 0, MAX_MENU_BORDER_WIDTH, 0},
-	{"background", THEME_COLOR, {.color = &submenu_theme.background}, {.color = "#555555"}, 0, 0, 0},
+	{"arrow-size", THEME_INT, {.i = &submenu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
+	{"border-width", THEME_INT, {.i = &submenu_theme.border_width}, {.i = 1}, 0, MAX_MENU_BORDER_WIDTH, 0},
+	{"compress-borders", THEME_BOOL, {.b = &submenu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
+	{"border", THEME_COLOR, {.color = &submenu_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"background", THEME_COLOR, {.color = &submenu_theme.background}, {.color = "#000000"}, 0, 0, 0},
 	{"scroll-background", THEME_COLOR, {.color = &submenu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
 	{"scroll-arrow-color", THEME_COLOR, {.color = &submenu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
-	{"text", THEME_COLOR, {.color = &submenu_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border", THEME_COLOR, {.color = &submenu_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill", THEME_COLOR, {.color = &submenu_theme.fill}, {.color = "#113355"}, 0, 0, 0},
-	{"text-pressed", THEME_COLOR, {.color = &submenu_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
-	{"border-pressed", THEME_COLOR, {.color = &submenu_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-pressed", THEME_COLOR, {.color = &submenu_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
-	{"text-active", THEME_COLOR, {.color = &submenu_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
-	{"border-active", THEME_COLOR, {.color = &submenu_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-active", THEME_COLOR, {.color = &submenu_theme.fill_active}, {.color = "#113355"}, 0, 0, 0},
-	{"text-disabled", THEME_COLOR, {.color = &submenu_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"border-disabled", THEME_COLOR, {.color = &submenu_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
-	{"fill-disabled", THEME_COLOR, {.color = &submenu_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
-	{"menu-border", THEME_COLOR, {.color = &submenu_theme.menu_border}, {.color = "#FFFFFF"}, 0, 0, 0},
 };
 static int submenu_parseinfo_sorted = 0;
 
@@ -224,40 +253,122 @@ ltk_submenu_uninitialize_theme(ltk_window *window) {
 	ltk_theme_uninitialize(window, submenu_parseinfo, LENGTH(submenu_parseinfo));
 }
 
+static ltk_theme_parseinfo submenu_entry_parseinfo[] = {
+	{"text-pad", THEME_INT, {.i = &submenu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
+	{"arrow-pad", THEME_INT, {.i = &submenu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
+	{"arrow-size", THEME_INT, {.i = &submenu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
+	{"border-width", THEME_INT, {.i = &submenu_entry_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
+	{"border-sides", THEME_BORDERSIDES, {.border = &submenu_entry_theme.border_sides}, {.border = LTK_BORDER_NONE}, 0, 0, 0},
+	{"compress-borders", THEME_BOOL, {.b = &submenu_entry_theme.compress_borders}, {.b = 0}, 0, 0, 0},
+	{"text", THEME_COLOR, {.color = &submenu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border", THEME_COLOR, {.color = &submenu_entry_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill", THEME_COLOR, {.color = &submenu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
+	{"text-pressed", THEME_COLOR, {.color = &submenu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
+	{"border-pressed", THEME_COLOR, {.color = &submenu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-pressed", THEME_COLOR, {.color = &submenu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
+	{"text-active", THEME_COLOR, {.color = &submenu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
+	{"border-active", THEME_COLOR, {.color = &submenu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-active", THEME_COLOR, {.color = &submenu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
+	{"text-disabled", THEME_COLOR, {.color = &submenu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"border-disabled", THEME_COLOR, {.color = &submenu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
+	{"fill-disabled", THEME_COLOR, {.color = &submenu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
+};
+static int submenu_entry_parseinfo_sorted = 0;
+
+int
+ltk_submenuentry_ini_handler(ltk_window *window, const char *prop, const char *value) {
+	return ltk_theme_handle_value(window, "submenu-entry", prop, value, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo), &submenu_entry_parseinfo_sorted);
+}
+
+int
+ltk_submenuentry_fill_theme_defaults(ltk_window *window) {
+	return ltk_theme_fill_defaults(window, "submenu-entry", submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
+}
+
+void
+ltk_submenuentry_uninitialize_theme(ltk_window *window) {
+	ltk_theme_uninitialize(window, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
+}
+
 static void
-ltk_menu_resize(ltk_widget *self) {
-	ltk_menu *menu = (ltk_menu *)self;
-	double x_old = menu->x_scroll_offset;
-	double y_old = menu->y_scroll_offset;
-	int max_x, max_y;
-	ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
-	if (menu->x_scroll_offset > max_x)
-		menu->x_scroll_offset = max_x;
-	if (menu->y_scroll_offset > max_y)
-		menu->y_scroll_offset = max_y;
-	if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
-	    fabs(y_old - menu->y_scroll_offset) < 0.01) {
-		menu->widget.dirty = 1;
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
+	ltk_menuentry *e = (ltk_menuentry *)self;
+	int in_submenu = IN_SUBMENU(e);
+	int submenus_opened = self->parent && self->parent->vtable->type == LTK_MENU && ((ltk_menu *)self->parent)->popup_submenus;
+	if (!(self->state & (LTK_ACTIVE | LTK_PRESSED))) {
+		/* Note: This only has to take care of the submenu that is the direct child
+		   of e because ltk_window_set_active_widget already calls change_state for
+		   the whole hierarchy */
+		unpopup_active_entry(e);
+	} else if ((self->state & LTK_PRESSED) && !(old_state & LTK_PRESSED) && submenus_opened) {
+		((ltk_menu *)self->parent)->popup_submenus = 0;
+	} else if (((self->state & LTK_PRESSED) ||
+	           ((self->state & LTK_ACTIVE) && (in_submenu || submenus_opened))) &&
+		   e->submenu && e->submenu->widget.hidden) {
+		printf("popup: %s, %d, %d, %d\n", self->id, submenus_opened, self->state, self->parent->hidden);
+		popup_active_menu(e);
+		if (self->parent && self->parent->vtable->type == LTK_MENU)
+			((ltk_menu *)self->parent)->popup_submenus = 1;
 	}
 }
 
 static void
-ltk_menu_change_state(ltk_widget *self) {
-	ltk_menu *menu = (ltk_menu *)self;
-	if (self->state != LTK_PRESSED && menu->pressed_entry < menu->num_entries) {
-		menu->pressed_entry = SIZE_MAX;
-		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);
-		}
+ltk_menuentry_draw(ltk_widget *self, ltk_rect clip) {
+	/* FIXME: figure out how hidden should work */
+	if (self->hidden)
+		return;
+	ltk_menuentry *entry = (ltk_menuentry *)self;
+	int in_submenu = IN_SUBMENU(entry);
+	struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
+	ltk_color *text, *border, *fill;
+	if (self->state & LTK_DISABLED) {
+		text = &t->text_disabled;
+		border = &t->border_disabled;
+		fill = &t->fill_disabled;
+	} else if (self->state & LTK_PRESSED) {
+		text = &t->text_pressed;
+		border = &t->border_pressed;
+		fill = &t->fill_pressed;
+	} else if (self->state & LTK_HOVERACTIVE) {
+		text = &t->text_active;
+		border = &t->border_active;
+		fill = &t->fill_active;
+	} else {
+		text = &t->text;
+		border = &t->border;
+		fill = &t->fill;
+	}
+	ltk_rect rect = self->rect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
+	ltk_surface_fill_rect(self->window->surface, fill, clip_final);
+
+	ltk_surface *s;
+	int text_w, text_h;
+	ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
+	if (!ltk_surface_cache_get_surface(entry->text_surface_key, &s) || self->dirty) {
+		ltk_surface_fill_rect(s, fill, (ltk_rect){0, 0, text_w, text_h});
+		ltk_text_line_draw(entry->text_line, s, text, 0, 0);
+		self->dirty = 0;
+	}
+	int text_x = rect.x + t->text_pad + t->border_width;
+	int text_y = rect.y + t->text_pad + t->border_width;
+	ltk_rect text_clip = ltk_rect_intersect(clip, (ltk_rect){text_x, text_y, text_w, text_h});
+	ltk_surface_copy(
+	    s, self->window->surface,
+	    (ltk_rect){text_clip.x - text_x, text_clip.y - text_y, text_clip.w, text_clip.h}, text_clip.x, text_clip.y
+	);
+
+	if (in_submenu && entry->submenu) {
+		ltk_point arrow_points[] = {
+		    {rect.x + rect.w - t->arrow_pad - t->border_width, rect.y + rect.h / 2},
+		    {rect.x + rect.w - t->arrow_pad - t->border_width - t->arrow_size, rect.y + rect.h / 2 - t->arrow_size / 2},
+		    {rect.x + rect.w - t->arrow_pad - t->border_width - t->arrow_size, rect.y + rect.h / 2 + t->arrow_size / 2}
+		};
+		ltk_surface_fill_polygon_clipped(self->window->surface, text, arrow_points, LENGTH(arrow_points), clip_final);
 	}
+	ltk_surface_draw_border_clipped(self->window->surface, border, rect, clip_final, t->border_width, t->border_sides);
 }
 
 static void
@@ -267,185 +378,110 @@ ltk_menu_draw(ltk_widget *self, ltk_rect clip) {
 	ltk_menu *menu = (ltk_menu *)self;
 	ltk_rect rect = self->rect;
 	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
-	ltk_surface *s;
-	if (!ltk_surface_cache_get_surface(self->surface_key, &s) || self->dirty)
-		ltk_menu_redraw_surface(menu, s);
-	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
+	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
+	ltk_surface_fill_rect(self->window->surface, &t->background, self->rect);
+	for (size_t i = 0; i < menu->num_entries; i++) {
+		/* FIXME: I guess it could be improved *slightly* by making the clip rect
+		   smaller when scrollarrows are shown */
+		/* draw active entry after others so it isn't hidden with compress_borders */
+		if ((menu->entries[i]->widget.state & (LTK_ACTIVE | LTK_PRESSED | LTK_HOVER)) && i < menu->num_entries - 1) {
+			ltk_menuentry_draw(&menu->entries[i + 1]->widget, clip_final);
+			ltk_menuentry_draw(&menu->entries[i]->widget, clip_final);
+			i++;
+		} else {
+			ltk_widget *widget = &menu->entries[i]->widget;
+			ltk_menuentry_draw(&menu->entries[i]->widget, clip_final);
+		}
+	}
+
+	/* FIXME: active, pressed states */
+	int sz = t->arrow_size + t->arrow_pad * 2;
+	int ww = self->rect.w;
+	int wh = self->rect.h;
+	int wx = self->rect.x;
+	int wy = self->rect.y;
+	int mbw = t->border_width;
+	/* FIXME: handle pathological case where rect is so small that this still draws outside */
+	if (rect.w < (int)self->ideal_w) {
+		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, sz, wh - mbw * 2});
+		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + ww - sz - mbw, wy + mbw, sz, wh - mbw * 2});
+		ltk_point arrow_points[3] = {
+		    {wx + t->arrow_pad + mbw, wy + wh / 2},
+		    {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 - t->arrow_size / 2},
+		    {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 + t->arrow_size / 2}
+		};
+		ltk_surface_fill_polygon(self->window->surface, &t->scroll_arrow_color, arrow_points, 3);
+		arrow_points[0] = (ltk_point){wx + ww - t->arrow_pad - mbw, wy + wh / 2};
+		arrow_points[1] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 - t->arrow_size / 2};
+		arrow_points[2] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 + t->arrow_size / 2};
+		ltk_surface_fill_polygon(self->window->surface, &t->scroll_arrow_color, arrow_points, 3);
+	}
+	if (rect.h < (int)self->ideal_h) {
+		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, ww - mbw * 2, sz});
+		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + mbw, wy + wh - sz - mbw, ww - mbw * 2, sz});
+		ltk_point arrow_points[3] = {
+		    {wx + ww / 2, wy + t->arrow_pad + mbw},
+		    {wx + ww / 2 - t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size},
+		    {wx + ww / 2 + t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size}
+		};
+		ltk_surface_fill_polygon(self->window->surface, &t->scroll_arrow_color, arrow_points, 3);
+		arrow_points[0] = (ltk_point){wx + ww / 2, wy + wh - t->arrow_pad - mbw};
+		arrow_points[1] = (ltk_point){wx + ww / 2 - t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size};
+		arrow_points[2] = (ltk_point){wx + ww / 2 + t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size};
+		ltk_surface_fill_polygon(self->window->surface, &t->scroll_arrow_color, arrow_points, 3);
+	}
+	ltk_surface_draw_border(self->window->surface, &t->border, rect, mbw, LTK_BORDER_ALL);
+
+	self->dirty = 0;
 }
 
-/* FIXME: glitches when drawing text with stb backend while scrolling */
+
 static void
-ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s) {
-	ltk_rect rect = menu->widget.rect;
-	int ideal_w = menu->widget.ideal_w, ideal_h = menu->widget.ideal_h;
+ltk_menu_resize(ltk_widget *self) {
+	ltk_menu *menu = (ltk_menu *)self;
+	int max_x, max_y;
+	ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
+	if (menu->x_scroll_offset > max_x)
+		menu->x_scroll_offset = max_x;
+	if (menu->y_scroll_offset > max_y)
+		menu->y_scroll_offset = max_y;
+
+	ltk_rect rect = self->rect;
 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
+	struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
 
+	int ideal_w = self->ideal_w, ideal_h = self->ideal_h;
 	int arrow_size = t->arrow_pad * 2 + t->arrow_size;
 	int start_x = rect.w < ideal_w ? arrow_size : 0;
 	int start_y = rect.h < ideal_h ? arrow_size : 0;
-	start_x += t->menu_border_width;
-	start_y += t->menu_border_width;
-	int real_w = rect.w - start_x * 2;
-	int real_h = rect.h - start_y * 2;
+	start_x += t->border_width;
+	start_y += t->border_width;
 
-	int offset_x = (int)menu->x_scroll_offset;
-	int offset_y = (int)menu->y_scroll_offset;
+	int mbw = t->border_width;
+	int cur_abs_x = -(int)menu->x_scroll_offset + rect.x + start_x + t->pad;
+	int cur_abs_y = -(int)menu->y_scroll_offset + rect.y + start_y + t->pad;
+	printf("%d, %d\n", self->rect.x, self->rect.w);
 
-	ltk_surface_fill_rect(s, &t->background, (ltk_rect){0, 0, rect.w, rect.h});
-	int text_w, text_h;
-	ltk_color *text, *border, *fill;
-	int cur_abs_x = 0, cur_abs_y = 0;
-	if (menu->is_submenu)
-		cur_abs_y = t->pad;
-	else
-		cur_abs_x = t->pad;
-	int overlap = t->compress_borders ? t->border_width - t->pad : 0;
-	int bw_advance = t->compress_borders ? t->border_width : t->border_width * 2;
-	int mbw = t->menu_border_width;
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		ltk_menuentry *e = &menu->entries[i];
-		ltk_text_line_get_size(e->text, &text_w, &text_h);
-		if (menu->is_submenu) {
-			if (cur_abs_y + t->border_width * 2 + t->text_pad * 2 + text_h <= offset_y) {
-				/* FIXME: ugly because repeated further down */
-				cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
-				continue;
-			} else if (cur_abs_y >= offset_y + real_h) {
-				break;
-			}
-		} else {
-			if (cur_abs_x + t->border_width * 2 + t->text_pad * 2 + text_w <= offset_x) {
-				cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
-				continue;
-			} else if (cur_abs_x >= offset_x + real_w) {
-				break;
-			}
-		}
-		/* FIXME: allow different border_sides for different states */
-		if (e->disabled) {
-			text = &t->text_disabled;
-			border = &t->border_disabled;
-			fill = &t->fill_disabled;
-		} else if (menu->pressed_entry == i) {
-			text = &t->text_pressed;
-			border = &t->border_pressed;
-			fill = &t->fill_pressed;
-		} else if (menu->active_entry == i) {
-			text = &t->text_active;
-			border = &t->border_active;
-			fill = &t->fill_active;
-		} else {
-			text = &t->text;
-			border = &t->border;
-			fill = &t->fill;
-		}
-		/* FIXME: how well-defined is it to give X drawing commands
-		   with parts outside of the actual pixmap? */
-		/* FIXME: optimize drawing (avoid drawing pixels multiple times) */
-		int draw_x = cur_abs_x - offset_x + start_x;
-		int draw_y = cur_abs_y - offset_y + start_y;
-		int last_special = i > 0 && (menu->active_entry == i - 1 || menu->pressed_entry == i - 1);
+		ltk_menuentry *e = menu->entries[i];
+		e->widget.rect.x = cur_abs_x;
+		e->widget.rect.y = cur_abs_y;
 		if (menu->is_submenu) {
-			int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
-			int height = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
-			ltk_rect r;
-			if (last_special && overlap > 0) {
-				r = (ltk_rect){
-				    draw_x + overlap,
-				    draw_y + t->pad, /* t->pad is the same as t->border_width - overlap */
-				    ideal_w - t->pad * 2 - mbw * 2,
-				    height - overlap
-				};
-			} else {
-				r = (ltk_rect){draw_x + t->pad, draw_y, ideal_w - t->pad * 2, height};
-			}
-			ltk_surface_fill_rect(s, fill, r);
-			ltk_text_line_draw(
-			    e->text, s, text,
-			    draw_x + t->pad + t->border_width + t->text_pad,
-			    draw_y + height / 2 - text_h / 2
-			);
-			if (e->submenu) {
-				ltk_point arrow_points[3] = {
-				    {draw_x + ideal_w - t->pad - t->arrow_pad, draw_y + height / 2},
-				    {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 - t->arrow_size / 2},
-				    {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 + t->arrow_size / 2}
-				};
-				ltk_surface_fill_polygon(s, text, arrow_points, 3);
-			}
-			if (last_special && overlap > 0) {
-				ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_TOP);
-				if (t->border_sides & LTK_BORDER_TOP)
-					ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_TOP);
-			} else {
-				ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
-			}
-			cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
+			e->widget.rect.w = ideal_w - 2 * t->pad - 2 * mbw;
+			e->widget.rect.h = e->widget.ideal_h;
+			cur_abs_y += e->widget.ideal_h + t->pad;
+			if (et->compress_borders)
+				cur_abs_y -= et->border_width;
 		} else {
-			ltk_rect r;
-			if (last_special && overlap > 0) {
-				r = (ltk_rect){
-				    draw_x + overlap,
-				    draw_y + t->pad,
-				    t->text_pad * 2 + t->border_width * 2 - overlap + text_w,
-				    ideal_h - t->pad * 2 - mbw * 2
-				};
-			} else {
-				r = (ltk_rect){draw_x, draw_y + t->pad, t->text_pad * 2 + t->border_width * 2 + text_w, ideal_h - t->pad * 2};
-			}
-			ltk_surface_fill_rect(s, fill, r);
-			/* FIXME: should the text be bottom-aligned in case different
-			   entries have different text height? */
-			ltk_text_line_draw(
-			    e->text, s, text,
-			    draw_x + t->border_width + t->text_pad,
-			    draw_y + t->pad + t->border_width + t->text_pad
-			);
-			if (last_special && overlap > 0) {
-				ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_LEFT);
-				if (t->border_sides & LTK_BORDER_LEFT)
-					ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_LEFT);
-			} else {
-				ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
-			}
-			cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
+			e->widget.rect.w = e->widget.ideal_w;
+			e->widget.rect.h = ideal_h - 2 * t->pad - 2 * mbw;
+			cur_abs_x += e->widget.ideal_w + t->pad;
+			if (et->compress_borders)
+				cur_abs_x -= et->border_width;
 		}
 	}
-	/* FIXME: active, pressed states */
-	int sz = t->arrow_size + t->arrow_pad * 2;
-	int ww = menu->widget.rect.w;
-	int wh = menu->widget.rect.h;
-	if (rect.w < ideal_w) {
-		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, sz, wh - mbw * 2});
-		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){ww - sz - mbw, mbw, sz, wh - mbw * 2});
-		ltk_point arrow_points[3] = {
-		    {t->arrow_pad + mbw, wh / 2},
-		    {t->arrow_pad + mbw + t->arrow_size, wh / 2 - t->arrow_size / 2},
-		    {t->arrow_pad + mbw + t->arrow_size, wh / 2 + t->arrow_size / 2}
-		};
-		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
-		arrow_points[0] = (ltk_point){ww - t->arrow_pad - mbw, wh / 2};
-		arrow_points[1] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 - t->arrow_size / 2};
-		arrow_points[2] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 + t->arrow_size / 2};
-		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
-	}
-	if (rect.h < ideal_h) {
-		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, ww - mbw * 2, sz});
-		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, wh - sz - mbw, ww - mbw * 2, sz});
-		ltk_point arrow_points[3] = {
-		    {ww / 2, t->arrow_pad + mbw},
-		    {ww / 2 - t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size},
-		    {ww / 2 + t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size}
-		};
-		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
-		arrow_points[0] = (ltk_point){ww / 2, wh - t->arrow_pad - mbw};
-		arrow_points[1] = (ltk_point){ww / 2 - t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
-		arrow_points[2] = (ltk_point){ww / 2 + t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
-		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
-	}
-	ltk_surface_draw_border(s, &t->menu_border, (ltk_rect){0, 0, ww, wh}, mbw, LTK_BORDER_ALL);
-
-	menu->widget.dirty = 0;
+	self->dirty = 1;
+	ltk_window_invalidate_rect(self->window, self->rect);
 }
 
 static void
@@ -485,8 +521,9 @@ ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
 	if (menu->y_scroll_offset > max_scroll_y)
 		menu->y_scroll_offset = max_scroll_y;
 	/* FIXME: sensible epsilon? */
-	if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
-	    fabs(y_old - menu->y_scroll_offset) < 0.01) {
+	if (fabs(x_old - menu->x_scroll_offset) > 0.01 ||
+	    fabs(y_old - menu->y_scroll_offset) > 0.01) {
+		ltk_menu_resize(&menu->widget);
 		menu->widget.dirty = 1;
 		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
 	}
@@ -503,8 +540,6 @@ ltk_menu_scroll_callback(void *data) {
 	);
 }
 
-/* FIXME: HANDLE mouse scroll wheel! */
-
 static void
 stop_scrolling(ltk_menu *menu) {
 	menu->scroll_top_hover = 0;
@@ -516,62 +551,30 @@ stop_scrolling(ltk_menu *menu) {
 }
 
 /* FIXME: should ideal_w, ideal_h just be int? */
-static size_t
-get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret) {
+static ltk_widget *
+ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
+	ltk_menu *menu = (ltk_menu *)self;
 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
 	int arrow_size = t->arrow_size + t->arrow_pad * 2;
-	int mbw = t->menu_border_width;
-	int start_x = menu->widget.rect.x + mbw, end_x = menu->widget.rect.x + menu->widget.rect.w - mbw;
-	int start_y = menu->widget.rect.y + mbw, end_y = menu->widget.rect.y + menu->widget.rect.h - mbw;
-	if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
+	int mbw = t->border_width;
+	int start_x = self->rect.x + mbw, end_x = self->rect.x + self->rect.w - mbw;
+	int start_y = self->rect.y + mbw, end_y = self->rect.y + self->rect.h - mbw;
+	if (self->rect.w < (int)self->ideal_w) {
 		start_x += arrow_size;
 		end_x -= arrow_size;
 	}
-	if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
+	if (self->rect.h < (int)self->ideal_h) {
 		start_y += arrow_size;
 		end_y -= arrow_size;
 	}
 	if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
-		return SIZE_MAX;
+		return NULL;
 
-	int bw_sub = t->compress_borders ? t->border_width : 0;
-	int cur_x = start_x - (int)menu->x_scroll_offset + t->pad;
-	int cur_y = start_y - (int)menu->y_scroll_offset + t->pad;
-	/* FIXME: could be optimized a bit */
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		ltk_menuentry *e = &menu->entries[i];
-		int text_w, text_h;
-		ltk_text_line_get_size(e->text, &text_w, &text_h);
-		if (menu->is_submenu) {
-			int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
-			int w = (int)menu->widget.ideal_w - t->pad * 2;
-			int h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
-			if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
-				if (entry_rect_ret) {
-					entry_rect_ret->x = cur_x;
-					entry_rect_ret->y = cur_y;
-					entry_rect_ret->w = w;
-					entry_rect_ret->h = h;
-				}
-				return i;
-			}
-			cur_y += h - bw_sub + t->pad;
-		} else {
-			int w = text_w + t->text_pad * 2 + t->border_width * 2;
-			int h = (int)menu->widget.ideal_h - t->pad * 2;
-			if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
-				if (entry_rect_ret) {
-					entry_rect_ret->x = cur_x;
-					entry_rect_ret->y = cur_y;
-					entry_rect_ret->w = w;
-					entry_rect_ret->h = h;
-				}
-				return i;
-			}
-			cur_x += w - bw_sub + t->pad;
-		}
+		if (ltk_collide_rect(menu->entries[i]->widget.rect, x, y))
+			return &menu->entries[i]->widget;
 	}
-	return SIZE_MAX;
+	return NULL;
 }
 
 /* FIXME: make sure timers are always destroyed when widget is destroyed */
@@ -610,54 +613,44 @@ set_scroll_timer(ltk_menu *menu, int x, int y) {
 }
 
 static int
-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->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 */
-		ltk_queue_event(self->window, LTK_EVENT_MENU, menu->entries[idx].id, "menu_entry_click");
-	}
-	if (menu->pressed_entry < menu->num_entries && idx < menu->num_entries)
-		menu->active_entry = menu->pressed_entry;
-	else if (idx < menu->num_entries)
-		menu->active_entry = idx;
-	menu->pressed_entry = SIZE_MAX;
-	self->dirty = 1;
+ltk_menuentry_mouse_release(ltk_widget *self, ltk_button_event *event) {
+	(void)event;
+	ltk_menuentry *e = (ltk_menuentry *)self;
+	int in_submenu = IN_SUBMENU(e);
+	int keep_popup = self->parent && self->parent->vtable->type == LTK_MENU && ((ltk_menu *)self->parent)->popup_submenus;
+	/* FIXME: problem when scrolling because actual shown rect may not be entire rect */
+	if ((self->state & LTK_PRESSED) && event->button == LTK_BUTTONL && ltk_collide_rect(self->rect, event->x, event->y)) {
+		if (in_submenu || !keep_popup) {
+			ltk_window_unregister_all_popups(self->window);
+		}
+		ltk_queue_event(self->window, LTK_EVENT_MENU, self->id, "menu_entry_click");
+	}
 	return 1;
 }
 
 static int
 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) {
-	case LTK_BUTTONL:
-		idx = get_entry_at_point(menu, event->x, event->y, NULL);
-		if (idx < menu->num_entries) {
-			menu->pressed_entry = idx;
-			self->dirty = 1;
-		}
-		break;
 	case LTK_BUTTON4:
 		ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
-		handle_hover(menu, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, event->x, event->y);
 		break;
 	case LTK_BUTTON5:
 		ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
-		handle_hover(menu, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, event->x, event->y);
 		break;
 	case LTK_BUTTON6:
 		ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
-		handle_hover(menu, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, event->x, event->y);
 		break;
 	case LTK_BUTTON7:
 		ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
-		handle_hover(menu, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, event->x, event->y);
 		break;
 	default:
-		break;
+		return 0;
 	}
 	return 1;
 }
@@ -665,167 +658,156 @@ ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event) {
 static void
 ltk_menu_hide(ltk_widget *self) {
 	ltk_menu *menu = (ltk_menu *)self;
-	menu->active_entry = menu->pressed_entry = SIZE_MAX;
 	if (menu->scroll_timer_id >= 0)
 		ltk_unregister_timer(menu->scroll_timer_id);
 	menu->scroll_bottom_hover = menu->scroll_top_hover = 0;
 	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: this is really ugly/hacky */
+	if (menu->unpopup_submenus_on_hide && self->parent && self->parent->vtable->type == LTK_MENUENTRY &&
+	    self->parent->parent && self->parent->parent->vtable->type == LTK_MENU) {
+		printf("hide: %s\n", self->id);
+		((ltk_menu *)self->parent->parent)->popup_submenus = 0;
 	}
+	menu->unpopup_submenus_on_hide = 1;
 }
 
-/* FIXME: don't require passing rect */
+/* FIXME: hacky because entries need to know about their parents to be able to properly position the popup */
 static void
-popup_active_menu(ltk_menu *menu, ltk_rect r) {
-	size_t idx = menu->active_entry;
-	if (idx >= menu->num_entries)
+popup_active_menu(ltk_menuentry *e) {
+	if (!e->submenu)
 		return;
-	int win_w = menu->widget.window->rect.w;
-	int win_h = menu->widget.window->rect.h;
-	if (menu->entries[idx].submenu) {
-		ltk_menu *submenu = menu->entries[idx].submenu;
-		int ideal_w = submenu->widget.ideal_w + 2;
-		int ideal_h = submenu->widget.ideal_h;
-		int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
-		if (menu->is_submenu) {
-			int space_left = menu->widget.rect.x;
-			int space_right = win_w - (menu->widget.rect.x + menu->widget.rect.w);
-			int x_right = menu->widget.rect.x + menu->widget.rect.w;
-			int x_left = menu->widget.rect.x - ideal_w;
-			if (menu->was_opened_left) {
-				if (x_left >= 0) {
-					x_final = x_left;
-					submenu->was_opened_left = 1;
-				} else if (space_right >= ideal_w) {
-					x_final = x_right;
-					submenu->was_opened_left = 0;
-				} else {
-					x_final = 0;
-					if (win_w < ideal_w)
-						w_final = win_w;
-					submenu->was_opened_left = 1;
-				}
+	int in_submenu = 0, was_opened_left = 0;
+	ltk_rect menu_rect = e->widget.rect;
+	ltk_rect entry_rect = e->widget.rect;
+	if (e->widget.parent && e->widget.parent->vtable->type == LTK_MENU) {
+		ltk_menu *menu = (ltk_menu *)e->widget.parent;
+		in_submenu = menu->is_submenu;
+		was_opened_left = menu->was_opened_left;
+		menu_rect = menu->widget.rect;
+	}
+	int win_w = e->widget.window->rect.w;
+	int win_h = e->widget.window->rect.h;
+	ltk_menu *submenu = e->submenu;
+	int ideal_w = submenu->widget.ideal_w;
+	int ideal_h = submenu->widget.ideal_h;
+	int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
+	if (in_submenu) {
+		int space_left = menu_rect.x;
+		int space_right = win_w - (menu_rect.x + menu_rect.w);
+		int x_right = menu_rect.x + menu_rect.w;
+		int x_left = menu_rect.x - ideal_w;
+		if (submenu_theme.compress_borders) {
+			x_right -= submenu_theme.border_width;
+			x_left += submenu_theme.border_width;
+		}
+		if (was_opened_left) {
+			if (x_left >= 0) {
+				x_final = x_left;
+				submenu->was_opened_left = 1;
+			} else if (space_right >= ideal_w) {
+				x_final = x_right;
+				submenu->was_opened_left = 0;
 			} else {
-				if (space_right >= ideal_w) {
-					x_final = x_right;
-					submenu->was_opened_left = 0;
-				} else if (space_left >= ideal_w) {
-					x_final = x_left;
-					submenu->was_opened_left = 1;
-				} else {
-					x_final = win_w - ideal_w;
-					if (x_final < 0) {
-						x_final = 0;
-						w_final = win_w;
-					}
-					submenu->was_opened_left = 0;
-				}
-			}
-			/* subtract padding and border width so the actual entries are at the right position */
-			y_final = r.y - submenu_theme.pad - submenu_theme.menu_border_width;
-			if (y_final + ideal_h > win_h)
-				y_final = win_h - ideal_h;
-			if (y_final < 0) {
-				y_final = 0;
-				h_final = win_h;
+				x_final = 0;
+				if (win_w < ideal_w)
+					w_final = win_w;
+				submenu->was_opened_left = 1;
 			}
 		} else {
-			int space_top = menu->widget.rect.y;
-			int space_bottom = win_h - (menu->widget.rect.y + menu->widget.rect.h);
-			int y_top = menu->widget.rect.y - ideal_h;
-			int y_bottom = menu->widget.rect.y + menu->widget.rect.h;
-			if (space_top > space_bottom) {
-				y_final = y_top;
-				if (y_final < 0) {
-					y_final = 0;
-					h_final = menu->widget.rect.y;
-				}
+			if (space_right >= ideal_w) {
+				x_final = x_right;
+				submenu->was_opened_left = 0;
+			} else if (space_left >= ideal_w) {
+				x_final = x_left;
+				submenu->was_opened_left = 1;
 			} else {
-				y_final = y_bottom;
-				if (space_bottom < ideal_h)
-					h_final = space_bottom;
+				x_final = win_w - ideal_w;
+				if (x_final < 0) {
+					x_final = 0;
+					w_final = win_w;
+				}
+				submenu->was_opened_left = 0;
 			}
-			/* FIXME: maybe threshold so there's always at least a part of
-			   the menu contents shown (instead of maybe just a few pixels) */
-			/* pathological case where window is way too small */
-			if (h_final <= 0) {
+		}
+		/* subtract padding and border width so the actual entries are at the right position */
+		y_final = entry_rect.y - submenu_theme.pad - submenu_theme.border_width;
+		if (y_final + ideal_h > win_h)
+			y_final = win_h - ideal_h;
+		if (y_final < 0) {
+			y_final = 0;
+			h_final = win_h;
+		}
+	} else {
+		int space_top = menu_rect.y;
+		int space_bottom = win_h - (menu_rect.y + menu_rect.h);
+		int y_top = menu_rect.y - ideal_h;
+		int y_bottom = menu_rect.y + menu_rect.h;
+		if (menu_theme.compress_borders) {
+			y_top += menu_theme.border_width;
+			y_bottom -= menu_theme.border_width;
+		}
+		if (space_top > space_bottom) {
+			y_final = y_top;
+			if (y_final < 0) {
 				y_final = 0;
-				h_final = win_h;
-			}
-			x_final = r.x;
-			if (x_final + ideal_w > win_w)
-				x_final = win_w - ideal_w;
-			if (x_final < 0) {
-				x_final = 0;
-				w_final = win_w;
+				h_final = menu_rect.y;
 			}
+		} else {
+			y_final = y_bottom;
+			if (space_bottom < ideal_h)
+				h_final = space_bottom;
+		}
+		/* FIXME: maybe threshold so there's always at least a part of
+		   the menu contents shown (instead of maybe just a few pixels) */
+		/* pathological case where window is way too small */
+		if (h_final <= 0) {
+			y_final = 0;
+			h_final = win_h;
+		}
+		x_final = entry_rect.x;
+		if (x_final + ideal_w > win_w)
+			x_final = win_w - ideal_w;
+		if (x_final < 0) {
+			x_final = 0;
+			w_final = win_w;
 		}
-		/* reset everything just in case */
-		submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
-		submenu->active_entry = submenu->pressed_entry = SIZE_MAX;
-		submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
-		submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
-		submenu->widget.rect.x = x_final;
-		submenu->widget.rect.y = y_final;
-		submenu->widget.rect.w = w_final;
-		submenu->widget.rect.h = h_final;
-		ltk_surface_cache_request_surface_size(submenu->widget.surface_key, w_final, h_final);
-		submenu->widget.dirty = 1;
-		submenu->widget.hidden = 0;
-		ltk_window_register_popup(menu->widget.window, (ltk_widget *)submenu);
-		ltk_window_invalidate_rect(submenu->widget.window, submenu->widget.rect);
-	}
-}
-
-static void
-unpopup_active_entry(ltk_menu *menu) {
-	if (menu->active_entry >= menu->num_entries)
-		return;
-	ltk_menu *cur_menu = menu->entries[menu->active_entry].submenu;
-	menu->active_entry = SIZE_MAX;
-	while (cur_menu) {
-		ltk_menu *tmp = NULL;
-		if (cur_menu->active_entry < cur_menu->num_entries)
-			tmp = cur_menu->entries[cur_menu->active_entry].submenu;
-		ltk_menu_hide((ltk_widget *)cur_menu);
-		cur_menu = tmp;
 	}
+	/* reset everything just in case */
+	submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
+	submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
+	submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
+	submenu->widget.rect.x = x_final;
+	submenu->widget.rect.y = y_final;
+	submenu->widget.rect.w = w_final;
+	submenu->widget.rect.h = h_final;
+	submenu->widget.dirty = 1;
+	submenu->widget.hidden = 0;
+	submenu->popup_submenus = 0;
+	submenu->unpopup_submenus_on_hide = 1;
+	ltk_menu_resize(&submenu->widget);
+	ltk_window_register_popup(e->widget.window, (ltk_widget *)submenu);
+	ltk_window_invalidate_rect(submenu->widget.window, submenu->widget.rect);
 }
 
 static void
-handle_hover(ltk_menu *menu, int x, int y) {
-	if (set_scroll_timer(menu, x, y) || menu->pressed_entry < menu->num_entries)
-		return;
-	ltk_rect r;
-	size_t idx = get_entry_at_point(menu, x, y, &r);
-	if (idx >= menu->num_entries)
-		return;
-	ltk_menu *cur_submenu = menu->active_entry < menu->num_entries ? menu->entries[menu->active_entry].submenu : NULL;
-	if (idx != menu->active_entry) {
-		unpopup_active_entry(menu);
-		menu->active_entry = idx;
-		menu->widget.dirty = 1;
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-		popup_active_menu(menu, r);
-	} else if (cur_submenu && cur_submenu->widget.hidden) {
-		popup_active_menu(menu, r);
+unpopup_active_entry(ltk_menuentry *e) {
+	if (e->submenu && !e->submenu->widget.hidden) {
+		e->submenu->unpopup_submenus_on_hide = 0;
+		ltk_widget_hide(&e->submenu->widget);
 	}
 }
 
 static int
 ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event) {
-	handle_hover((ltk_menu *)self, event->x, event->y);
+	set_scroll_timer((ltk_menu *)self, event->x, event->y);
 	return 1;
 }
 
 static int
 ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
-	handle_hover((ltk_menu *)self, event->x, event->y);
+	set_scroll_timer((ltk_menu *)self, event->x, event->y);
 	return 1;
 }
 
@@ -846,317 +828,269 @@ ltk_menu_create(ltk_window *window, const char *id, int is_submenu) {
 
 	menu->entries = NULL;
 	menu->num_entries = menu->num_alloc = 0;
-	menu->pressed_entry = menu->active_entry = SIZE_MAX;
 	menu->x_scroll_offset = menu->y_scroll_offset = 0;
 	menu->is_submenu = is_submenu;
 	menu->was_opened_left = 0;
 	menu->scroll_timer_id = -1;
 	menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
 	menu->scroll_left_hover = menu->scroll_right_hover = 0;
+	menu->popup_submenus = 0;
+	menu->unpopup_submenus_on_hide = 1;
 	/* FIXME: hide widget by default so recalc doesn't cause
 	   unnecessary redrawing */
-	recalc_menu_size(menu);
+	recalc_ideal_menu_size(&menu->widget, NULL);
 
 	return menu;
 }
 
-static ltk_menuentry *
-insert_entry(ltk_menu *menu, size_t idx) {
+static int
+insert_entry(ltk_menu *menu, ltk_menuentry *e, size_t idx) {
 	if (idx > menu->num_entries)
-		return NULL;
+		return 1;
 	if (menu->num_entries == menu->num_alloc) {
 		menu->num_alloc = ideal_array_size(menu->num_alloc, menu->num_entries + 1);
-		menu->entries = ltk_reallocarray(menu->entries, menu->num_alloc, sizeof(ltk_menuentry));
+		menu->entries = ltk_reallocarray(menu->entries, menu->num_alloc, sizeof(ltk_menuentry *));
 	}
 	memmove(
 	    menu->entries + idx + 1,
 	    menu->entries + idx,
-	    sizeof(ltk_menuentry) * (menu->num_entries - idx)
+	    sizeof(ltk_menuentry *) * (menu->num_entries - idx)
 	);
-	if (menu->active_entry >= idx && menu->active_entry < menu->num_entries)
-		menu->active_entry++;
-	if (menu->pressed_entry >= idx && menu->pressed_entry < menu->num_entries)
-		menu->pressed_entry++;
 	menu->num_entries++;
-	return &menu->entries[idx];
+	menu->entries[idx] = e;
+	return 0;
 }
 
-/* FIXME: handle child_size_change - what if something added while menu shown?
-   -> I guess the scroll arrows will just be added when that's the case - it's
-      kind of pointless to spend time implementing the logic for recalculating
-      all submenu positions and sizes when it's such a corner case */
 static void
-recalc_menu_size(ltk_menu *menu) {
+recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget) {
+	ltk_menu *menu = (ltk_menu *)self;
+	/* If widget with size change is submenu, it doesn't affect this menu */
+	if (widget && widget->vtable->type == LTK_MENU) {
+		ltk_widget_resize(widget);
+		return;
+	}
 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
-	menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->menu_border_width * 2;
+	struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
+	unsigned int old_w = menu->widget.ideal_w, old_h = menu->widget.ideal_h;
+	menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->border_width * 2;
 	ltk_menuentry *e;
-	int text_w, text_h, bw;
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		e = &menu->entries[i];
-		ltk_text_line_get_size(e->text, &text_w, &text_h);
-		bw = t->border_width * 2;
-		if (t->compress_borders && i != 0)
-			bw = t->border_width;
+		e = menu->entries[i];
 		if (menu->is_submenu) {
-			int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
-			menu->widget.ideal_w =
-			    MAX(text_w + extra_size + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_w);
-			menu->widget.ideal_h += MAX(text_h + t->text_pad * 2, extra_size) + bw + t->pad;
+			menu->widget.ideal_w = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_w, (int)menu->widget.ideal_w);
+			menu->widget.ideal_h += e->widget.ideal_h + t->pad;
+			if (et->compress_borders && i != 0)
+				menu->widget.ideal_h -= et->border_width;
 		} else {
-			menu->widget.ideal_h =
-			    MAX(text_h + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_h);
-			menu->widget.ideal_w += text_w + t->text_pad * 2 + bw + t->pad;
+			menu->widget.ideal_w += e->widget.ideal_w + t->pad;
+			if (et->compress_borders && i != 0)
+				menu->widget.ideal_w -= et->border_width;
+			menu->widget.ideal_h = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_h, (int)menu->widget.ideal_h);
 		}
 	}
-	if (menu->widget.parent && menu->widget.parent->vtable->child_size_change) {
+	if ((old_w != menu->widget.ideal_w || old_h != menu->widget.ideal_h) &&
+	    menu->widget.parent && menu->widget.parent->vtable->child_size_change) {
 		menu->widget.parent->vtable->child_size_change(menu->widget.parent, (ltk_widget *)menu);
+	} else {
+		ltk_menu_resize(self);
 	}
 	menu->widget.dirty = 1;
 	if (!menu->widget.hidden)
 		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
 }
 
+static void
+ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry) {
+	int in_submenu = IN_SUBMENU(entry);
+	struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
+	int extra_size = (in_submenu && entry->submenu) ? t->arrow_pad * 2 + t->arrow_size : 0;
+
+	int text_w, text_h;
+	ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
+	entry->widget.ideal_w = text_w + extra_size + (t->text_pad + t->border_width) * 2;
+	entry->widget.ideal_h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
+	/* FIXME: only call if something changed */
+	if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) {
+		entry->widget.parent->vtable->child_size_change(entry->widget.parent, (ltk_widget *)entry);
+	}
+}
+
 static ltk_menuentry *
-ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
-	if (submenu && submenu->widget.parent) {
-		*errstr = "Submenu already part of other menu.\n";
-		return NULL;
+ltk_menuentry_create(ltk_window *window, const char *id, const char *text) {
+	ltk_menuentry *e = ltk_malloc(sizeof(ltk_menuentry));
+	e->text_line = ltk_text_line_create(window->text_context, window->theme->font_size, (char *)text, 0, -1);
+	e->submenu = NULL;
+	int w, h;
+	ltk_text_line_get_size(e->text_line, &w, &h);
+	e->text_surface_key = ltk_surface_cache_get_unnamed_key(window->surface_cache, w, h);
+	ltk_fill_widget_defaults(&e->widget, id, window, &entry_vtable, 5, 5);
+	/* Note: This is only set as a dummy value! The actual ideal size can't
+	   be set until it is part of a menu because it needs to know which
+	   theme it should use */
+	ltk_menuentry_recalc_ideal_size(e);
+	e->widget.dirty = 1;
+	return e;
+}
+
+static void
+ltk_menuentry_destroy(ltk_widget *self, int shallow) {
+	ltk_menuentry *e = (ltk_menuentry *)self;
+	/* FIXME: should be in widget destroy function */
+	ltk_free(e->widget.id);
+	ltk_text_line_destroy(e->text_line);
+	ltk_surface_cache_release_key(e->text_surface_key);
+	/* FIXME: function to call when parent is destroyed */
+	/* also function to call when parent added */
+	/* also function to call when child destroyed */
+	if (e->submenu) {
+		e->submenu->widget.parent = NULL;
+		if (!shallow) {
+			ltk_menu_destroy(&e->submenu->widget, shallow);
+		}
 	}
-	ltk_menuentry *e = insert_entry(menu, idx);
-	if (!e) {
-		*errstr = "Unable to insert menu entry at given index.\n";
-		return NULL;
+	ltk_free(e);
+}
+
+static void
+ltk_menu_insert_entry(ltk_menu *menu, ltk_menuentry *entry, size_t idx, char **errstr) {
+	if (entry->widget.parent) {
+		*errstr = "Entry already part of other menu.\n";
+		return;
 	}
-	e->id = ltk_strdup(id);
-	ltk_window *w = menu->widget.window;
-	/* FIXME: pass const text */
-	e->text = ltk_text_line_create(w->text_context, w->theme->font_size, (char *)text, 0, -1);
-	e->submenu = submenu;
-	if (submenu)
-		submenu->widget.parent = (ltk_widget *)menu;
-	e->disabled = 0;
-	recalc_menu_size(menu);
+	if (insert_entry(menu, entry, idx)) {
+		*errstr = "Illegal index.\n";
+		return;
+	}
+	entry->widget.parent = &menu->widget;
+	ltk_menuentry_recalc_ideal_size(entry);
+	recalc_ideal_menu_size(&menu->widget, NULL);
 	menu->widget.dirty = 1;
-	return e;
 }
 
-static ltk_menuentry *
-ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
-	return ltk_menu_insert_entry(menu, id, text, submenu, menu->num_entries, errstr);
+static void
+ltk_menu_add_entry(ltk_menu *menu, ltk_menuentry *entry, char **errstr) {
+	ltk_menu_insert_entry(menu, entry, menu->num_entries, errstr);
 }
 
 /* FIXME: maybe allow any menu and just change is_submenu (also need to recalculate size then) */
-static ltk_menuentry *
-ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
+static void
+ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, char **errstr) {
 	if (!submenu->is_submenu) {
 		*errstr = "Not a submenu.\n";
-		return NULL;
+		return;
+	} else if (e->submenu) {
+		*errstr = "Menu entry already has attached submenu.\n";
+		return;
 	}
-	return ltk_menu_insert_entry(menu, id, text, submenu, idx, errstr);
+	e->submenu = submenu;
+	ltk_menuentry_recalc_ideal_size(e);
+	e->widget.dirty = 1;
+	if (submenu) {
+		submenu->widget.hidden = 1;
+		submenu->widget.parent = &e->widget;
+	}
+	if (!e->widget.hidden)
+		ltk_window_invalidate_rect(e->widget.window, e->widget.rect);
 }
 
-static ltk_menuentry *
-ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
-	return ltk_menu_insert_submenu(menu, id, text, submenu, menu->num_entries, errstr);
-}
+/* FIXME: hide all entries when menu hidden? */
 
 static void
 shrink_entries(ltk_menu *menu) {
 	size_t new_alloc = ideal_array_size(menu->num_alloc, menu->num_entries);
 	if (new_alloc != menu->num_alloc) {
-		menu->entries = ltk_reallocarray(menu->entries, new_alloc, sizeof(ltk_menuentry));
+		menu->entries = ltk_reallocarray(menu->entries, new_alloc, sizeof(ltk_menuentry *));
 		menu->num_alloc = new_alloc;
 	}
 }
 
 static int
-ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr) {
+ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
 	if (idx >= menu->num_entries) {
 		*errstr = "Invalid menu entry index.\n";
 		return 1;
 	}
-	ltk_menuentry *e = &menu->entries[idx];
-	ltk_free(e->id);
-	ltk_text_line_destroy(e->text);
-	if (e->submenu) {
-		e->submenu->widget.parent = NULL;
-		if (!shallow)
-			ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
-	}
+	menu->entries[idx]->widget.parent = NULL;
+	ltk_menuentry_recalc_ideal_size(menu->entries[idx]);
 	memmove(
 	    menu->entries + idx,
 	    menu->entries + idx + 1,
-	    sizeof(ltk_menuentry) * (menu->num_entries - idx - 1)
+	    sizeof(ltk_menuentry *) * (menu->num_entries - idx - 1)
 	);
 	menu->num_entries--;
 	shrink_entries(menu);
-	recalc_menu_size(menu);
+	recalc_ideal_menu_size(&menu->widget, NULL);
 	return 0;
 }
 
+static size_t
+get_entry_with_id(ltk_menu *menu, const char *id) {
+	for (size_t i = 0; i < menu->num_entries; i++) {
+		if (!strcmp(id, menu->entries[i]->widget.id))
+			return i;
+	}
+	return SIZE_MAX;
+}
+
 static int
-ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr) {
+ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, char **errstr) {
 	size_t idx = get_entry_with_id(menu, id);
 	if (idx >= menu->num_entries) {
 		*errstr = "Invalid menu entry id.\n";
 		return 1;
 	}
-	ltk_menu_remove_entry_index(menu, idx, shallow, errstr);
+	ltk_menu_remove_entry_index(menu, idx, errstr);
 	return 0;
 }
 
 static int
-ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr) {
-	(void)errstr; /* FIXME: why is errstr given at all? */
+ltk_menu_remove_child(ltk_widget *child, ltk_widget *self, char **errstr) {
+	ltk_menu *menu = (ltk_menu *)self;
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		ltk_menuentry *e = &menu->entries[i];
-		ltk_free(e->id);
-		ltk_text_line_destroy(e->text);
-		if (e->submenu) {
-			e->submenu->widget.parent = NULL;
-			if (!shallow)
-				ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
+		if (&menu->entries[i]->widget == child) {
+			return ltk_menu_remove_entry_index(menu, i, errstr);
 		}
 	}
-	menu->num_entries = menu->num_alloc = 0;
-	ltk_free(menu->entries);
-	menu->entries = NULL;
-	recalc_menu_size(menu);
-	return 0;
+	*errstr = "Widget not contained in menu.\n";
+	return 1;
 }
 
-/* FIXME: how to get rid of duplicate IDs? */
-
-static size_t
-get_entry_with_id(ltk_menu *menu, const char *id) {
+static void
+ltk_menu_remove_all_entries(ltk_menu *menu) {
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		if (!strcmp(id, menu->entries[i].id))
-			return i;
+		menu->entries[i]->widget.parent = NULL;
+		ltk_menuentry_recalc_ideal_size(menu->entries[i]);
 	}
-	return SIZE_MAX;
+	menu->num_entries = menu->num_alloc = 0;
+	ltk_free(menu->entries);
+	menu->entries = NULL;
+	recalc_ideal_menu_size(&menu->widget, NULL);
 }
 
 /* FIXME: unregister from window popups? */
-static int
-ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr) {
-	size_t idx = get_entry_with_id(menu, id);
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry id.\n";
-		return 1;
-	}
-	/* FIXME: error if submenu already NULL? */
-	menu->entries[idx].submenu = NULL;
-	recalc_menu_size(menu);
-	return 0;
-}
-
-static int
-ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry index.\n";
-		return 1;
-	}
-	menu->entries[idx].submenu = NULL;
-	recalc_menu_size(menu);
-	return 0;
-}
-
-static int
-ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry index.\n";
-		return 1;
-	}
-	menu->entries[idx].disabled = 1;
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
-}
-
-static int
-ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
-	size_t idx = get_entry_with_id(menu, id);
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry id.\n";
-		return 1;
-	}
-	menu->entries[idx].disabled = 1;
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
-}
-
-static int
-ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr) {
-	(void)errstr;
-	for (size_t i = 0; i < menu->num_entries; i++) {
-		menu->entries[i].disabled = 1;
-	}
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
-}
-
-static int
-ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry index.\n";
-		return 1;
-	}
-	menu->entries[idx].disabled = 0;
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
-}
-
-static int
-ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
-	size_t idx = get_entry_with_id(menu, id);
-	if (idx >= menu->num_entries) {
-		*errstr = "Invalid menu entry id.\n";
-		return 1;
-	}
-	menu->entries[idx].disabled = 0;
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
-}
-
-static int
-ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr) {
-	(void)errstr;
-	for (size_t i = 0; i < menu->num_entries; i++) {
-		menu->entries[i].disabled = 0;
-	}
-	menu->widget.dirty = 1;
-	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
-	return 0;
+static void
+ltk_menuentry_detach_submenu(ltk_menuentry *e) {
+	if (e->submenu)
+		e->submenu->widget.parent = NULL;
+	e->submenu = NULL;
+	ltk_menuentry_recalc_ideal_size(e);
 }
 
 static void
 ltk_menu_destroy(ltk_widget *self, int shallow) {
 	ltk_menu *menu = (ltk_menu *)self;
-	char *errstr;
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	if (!menu) {
 		ltk_warn("Tried to destroy NULL menu.\n");
 		return;
 	}
-	/* FIXME: this should be generic part of widget */
-	ltk_surface_cache_release_key(self->surface_key);
 	if (menu->scroll_timer_id >= 0)
 		ltk_unregister_timer(menu->scroll_timer_id);
-	ltk_menu_remove_all_entries(menu, shallow, NULL);
+	if (!shallow) {
+		for (size_t i = 0; i < menu->num_entries; i++) {
+			ltk_menuentry_destroy(&menu->entries[i]->widget, shallow);
+		}
+	}
+	ltk_menu_remove_all_entries(menu);
 	ltk_window_unregister_popup(self->window, self);
 	/* FIXME: what to do on error here? */
 	/* FIXME: maybe unregister popup in ltk_remove_widget? */
@@ -1194,7 +1128,7 @@ ltk_menu_cmd_create(
 	return 0;
 }
 
-/* menu <menu id> insert-entry <entry id> <entry text> <index> */
+/* menu <menu id> insert-entry <entry widget id> <index> */
 static int
 ltk_menu_cmd_insert_entry(
     ltk_window *window,
@@ -1203,228 +1137,46 @@ ltk_menu_cmd_insert_entry(
     char **errstr) {
 	(void)window;
 	ltk_menu *menu;
+	ltk_menuentry *e;
 	const char *errstr_num;
-	if (num_tokens != 6) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	/* FIXME: actually use this errstr */
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	size_t idx = (size_t)ltk_strtonum(tokens[5], 0, (long long)menu->num_entries, &errstr_num);
-	if (errstr_num) {
-		*errstr = "Invalid index.\n";
-		return 1;
-	}
-	if (!ltk_menu_insert_entry(menu, tokens[3], tokens[4], NULL, idx, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> add-entry <entry id> <entry text> */
-static int
-ltk_menu_cmd_add_entry(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu;
 	if (num_tokens != 5) {
 		*errstr = "Invalid number of arguments.\n";
 		return 1;
 	}
+	/* FIXME: actually use this errstr */
 	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
 	if (!menu) {
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-	if (!ltk_menu_add_entry(menu, tokens[3], tokens[4], NULL, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> insert-submenu <entry id> <entry text> <submenu id> <index> */
-static int
-ltk_menu_cmd_insert_submenu(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu, *submenu;
-	const char *errstr_num;
-	if (num_tokens != 7) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
-	if (!menu || !submenu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	size_t idx = (size_t)ltk_strtonum(tokens[6], 0, (long long)menu->num_entries, &errstr_num);
-	if (errstr_num) {
-		*errstr = "Invalid index.\n";
-		return 1;
-	}
-	if (!ltk_menu_insert_submenu(menu, tokens[3], tokens[4], submenu, idx, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> add-submenu <entry id> <entry text> <submenu id> */
-static int
-ltk_menu_cmd_add_submenu(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu, *submenu;
-	if (num_tokens != 6) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
-	if (!menu || !submenu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	if (!ltk_menu_add_submenu(menu, tokens[3], tokens[4], submenu, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> remove-entry-index <entry index> [shallow|deep] */
-static int
-ltk_menu_cmd_remove_entry_index(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu;
-	const char *errstr_num;
-	if (num_tokens != 4 && num_tokens != 5) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
+	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_MENUENTRY, errstr);
+	if (!e) {
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-	size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
+	size_t idx = (size_t)ltk_strtonum(tokens[5], 0, (long long)menu->num_entries, &errstr_num);
 	if (errstr_num) {
 		*errstr = "Invalid index.\n";
 		return 1;
 	}
-	int shallow = 1;
-	if (num_tokens == 5) {
-		if (!strcmp(tokens[4], "shallow")) {
-			/* NOP */
-		} else if (!strcmp(tokens[4], "deep")) {
-			shallow = 0;
-		} else {
-			*errstr = "Invalid shallow specifier.\n";
-			return 1;
-		}
-	}
-	if (!ltk_menu_remove_entry_index(menu, idx, shallow, errstr))
+	*errstr = NULL;
+	ltk_menu_insert_entry(menu, e, idx, errstr);
+	if (*errstr)
 		return 1;
 
 	return 0;
 }
 
-/* menu <menu id> remove-entry-id <entry id> [shallow|deep] */
+/* menu <menu id> add-entry <entry widget id> */
 static int
-ltk_menu_cmd_remove_entry_id(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu;
-	if (num_tokens != 4 && num_tokens != 5) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	int shallow = 1;
-	if (num_tokens == 5) {
-		if (!strcmp(tokens[4], "shallow")) {
-			/* NOP */
-		} else if (!strcmp(tokens[4], "deep")) {
-			shallow = 0;
-		} else {
-			*errstr = "Invalid shallow specifier.\n";
-			return 1;
-		}
-	}
-	if (!ltk_menu_remove_entry_id(menu, tokens[3], shallow, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> remove-all-entries [shallow|deep] */
-static int
-ltk_menu_cmd_remove_all_entries(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu;
-	if (num_tokens != 3 && num_tokens != 4) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	int shallow = 1;
-	if (num_tokens == 4) {
-		if (!strcmp(tokens[3], "shallow")) {
-			/* NOP */
-		} else if (!strcmp(tokens[3], "deep")) {
-			shallow = 0;
-		} else {
-			*errstr = "Invalid shallow specifier.\n";
-			return 1;
-		}
-	}
-	if (!ltk_menu_remove_all_entries(menu, shallow, errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> detach-submenu-from-entry-id <entry id> */
-static int
-ltk_menu_cmd_detach_submenu_from_entry_id(
+ltk_menu_cmd_add_entry(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
     char **errstr) {
 	(void)window;
 	ltk_menu *menu;
+	ltk_menuentry *e;
 	if (num_tokens != 4) {
 		*errstr = "Invalid number of arguments.\n";
 		return 1;
@@ -1434,47 +1186,22 @@ ltk_menu_cmd_detach_submenu_from_entry_id(
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-
-	if (!ltk_menu_detach_submenu_from_entry_id(menu, tokens[3], errstr))
-		return 1;
-
-	return 0;
-}
-
-/* menu <menu id> detach-submenu-from-entry-index <entry index> */
-static int
-ltk_menu_cmd_detach_submenu_from_entry_index(
-    ltk_window *window,
-    char **tokens,
-    size_t num_tokens,
-    char **errstr) {
-	(void)window;
-	ltk_menu *menu;
-	const char *errstr_num;
-	if (num_tokens != 4) {
-		*errstr = "Invalid number of arguments.\n";
-		return 1;
-	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
+	e = (ltk_menuentry *)ltk_get_widget(tokens[3], LTK_MENUENTRY, errstr);
+	if (!e) {
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-	size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
-	if (errstr_num) {
-		*errstr = "Invalid index.\n";
-		return 1;
-	}
-
-	if (!ltk_menu_detach_submenu_from_entry_index(menu, idx, errstr))
+	*errstr = NULL;
+	ltk_menu_add_entry(menu, e, errstr);
+	if (*errstr)
 		return 1;
 
 	return 0;
 }
 
-/* menu <menu id> enable-entry-index <entry index> */
+/* menu <menu id> remove-entry-index <entry index> */
 static int
-ltk_menu_cmd_enable_entry_index(
+ltk_menu_cmd_remove_entry_index(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
@@ -1496,16 +1223,15 @@ ltk_menu_cmd_enable_entry_index(
 		*errstr = "Invalid index.\n";
 		return 1;
 	}
-
-	if (!ltk_menu_enable_entry_index(menu, idx, errstr))
+	if (!ltk_menu_remove_entry_index(menu, idx, errstr))
 		return 1;
 
 	return 0;
 }
 
-/* menu <menu id> enable-entry-id <entry id> */
+/* menu <menu id> remove-entry-id <entry id> */
 static int
-ltk_menu_cmd_enable_entry_id(
+ltk_menu_cmd_remove_entry_id(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
@@ -1521,16 +1247,15 @@ ltk_menu_cmd_enable_entry_id(
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-
-	if (!ltk_menu_enable_entry_id(menu, tokens[3], errstr))
+	if (!ltk_menu_remove_entry_id(menu, tokens[3], errstr))
 		return 1;
 
 	return 0;
 }
 
-/* menu <menu id> enable-all-entries */
+/* menu <menu id> remove-all-entries */
 static int
-ltk_menu_cmd_enable_all_entries(
+ltk_menu_cmd_remove_all_entries(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
@@ -1546,97 +1271,94 @@ ltk_menu_cmd_enable_all_entries(
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
-
-	if (!ltk_menu_enable_all_entries(menu, errstr))
-		return 1;
+	ltk_menu_remove_all_entries(menu);
 
 	return 0;
 }
 
-/* menu <menu id> disable-entry-index <entry index> */
+/* menuentry <id> create <text> */
 static int
-ltk_menu_cmd_disable_entry_index(
+ltk_menuentry_cmd_create(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
     char **errstr) {
-	(void)window;
-	ltk_menu *menu;
-	const char *errstr_num;
+	ltk_menuentry *e;
 	if (num_tokens != 4) {
 		*errstr = "Invalid number of arguments.\n";
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
-		*errstr = "Invalid widget ID.\n";
-		return 1;
-	}
-	size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
-	if (errstr_num) {
-		*errstr = "Invalid index.\n";
+	if (!ltk_widget_id_free(tokens[1])) {
+		*errstr = "Widget ID already taken.\n";
 		return 1;
 	}
-
-	if (!ltk_menu_disable_entry_index(menu, idx, errstr))
-		return 1;
+	e = ltk_menuentry_create(window, tokens[1], tokens[3]);
+	ltk_set_widget((ltk_widget *)e, tokens[1]);
 
 	return 0;
 }
 
-/* menu <menu id> disable-entry-id <entry id> */
+/* menuentry <menuentry id> attach-submenu <submenu id> */
 static int
-ltk_menu_cmd_disable_entry_id(
+ltk_menuentry_cmd_attach_submenu(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
     char **errstr) {
 	(void)window;
-	ltk_menu *menu;
+	ltk_menuentry *e;
+	ltk_menu *submenu;
 	if (num_tokens != 4) {
 		*errstr = "Invalid number of arguments.\n";
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
-		*errstr = "Invalid widget ID.\n";
+	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_MENUENTRY, errstr);
+	if (!e) {
+		*errstr = "Invalid entry widget ID.\n";
+		return 1;
+	}
+	submenu = (ltk_menu *)ltk_get_widget(tokens[3], LTK_MENU, errstr);
+	if (!submenu) {
+		*errstr = "Invalid submenu widget ID.\n";
 		return 1;
 	}
 
-	if (!ltk_menu_disable_entry_id(menu, tokens[3], errstr))
+	*errstr = NULL;
+	ltk_menuentry_attach_submenu(e, submenu, errstr);
+	if (*errstr)
 		return 1;
 
 	return 0;
 }
 
-/* menu <menu id> disable-all-entries */
+/* menuentry <menuentry id> detach-submenu */
 static int
-ltk_menu_cmd_disable_all_entries(
+ltk_menuentry_cmd_detach_submenu(
     ltk_window *window,
     char **tokens,
     size_t num_tokens,
     char **errstr) {
 	(void)window;
-	ltk_menu *menu;
+	ltk_menuentry *e;
 	if (num_tokens != 3) {
 		*errstr = "Invalid number of arguments.\n";
 		return 1;
 	}
-	menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
-	if (!menu) {
+	e = (ltk_menuentry *)ltk_get_widget(tokens[1], LTK_MENUENTRY, errstr);
+	if (!e) {
 		*errstr = "Invalid widget ID.\n";
 		return 1;
 	}
 
-	if (!ltk_menu_disable_all_entries(menu, errstr))
-		return 1;
+	ltk_menuentry_detach_submenu(e);
 
 	return 0;
 }
 
+/* FIXME: sort out menu/submenu - it's weird right now */
 /* FIXME: binary search for command handler */
 /* FIXME: distinguish between menu/submenu in commands other than create? */
-/* menu <menu id> <command> ... */
+/* [sub]menu <menu id> <command> ... */
 int
 ltk_menu_cmd(
     ltk_window *window,
@@ -1653,32 +1375,37 @@ ltk_menu_cmd(
 		return ltk_menu_cmd_insert_entry(window, tokens, num_tokens, errstr);
 	} else if (strcmp(tokens[2], "add-entry") == 0) {
 		return ltk_menu_cmd_add_entry(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "insert-submenu") == 0) {
-		return ltk_menu_cmd_insert_submenu(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "add-submenu") == 0) {
-		return ltk_menu_cmd_add_submenu(window, tokens, num_tokens, errstr);
 	} else if (strcmp(tokens[2], "remove-entry-index") == 0) {
 		return ltk_menu_cmd_remove_entry_index(window, tokens, num_tokens, errstr);
 	} else if (strcmp(tokens[2], "remove-entry-id") == 0) {
 		return ltk_menu_cmd_remove_entry_id(window, tokens, num_tokens, errstr);
 	} else if (strcmp(tokens[2], "remove-all-entries") == 0) {
 		return ltk_menu_cmd_remove_all_entries(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "detach-submenu-from-entry-id") == 0) {
-		return ltk_menu_cmd_detach_submenu_from_entry_id(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "detach-submenu-from-entry-index") == 0) {
-		return ltk_menu_cmd_detach_submenu_from_entry_index(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "disable-entry-index") == 0) {
-		return ltk_menu_cmd_disable_entry_index(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "disable-entry-id") == 0) {
-		return ltk_menu_cmd_disable_entry_id(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "disable-all-entries") == 0) {
-		return ltk_menu_cmd_disable_all_entries(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "enable-entry-index") == 0) {
-		return ltk_menu_cmd_enable_entry_index(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "enable-entry-id") == 0) {
-		return ltk_menu_cmd_enable_entry_id(window, tokens, num_tokens, errstr);
-	} else if (strcmp(tokens[2], "enable-all-entries") == 0) {
-		return ltk_menu_cmd_enable_all_entries(window, tokens, num_tokens, errstr);
+	} else {
+		*errstr = "Invalid command.\n";
+		return 1;
+	}
+
+	return 0;
+}
+
+/* menuentry <menuentry id> <command> ... */
+int
+ltk_menuentry_cmd(
+    ltk_window *window,
+    char **tokens,
+    size_t num_tokens,
+    char **errstr) {
+	if (num_tokens < 3) {
+		*errstr = "Invalid number of arguments.\n";
+		return 1;
+	}
+	if (strcmp(tokens[2], "create") == 0) {
+		return ltk_menuentry_cmd_create(window, tokens, num_tokens, errstr);
+	} else if (strcmp(tokens[2], "attach-submenu") == 0) {
+		return ltk_menuentry_cmd_attach_submenu(window, tokens, num_tokens, errstr);
+	} else if (strcmp(tokens[2], "detach-submenu") == 0) {
+		return ltk_menuentry_cmd_detach_submenu(window, tokens, num_tokens, errstr);
 	} else {
 		*errstr = "Invalid command.\n";
 		return 1;
diff --git a/src/menu.h b/src/menu.h
@@ -21,33 +21,37 @@
 #include "text.h"
 #include "widget.h"
 
-/* TODO: implement scrolling */
-
 typedef struct ltk_menuentry ltk_menuentry;
 
 typedef struct {
 	ltk_widget widget;
-	ltk_menuentry *entries;
+	ltk_menuentry **entries;
 	size_t num_entries;
 	size_t num_alloc;
-	size_t pressed_entry;
-	size_t active_entry;
 	double x_scroll_offset;
 	double y_scroll_offset;
 	int scroll_timer_id;
 	char is_submenu;
 	char was_opened_left;
+	/* FIXME: better names */
+	char popup_submenus;
+	char unpopup_submenus_on_hide;
 	char scroll_top_hover;
 	char scroll_bottom_hover;
 	char scroll_left_hover;
 	char scroll_right_hover;
 } ltk_menu;
 
+/* FIXME: maybe need to set entire widget hierarchy to hover state so menu entry
+   is also hover when nested widget is hover? */
+
 struct ltk_menuentry {
-	char *id;
-	ltk_text_line *text;
+	ltk_widget widget;
+	/* FIXME: I guess if the regular label got the ability to
+	   change its color, a label could just be used instead of this */
+	ltk_text_line *text_line;
+	ltk_surface_cache_key *text_surface_key;
 	ltk_menu *submenu;
-	int disabled;
 };
 
 int ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value);
@@ -57,6 +61,13 @@ int ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *va
 int ltk_submenu_fill_theme_defaults(ltk_window *window);
 void ltk_submenu_uninitialize_theme(ltk_window *window);
 
+int ltk_menuentry_ini_handler(ltk_window *window, const char *prop, const char *value);
+int ltk_menuentry_fill_theme_defaults(ltk_window *window);
+void ltk_menuentry_uninitialize_theme(ltk_window *window);
+int ltk_submenuentry_ini_handler(ltk_window *window, const char *prop, const char *value);
+int ltk_submenuentry_fill_theme_defaults(ltk_window *window);
+void ltk_submenuentry_uninitialize_theme(ltk_window *window);
+
 int ltk_menu_cmd(
 	ltk_window *window,
 	char **tokens,
@@ -64,4 +75,11 @@ int ltk_menu_cmd(
 	char **errstr
 );
 
+int ltk_menuentry_cmd(
+	ltk_window *window,
+	char **tokens,
+	size_t num_tokens,
+	char **errstr
+);
+
 #endif /* _LTK_MENU_H_ */
diff --git a/src/rect.h b/src/rect.h
@@ -14,8 +14,13 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef _LTK_RECT_H_
-#define _LTK_RECT_H_
+#ifndef LTK_RECT_H
+#define LTK_RECT_H
+
+/* FIXME: X only supports 16-bit numbers */
+typedef struct {
+	int x, y;
+} ltk_point;
 
 typedef struct {
 	int x;
@@ -29,4 +34,4 @@ ltk_rect ltk_rect_relative(ltk_rect parent, ltk_rect child);
 ltk_rect ltk_rect_union(ltk_rect r1, ltk_rect r2);
 int ltk_collide_rect(ltk_rect rect, int x, int y);
 
-#endif /* _LTK_RECT_H_ */
+#endif /* LTK_RECT_H */
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -53,7 +53,8 @@ static struct ltk_widget_vtable vtable = {
 	.child_size_change = NULL,
 	.remove_child = NULL,
 	.type = LTK_UNKNOWN, /* FIXME */
-	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE,
+	/* FIXME: need different activatable state so arrow keys don't move onto scrollbar */
+	.flags = LTK_NEEDS_REDRAW | LTK_NEEDS_SURFACE | LTK_ACTIVATABLE_ALWAYS,
 };
 
 static struct {
@@ -131,27 +132,19 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 	ltk_scrollbar *scrollbar = (ltk_scrollbar *)self;
 	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;
-		break;
-	case LTK_PRESSED:
+	if (self->state & LTK_DISABLED) {
+		bg = &theme.bg_disabled;
+		fg = &theme.fg_disabled;
+	} else if (self->state & LTK_PRESSED) {
 		bg = &theme.bg_normal;
 		fg = &theme.fg_pressed;
-		break;
-	case LTK_HOVER:
-	case LTK_ACTIVE:
+	} else if (self->state & LTK_HOVERACTIVE) {
 		bg = &theme.bg_normal;
 		fg = &theme.fg_active;
-		break;
-	case LTK_DISABLED:
-		bg = &theme.bg_disabled;
-		fg = &theme.fg_disabled;
-		break;
-	default:
-		ltk_fatal("No style found for current scrollbar state.\n");
+	} else {
+		bg = &theme.bg_normal;
+		fg = &theme.fg_normal;
 	}
 	ltk_surface *s;
 	ltk_surface_cache_get_surface(self->surface_key, &s);
@@ -221,7 +214,7 @@ static int
 ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event) {
 	ltk_scrollbar *sc = (ltk_scrollbar *)self;
 	int delta;
-	if (self->state != LTK_PRESSED) {
+	if (!(self->state & LTK_PRESSED)) {
 		return 1;
 	}
 	if (sc->orient == LTK_HORIZONTAL)
@@ -256,12 +249,6 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
 static void
 ltk_scrollbar_destroy(ltk_widget *self, int shallow) {
 	(void)shallow;
-	char *errstr;
-	if (self->parent && self->parent->vtable->remove_child) {
-		self->parent->vtable->remove_child(
-		    self->window, self, self->parent, &errstr
-		);
-	}
 	ltk_surface_cache_release_key(self->surface_key);
 	ltk_free(self);
 }
diff --git a/src/widget.c b/src/widget.c
@@ -1,10 +1,5 @@
 /* FIXME: store coordinates relative to parent widget */
 /* FIXME: Destroy function for widget to destroy pixmap! */
-/* FIXME/NOTE: maybe it would be better to do some sort of
-   inheritance where the generic widget destroy function is
-   called before the specific function for each widget type
-   so each widget doesn't have to manually remove itself from
-   its parent */
 /*
  * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
  *
@@ -47,11 +42,12 @@ ltk_destroy_widget_hash(void) {
 	hash_locked = 1;
 	khint_t k;
 	ltk_widget *ptr;
+	char *errstr;
 	for (k = kh_begin(widget_hash); k != kh_end(widget_hash); k++) {
 		if (kh_exist(widget_hash, k)) {
 			ptr = kh_value(widget_hash, k);
 			ltk_free((char *)kh_key(widget_hash, k));
-			ptr->vtable->destroy(ptr, 1);
+			ltk_widget_destroy(ptr, 1, &errstr);
 		}
 	}
 	kh_destroy(widget, widget_hash);
@@ -105,6 +101,47 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
 	widget->hidden = 0;
 }
 
+void
+ltk_widget_hide(ltk_widget *widget) {
+	if (widget->vtable->hide)
+		widget->vtable->hide(widget);
+	widget->hidden = 1;
+	/* remove hover state */
+	/* FIXME: this needs to call change_state but that might cause issues */
+	ltk_widget *hover = widget->window->hover_widget;
+	while (hover) {
+		if (hover == widget) {
+			widget->window->hover_widget->state &= ~LTK_HOVER;
+			widget->window->hover_widget = NULL;
+			break;
+		}
+		hover = hover->parent;
+	}
+	ltk_widget *pressed = widget->window->pressed_widget;
+	while (pressed) {
+		if (pressed == widget) {
+			widget->window->pressed_widget->state &= ~LTK_PRESSED;
+			widget->window->pressed_widget = NULL;
+			break;
+		}
+		pressed = pressed->parent;
+	}
+	ltk_widget *active = widget->window->active_widget;
+	/* if current active widget is child, set active widget to widget above in hierarchy */
+	int set_next = 0;
+	while (active) {
+		if (active == widget) {
+			set_next = 1;
+		} else if (set_next && (active->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+			ltk_window_set_active_widget(active->window, active);
+			break;
+		}
+		active = active->parent;
+	}
+	if (set_next && !active)
+		ltk_window_set_active_widget(active->window, NULL);
+}
+
 /* FIXME: Maybe pass the new width as arg here?
    That would make a bit more sense */
 void
@@ -112,18 +149,20 @@ ltk_widget_resize(ltk_widget *widget) {
 	/* FIXME: should surface maybe be resized first? */
 	if (widget->vtable->resize)
 		widget->vtable->resize(widget);
-	if (!widget->vtable->flags & LTK_NEEDS_SURFACE)
+	if (!(widget->vtable->flags & LTK_NEEDS_SURFACE))
 		return;
 	ltk_surface_cache_request_surface_size(widget->surface_key, widget->rect.w, widget->rect.h);
 	widget->dirty = 1;
 }
 
 void
-ltk_widget_change_state(ltk_widget *widget) {
+ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
 	if (widget->vtable->change_state)
-		widget->vtable->change_state(widget);
-	if (widget->vtable->flags & LTK_NEEDS_REDRAW)
+		widget->vtable->change_state(widget, old_state);
+	if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
+		widget->dirty = 1;
 		ltk_window_invalidate_rect(widget->window, widget->rect);
+	}
 }
 
 static ltk_widget *
@@ -148,25 +187,51 @@ get_hover_popup(ltk_window *window, int x, int y) {
 	return NULL;
 }
 
+static int
+is_parent(ltk_widget *parent, ltk_widget *child) {
+	while (child && child != parent) {
+		child = child->parent;
+	}
+	return child != NULL;
+}
+
 /* FIXME: This is still weird. */
 void
 ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	int check_hide = 0;
 	if (!widget) {
-		ltk_window_unregister_all_popups(window);
 		widget = window->root_widget;
+		check_hide = 1;
 	}
-	if (!widget)
+	if (!widget) {
+		ltk_window_unregister_all_popups(window);
 		return;
+	}
 	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y);
+	/* FIXME: need to add more flags for more fine-grained control
+	   -> also, should the widget still get mouse_press even if state doesn't change? */
+	if (!(cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+		ltk_window_unregister_all_popups(window);
+	}
+
+	/* FIXME: this doesn't make much sense if the popups aren't a
+	   hierarchy (right now, they're just menus, so that's always
+	   a hierarchy */
+	/* don't hide popups if they are children of the now pressed widget */
+	if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
+		ltk_window_unregister_all_popups(window);
+
 	int first = 1;
 	while (cur_widget) {
 		int handled = 0;
 		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 (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) {
+			if (first && event->button == LTK_BUTTONL && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
 				ltk_window_set_pressed_widget(window, cur_widget);
 				first = 0;
 			}
@@ -179,17 +244,26 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 }
 
 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);
+}
+
+void
 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;
+	ltk_widget *widget = window->pressed_widget;
+	if (!widget) {
+		widget = get_hover_popup(window, event->x, event->y);
+		widget = get_widget_under_pointer(widget, event->x, event->y);
+	}
+	/* FIXME: loop up to top of hierarchy if not handled */
 	if (widget && widget->vtable->mouse_release)
 		widget->vtable->mouse_release(widget, event);
 	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);
+		/* FIXME: only when not collide with rect */
+		ltk_window_fake_motion_event(window, event->x, event->y);
 	}
 }
 
@@ -221,7 +295,7 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *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) {
+			if (first && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
 				ltk_window_set_hover_widget(window, cur_widget, event);
 				first = 0;
 			}
@@ -231,6 +305,8 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 		else
 			break;
 	}
+	if (first)
+		ltk_window_set_hover_widget(window, NULL, event);
 }
 
 int
@@ -287,10 +363,9 @@ ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr) {
 	/* widget->parent->remove_child should never be NULL because of the fact that
 	   the widget is set as parent, but let's just check anyways... */
 	int err = 0;
-	/* FIXME: why is window passed here? */
 	if (widget->parent && widget->parent->vtable->remove_child) {
 		err = widget->parent->vtable->remove_child(
-		    widget->window, widget, widget->parent, errstr
+		    widget, widget->parent, errstr
 		);
 	}
 	widget->vtable->destroy(widget, shallow);
diff --git a/src/widget.h b/src/widget.h
@@ -14,8 +14,8 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef _LTK_WIDGET_H_
-#define _LTK_WIDGET_H_
+#ifndef LTK_WIDGET_H
+#define LTK_WIDGET_H
 
 #include "rect.h"
 #include "event.h"
@@ -31,6 +31,7 @@ typedef enum {
 	LTK_GRABS_INPUT = 4,
 	LTK_NEEDS_REDRAW = 8,
 	LTK_NEEDS_SURFACE = 16, /* FIXME: let widgets handle this themselves */
+	LTK_HOVER_IS_ACTIVE = 32,
 } ltk_widget_flags;
 
 typedef enum {
@@ -46,11 +47,13 @@ typedef enum {
 } ltk_orientation;
 
 typedef enum {
-	LTK_NORMAL,
-	LTK_HOVER,
-	LTK_PRESSED,
-	LTK_ACTIVE,
-	LTK_DISABLED
+	/* FIXME: maybe remove normal or set it to 0 */
+	LTK_NORMAL = 1,
+	LTK_HOVER = 2,
+	LTK_PRESSED = 4,
+	LTK_ACTIVE = 8,
+	LTK_HOVERACTIVE = 2 | 8,
+	LTK_DISABLED = 16,
 } ltk_widget_state;
 
 typedef enum {
@@ -63,6 +66,7 @@ typedef enum {
 	LTK_WIDGET,
 	LTK_BOX,
 	LTK_MENU,
+	LTK_MENUENTRY,
 	LTK_NUM_WIDGETS
 } ltk_widget_type;
 
@@ -97,19 +101,19 @@ struct ltk_widget {
 /* FIXME: just give the structs for the actual event type here instead
    of the generic ltk_event */
 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_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 *);
-	void (*draw) (struct ltk_widget *, ltk_rect);
-	void (*change_state) (struct ltk_widget *);
-	void (*destroy) (struct ltk_widget *, int);
+	void (*key_press)(struct ltk_widget *, ltk_event *);
+	void (*key_release)(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 *);
+	void (*draw)(struct ltk_widget *, ltk_rect);
+	void (*change_state)(struct ltk_widget *, ltk_widget_state);
+	void (*destroy)(struct ltk_widget *, int);
 
 	struct ltk_widget *(*nearest_child)(struct ltk_widget *self, ltk_rect rect);
 	struct ltk_widget *(*nearest_child_left)(struct ltk_widget *self, ltk_rect rect);
@@ -120,33 +124,25 @@ struct ltk_widget_vtable {
 	struct ltk_widget *(*prev_child)(struct ltk_widget *self, ltk_widget *child);
 	struct ltk_widget *(*first_child)(struct ltk_widget *self);
 
-	/* FIXME: make menu entries actual widgets so this isn't needed anymore */
-	int (*move_active_left)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_right)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_above)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_below)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_next)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_prev)(struct ltk_widget *self, ltk_rect *rect_ret);
-	int (*move_active_first)(struct ltk_widget *self, ltk_rect *rect_ret);
-
 	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 **);
+	int (*remove_child)(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;
 	ltk_widget_flags flags;
 };
 
+void ltk_widget_hide(ltk_widget *widget);
 int ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr);
 int ltk_widget_destroy_cmd(struct ltk_window *window, char **tokens, size_t num_tokens, char **errstr);
 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_change_state(ltk_widget *widget, ltk_widget_state old_state);
 /* 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);
+void ltk_window_fake_motion_event(ltk_window *window, int x, int y);
 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);
@@ -155,4 +151,4 @@ void ltk_widgets_cleanup();
 void ltk_widgets_init();
 void ltk_widget_resize(ltk_widget *widget);
 
-#endif /* _LTK_WIDGET_H_ */
+#endif /* LTK_WIDGET_H */
diff --git a/test2.gui b/test2.gui
@@ -3,23 +3,42 @@ grid grd1 set-row-weight 1 1
 grid grd1 set-column-weight 0 1
 set-root-widget grd1
 menu menu1 create
-menu menu1 add-entry entry1 "Menu Entry"
-menu menu1 add-entry entrya1 "Menu Entry 2"
+menuentry entry1 create "Entry 1"
+menuentry entry2 create "Entry 2"
+menuentry entry3 create "Entry 3"
+menuentry entry4 create "Entry 4"
+menuentry entry5 create "Entry 5"
+menuentry entry6 create "Entry 6"
+menuentry entry7 create "Entry 7"
+menuentry entry8 create "Entry 8"
+menuentry entry9 create "Entry 9"
+menuentry entry10 create "Entry 10"
+menuentry entry11 create "Entry 11"
+menuentry entry12 create "Entry 12"
+menuentry entry13 create "Entry 13"
+menuentry entry14 create "Entry 14"
+menuentry entry15 create "Entry 15"
+menuentry entry16 create "Entry 16"
+menu menu1 add-entry entry1
+menu menu1 add-entry entry2
+menu menu1 add-entry entry12
 submenu submenu1 create
-menu submenu1 add-entry entry2 "Submenu Entry 1"
-menu submenu1 add-entry entry6 "Submenu Entry 2"
-menu submenu1 add-entry entry7 "Submenu Entry 3"
-menu submenu1 add-entry entry8 "Submenu Entry 4"
-menu submenu1 add-entry entry9 "Submenu Entry 5"
-menu submenu1 add-entry entry10 "Submenu Entry 6"
-menu submenu1 add-entry entry11 "Submenu Entry 7"
-menu submenu1 add-entry entry12 "Submenu Entry 8"
-menu submenu1 add-entry entry13 "Submenu Entry 9"
-menu menu1 add-submenu entry3 "Submenu" submenu1
+menu submenu1 add-entry entry3
+menu submenu1 add-entry entry4
+menu submenu1 add-entry entry5
+menu submenu1 add-entry entry6
+menu submenu1 add-entry entry7
+menu submenu1 add-entry entry8
+menu submenu1 add-entry entry9
+menu submenu1 add-entry entry10
+menu submenu1 add-entry entry11
+menuentry entry12 attach-submenu submenu1
 submenu submenu2 create
-menu submenu2 add-entry entry4 "Submenu Entry"
-menu submenu1 add-submenu entry5 "Submenu" submenu2
+menu submenu2 add-entry entry13
+menu submenu2 add-entry entry15
+menu submenu1 add-entry entry14
+menuentry entry14 attach-submenu submenu2
 submenu submenu3 create
-menu submenu3 add-entry entrya3 "Submenu Entry"
-menu submenu2 add-submenu entrya2 "Submenu" submenu3
-grid grd1 add menu1 0 0 1 1 w
+menu submenu3 add-entry entry16
+menuentry entry15 attach-submenu submenu3
+grid grd1 add menu1 0 0 1 1 ew