commit 6b058581e1cb02dd6d4775d8ab3f008bcc87829c
parent 610c7a836cb111096f1ae31d5190a2964ba25310
Author: lumidify <nobody@lumidify.org>
Date:   Sat, 23 Jul 2022 13:08:01 +0200
Implement key events and mappings
It's probably still very buggy.
Diffstat:
34 files changed, 2850 insertions(+), 384 deletions(-)
diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
@@ -0,0 +1,65 @@
+[general]
+explicit-focus = true
+all-activatable = true
+# In future:
+# text-editor = ...
+# line-editor = ...
+
+[key-binding]
+# In future:
+# bind edit-text-external ...
+# bind edit-line-external ...
+bind-keypress move-next sym tab
+bind-keypress move-prev sym tab mods shift
+bind-keypress move-next text n
+bind-keypress move-prev text p
+bind-keypress move-left sym left
+bind-keypress move-right sym right
+bind-keypress move-up sym up
+bind-keypress move-down sym down
+bind-keypress move-left text h
+bind-keypress move-right text l
+bind-keypress move-up text k
+bind-keypress move-down text j
+bind-keypress focus-active sym return
+# FIXME: Test that this works properly once widgets that need
+# focus are added - remove-popups should be called if escape
+# wasn't handled already by unfocus-active.
+bind-keypress unfocus-active sym escape
+bind-keypress remove-popups sym escape
+bind-keypress set-pressed sym return #flags run-always
+bind-keyrelease unset-pressed sym return #flags run-always
+# alternative: rawtext instead of text to ignore mapping
+
+# default mapping (just to silence warnings)
+[key-mapping]
+language = "English (US)"
+
+[key-mapping]
+language = "German"
+map "z" "y"
+map "y" "z"
+map "Z" "Y"
+map "Y" "Z"
+map "Ö" ":"
+map "_" "?"
+map "-" "/"
+map "ä" "'"
+
+[key-mapping]
+language = "Urdu (Pakistan)"
+map "ج" "j"
+map "ک" "k"
+map "ح" "h"
+map "ل" "l"
+map "ن" "n"
+map "پ" "p"
+
+[key-mapping]
+language = "Hindi (Bolnagri)"
+map "ज" "j"
+map "क" "k"
+map "ह" "h"
+map "ल" "l"
+map "न" "n"
+map "प" "p"
diff --git a/.ltk/theme.ini b/.ltk/theme.ini
@@ -1,6 +1,5 @@
 [window]
 font-size = 15
-border-width = 0
 bg = #000000
 fg = #FFFFFF
 font = Liberation Mono
diff --git a/Makefile b/Makefile
@@ -8,6 +8,7 @@ VERSION = -999-prealpha0
 # FIXME: Using DEBUG here doesn't work because it somehow
 # interferes with a predefined macro, at least on OpenBSD.
 DEV = 0
+MEMDEBUG = 0
 SANITIZE = 0
 USE_PANGO = 0
 
@@ -15,7 +16,7 @@ USE_PANGO = 0
 
 # debug
 DEV_CFLAGS_1 = -g -Wall -Wextra -pedantic
-SANITIZE_FLAGS_1 = -fsanitize=address
+SANITIZE_FLAGS_1 = -g -fsanitize=address
 # don't include default flags when debugging so possible
 # optimization flags don't interfere with it
 DEV_CFLAGS_0 = $(CFLAGS)
@@ -33,7 +34,7 @@ EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
 EXTRA_CFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
 EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
 
-LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
+LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
 LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext`
 
 OBJ = \
@@ -55,7 +56,8 @@ OBJ = \
 	src/graphics_xlib.o \
 	src/surface_cache.o \
 	src/event_xlib.o \
-	src/err.c \
+	src/err.o \
+	src/config.o \
 	$(EXTRA_OBJ)
 
 # Note: This could be improved so a change in a header only causes the .c files
@@ -83,9 +85,12 @@ HDR = \
 	src/surface_cache.h \
 	src/macros.h \
 	src/event.h \
+	src/eventdefs.h \
 	src/xlib_shared.h \
 	src/err.h \
-	src/proto_types.h
+	src/proto_types.h \
+	src/config.h \
+	src/widget_config.h
 
 all: src/ltkd src/ltkc
 
diff --git a/src/box.c b/src/box.c
@@ -30,7 +30,7 @@
 #include "scrollbar.h"
 #include "box.h"
 
-static void ltk_box_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
 static ltk_box *ltk_box_create(ltk_window *window, const char *id, ltk_orientation orient);
 static void ltk_box_destroy(ltk_widget *self, int shallow);
 static void ltk_recalculate_box(ltk_widget *self);
@@ -42,6 +42,18 @@ static int ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err);
 static void ltk_box_scroll(ltk_widget *self);
 static int ltk_box_mouse_press(ltk_widget *self, ltk_button_event *event);
 static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
+static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
+
+static ltk_widget *ltk_box_prev_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_box_next_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_box_first_child(ltk_widget *self);
+static ltk_widget *ltk_box_last_child(ltk_widget *self);
+
+static ltk_widget *ltk_box_nearest_child(ltk_widget *self, ltk_rect rect);
+static ltk_widget *ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget);
 
 static struct ltk_widget_vtable vtable = {
 	.change_state = NULL,
@@ -59,6 +71,16 @@ static struct ltk_widget_vtable vtable = {
 	.get_child_at_pos = <k_box_get_child_at_pos,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
+	.prev_child = <k_box_prev_child,
+	.next_child = <k_box_next_child,
+	.first_child = <k_box_first_child,
+	.last_child = <k_box_last_child,
+	.nearest_child = <k_box_nearest_child,
+	.nearest_child_left = <k_box_nearest_child_left,
+	.nearest_child_right = <k_box_nearest_child_right,
+	.nearest_child_above = <k_box_nearest_child_above,
+	.nearest_child_below = <k_box_nearest_child_below,
+	.ensure_rect_shown = <k_box_ensure_rect_shown,
 	.type = LTK_WIDGET_BOX,
 	.flags = 0,
 };
@@ -87,17 +109,22 @@ static int ltk_box_cmd_create(
     ltk_error *err);
 
 static void
-ltk_box_draw(ltk_widget *self, ltk_rect clip) {
+ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
 	ltk_box *box = (ltk_box *)self;
 	ltk_widget *ptr;
-	ltk_rect real_clip = ltk_rect_intersect(box->widget.rect, clip);
+	ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
 	for (size_t i = 0; i < box->num_widgets; i++) {
 		ptr = box->widgets[i];
 		/* FIXME: Maybe continue immediately if widget is
 		   obviously outside of clipping rect */
-		ptr->vtable->draw(ptr, real_clip);
+		ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
 	}
-	box->sc->widget.vtable->draw((ltk_widget *)box->sc, real_clip);
+	box->sc->widget.vtable->draw(
+	    (ltk_widget *)box->sc, s,
+	    x + box->sc->widget.lrect.x,
+	    y + box->sc->widget.lrect.y,
+	    ltk_rect_relative(box->sc->widget.lrect, real_clip)
+	);
 }
 
 static ltk_box *
@@ -107,28 +134,55 @@ ltk_box_create(ltk_window *window, const char *id, ltk_orientation orient) {
 	ltk_fill_widget_defaults(&box->widget, id, window, &vtable, 0, 0);
 
 	box->sc = ltk_scrollbar_create(window, orient, <k_box_scroll, box);
+	box->sc->widget.parent = &box->widget;
 	box->widgets = NULL;
 	box->num_alloc = 0;
 	box->num_widgets = 0;
 	box->orient = orient;
+	if (orient == LTK_HORIZONTAL)
+		box->widget.ideal_h = box->sc->widget.ideal_h;
+	else
+		box->widget.ideal_w = box->sc->widget.ideal_w;
+	ltk_recalculate_box(&box->widget);
 
 	return box;
 }
 
 static void
+ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
+	ltk_box *box = (ltk_box *)self;
+	int delta = 0;
+	if (box->orient == LTK_HORIZONTAL) {
+		if (r.x + r.w > self->lrect.w && r.w <= self->lrect.w)
+			delta = r.x - (self->lrect.w - r.w);
+		else if (r.x < 0 || r.w > self->lrect.w)
+			delta = r.x;
+	} else {
+		if (r.y + r.h > self->lrect.h && r.h <= self->lrect.h)
+			delta = r.y - (self->lrect.h - r.h);
+		else if (r.y < 0 || r.h > self->lrect.h)
+			delta = r.y;
+	}
+	if (delta)
+		ltk_scrollbar_scroll(&box->sc->widget, delta, 0);
+}
+
+static void
 ltk_box_destroy(ltk_widget *self, int shallow) {
 	ltk_box *box = (ltk_box *)self;
 	ltk_widget *ptr;
+	ltk_error err;
 	for (size_t i = 0; i < box->num_widgets; i++) {
 		ptr = box->widgets[i];
 		ptr->parent = NULL;
 		if (!shallow)
-			ptr->vtable->destroy(ptr, shallow);
+			ltk_widget_destroy(ptr, shallow, &err);
 	}
 	ltk_free(box->widgets);
-	ltk_remove_widget(self->id);
-	ltk_free(self->id);
-	box->sc->widget.vtable->destroy((ltk_widget *)box->sc, 0);
+	box->sc->widget.parent = NULL;
+	/* FIXME: this is a bit weird because sc isn't a normal widget
+	   (it isn't in the widget hash) */
+	ltk_widget_destroy(&box->sc->widget, 0, &err);
 	ltk_free(box);
 }
 
@@ -140,47 +194,52 @@ static void
 ltk_recalculate_box(ltk_widget *self) {
 	ltk_box *box = (ltk_box *)self;
 	ltk_widget *ptr;
-	ltk_rect *sc_rect = &box->sc->widget.rect;
-	int offset = box->orient == LTK_HORIZONTAL ? box->widget.rect.x : box->widget.rect.y;
-	int cur_pos = offset;
+	ltk_rect *sc_rect = &box->sc->widget.lrect;
+	int cur_pos = 0;
+	if (box->orient == LTK_HORIZONTAL)
+		sc_rect->h = box->sc->widget.ideal_h;
+	else
+		sc_rect->w = box->sc->widget.ideal_w;
 	for (size_t i = 0; i < box->num_widgets; i++) {
 		ptr = box->widgets[i];
 		if (box->orient == LTK_HORIZONTAL) {
-			ptr->rect.x = cur_pos - box->sc->cur_pos;
+			ptr->lrect.x = cur_pos - box->sc->cur_pos;
 			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
-				ptr->rect.h = box->widget.rect.h - sc_rect->h;
+				ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
 			if (ptr->sticky & LTK_STICKY_TOP)
-				ptr->rect.y = box->widget.rect.y;
+				ptr->lrect.y = 0;
 			else if (ptr->sticky & LTK_STICKY_BOTTOM)
-				ptr->rect.y = box->widget.rect.y + box->widget.rect.h - ptr->rect.h - sc_rect->h;
+				ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
 			else
-				ptr->rect.y = box->widget.rect.y + (box->widget.rect.h - ptr->rect.h) / 2;
-			ltk_widget_resize(ptr);
-			cur_pos += ptr->rect.w;
+				ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
+			cur_pos += ptr->lrect.w;
 		} else {
-			ptr->rect.y = cur_pos - box->sc->cur_pos;
+			ptr->lrect.y = cur_pos - box->sc->cur_pos;
 			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
-				ptr->rect.w = box->widget.rect.w - sc_rect->w;
+				ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
 			if (ptr->sticky & LTK_STICKY_LEFT)
-				ptr->rect.x = box->widget.rect.x;
+				ptr->lrect.x = 0;
 			else if (ptr->sticky & LTK_STICKY_RIGHT)
-				ptr->rect.x = box->widget.rect.x + box->widget.rect.w - ptr->rect.w - sc_rect->w;
+				ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
 			else
-				ptr->rect.x = box->widget.rect.x + (box->widget.rect.w - ptr->rect.w) / 2;
-			ltk_widget_resize(ptr);
-			cur_pos += ptr->rect.h;
+				ptr->lrect.x = (box->widget.lrect.w - ptr->lrect.w) / 2;
+			cur_pos += ptr->lrect.h;
 		}
+		ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
+		ltk_widget_resize(ptr);
 	}
-	ltk_scrollbar_set_virtual_size(box->sc, cur_pos - offset);
+	ltk_scrollbar_set_virtual_size(box->sc, cur_pos);
 	if (box->orient == LTK_HORIZONTAL) {
-		sc_rect->x = box->widget.rect.x;
-		sc_rect->y = box->widget.rect.y + box->widget.rect.h - sc_rect->h;
-		sc_rect->w = box->widget.rect.w;
+		sc_rect->x = 0;
+		sc_rect->y = box->widget.lrect.h - sc_rect->h;
+		sc_rect->w = box->widget.lrect.w;
 	} else {
-		sc_rect->x = box->widget.rect.x + box->widget.rect.w - sc_rect->w;
-		sc_rect->y = box->widget.rect.y;
-		sc_rect->h = box->widget.rect.h;
+		sc_rect->x = box->widget.lrect.w - sc_rect->w;
+		sc_rect->y = 0;
+		sc_rect->h = box->widget.lrect.h;
 	}
+	*sc_rect = ltk_rect_intersect(*sc_rect, (ltk_rect){0, 0, box->widget.lrect.w, box->widget.lrect.h});
+	box->sc->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, *sc_rect);
 	ltk_widget_resize((ltk_widget *)box->sc);
 }
 
@@ -206,12 +265,12 @@ ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
 	   could also set all widgets even if they don't have any sticky
 	   settings, but there'd probably be some catch as well. */
 	/* FIXME: the same comment as in grid.c applies */
-	int orig_w = widget->rect.w;
-	int orig_h = widget->rect.h;
-	widget->rect.w = widget->ideal_w;
-	widget->rect.h = widget->ideal_h;
-	int sc_w = box->sc->widget.rect.w;
-	int sc_h = box->sc->widget.rect.h;
+	int orig_w = widget->lrect.w;
+	int orig_h = widget->lrect.h;
+	widget->lrect.w = widget->ideal_w;
+	widget->lrect.h = widget->ideal_h;
+	int sc_w = box->sc->widget.lrect.w;
+	int sc_h = box->sc->widget.lrect.h;
 	if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h > box->widget.ideal_h) {
 		box->widget.ideal_h = widget->ideal_h + sc_h;
 		size_changed = 1;
@@ -224,7 +283,7 @@ ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
 		box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
 	else
 		ltk_recalculate_box((ltk_widget *)box);
-	if (orig_w != widget->rect.w || orig_h != widget->rect.h)
+	if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
 		ltk_widget_resize(widget);
 }
 
@@ -241,8 +300,8 @@ ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short
 		box->widgets = new;
 	}
 
-	int sc_w = box->sc->widget.rect.w;
-	int sc_h = box->sc->widget.rect.h;
+	int sc_w = box->sc->widget.lrect.w;
+	int sc_h = box->sc->widget.lrect.h;
 
 	box->widgets[box->num_widgets++] = widget;
 	if (box->orient == LTK_HORIZONTAL) {
@@ -257,7 +316,7 @@ ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short
 	widget->parent = (ltk_widget *)box;
 	widget->sticky = sticky;
 	ltk_box_child_size_change((ltk_widget *)box, widget);
-	ltk_window_invalidate_rect(window, box->widget.rect);
+	ltk_window_invalidate_widget_rect(window, &box->widget);
 
 	return 0;
 }
@@ -265,8 +324,8 @@ ltk_box_add(ltk_window *window, ltk_widget *widget, ltk_box *box, unsigned short
 static int
 ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
 	ltk_box *box = (ltk_box *)self;
-	int sc_w = box->sc->widget.rect.w;
-	int sc_h = box->sc->widget.rect.h;
+	int sc_w = box->sc->widget.lrect.w;
+	int sc_h = box->sc->widget.lrect.h;
 	if (widget->parent != (ltk_widget *)box) {
 		err->type = ERR_WIDGET_NOT_IN_CONTAINER;
 		return 1;
@@ -278,7 +337,7 @@ ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
 				memmove(box->widgets + i, box->widgets + i + 1,
 				    (box->num_widgets - i - 1) * sizeof(ltk_widget *));
 			box->num_widgets--;
-			ltk_window_invalidate_rect(widget->window, box->widget.rect);
+			ltk_window_invalidate_widget_rect(widget->window, &box->widget);
 			/* 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 */
@@ -307,20 +366,105 @@ ltk_box_remove(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
 	return 1;
 }
 
+/* FIXME: maybe come up with a more efficient method */
+static ltk_widget *
+ltk_box_nearest_child(ltk_widget *self, ltk_rect rect) {
+	ltk_box *box = (ltk_box *)self;
+	ltk_widget *minw = NULL;
+	int min_dist = INT_MAX;
+	int cx = rect.x + rect.w / 2;
+	int cy = rect.y + rect.h / 2;
+	ltk_rect r;
+	int dist;
+	for (size_t i = 0; i < box->num_widgets; i++) {
+		r = box->widgets[i]->lrect;
+		dist = abs((r.x + r.w / 2) - cx) + abs((r.y + r.h / 2) - cy);
+		if (dist < min_dist) {
+			min_dist = dist;
+			minw = box->widgets[i];
+		}
+	}
+	return minw;
+}
+
+static ltk_widget *
+ltk_box_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
+	ltk_box *box = (ltk_box *)self;
+	if (box->orient == LTK_VERTICAL)
+		return NULL;
+	return ltk_box_prev_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
+	ltk_box *box = (ltk_box *)self;
+	if (box->orient == LTK_VERTICAL)
+		return NULL;
+	return ltk_box_next_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
+	ltk_box *box = (ltk_box *)self;
+	if (box->orient == LTK_HORIZONTAL)
+		return NULL;
+	return ltk_box_prev_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
+	ltk_box *box = (ltk_box *)self;
+	if (box->orient == LTK_HORIZONTAL)
+		return NULL;
+	return ltk_box_next_child(self, widget);
+}
+
+static ltk_widget *
+ltk_box_prev_child(ltk_widget *self, ltk_widget *child) {
+	ltk_box *box = (ltk_box *)self;
+	for (size_t i = box->num_widgets; i-- > 0;) {
+		if (box->widgets[i] == child)
+			return i > 0 ? box->widgets[i-1] : NULL;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_box_next_child(ltk_widget *self, ltk_widget *child) {
+	ltk_box *box = (ltk_box *)self;
+	for (size_t i = 0; i < box->num_widgets; i++) {
+		if (box->widgets[i] == child)
+			return i < box->num_widgets - 1 ? box->widgets[i+1] : NULL;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_box_first_child(ltk_widget *self) {
+	ltk_box *box = (ltk_box *)self;
+	return box->num_widgets > 0 ? box->widgets[0] : NULL;
+}
+
+static ltk_widget *
+ltk_box_last_child(ltk_widget *self) {
+	ltk_box *box = (ltk_box *)self;
+	return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
+}
+
 static void
 ltk_box_scroll(ltk_widget *self) {
 	ltk_box *box = (ltk_box *)self;
 	ltk_recalculate_box(self);
-	ltk_window_invalidate_rect(box->widget.window, box->widget.rect);
+	ltk_window_invalidate_widget_rect(box->widget.window, &box->widget);
 }
 
 static ltk_widget *
 ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
 	ltk_box *box = (ltk_box *)self;
-	if (ltk_collide_rect(box->sc->widget.rect, x, y))
+	if (ltk_collide_rect(box->sc->widget.crect, x, y))
 		return (ltk_widget *)box->sc;
 	for (size_t i = 0; i < box->num_widgets; i++) {
-		if (ltk_collide_rect(box->widgets[i]->rect, x, y))
+		if (ltk_collide_rect(box->widgets[i]->crect, x, y))
 			return box->widgets[i];
 	}
 	return NULL;
@@ -334,7 +478,8 @@ 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);
+		ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		return 1;
 	} else {
 		return 0;
diff --git a/src/button.c b/src/button.c
@@ -37,8 +37,8 @@
 #define MAX_BUTTON_BORDER_WIDTH 100
 #define MAX_BUTTON_PADDING 500
 
-static void ltk_button_draw(ltk_widget *self, ltk_rect clip);
-static int ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event);
+static void ltk_button_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
+static int ltk_button_release(ltk_widget *self);
 static ltk_button *ltk_button_create(ltk_window *window,
     const char *id, char *text);
 static void ltk_button_destroy(ltk_widget *self, int shallow);
@@ -48,7 +48,8 @@ static struct ltk_widget_vtable vtable = {
 	.key_press = NULL,
 	.key_release = NULL,
 	.mouse_press = NULL,
-	.mouse_release = <k_button_mouse_release,
+	.mouse_release = NULL,
+	.release = <k_button_release,
 	.motion_notify = NULL,
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
@@ -119,20 +120,22 @@ ltk_button_uninitialize_theme(ltk_window *window) {
 
 /* FIXME: only keep text in surface to avoid large surface */
 static void
-ltk_button_draw(ltk_widget *self, ltk_rect clip) {
+ltk_button_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
 	ltk_button *button = (ltk_button *)self;
-	ltk_rect rect = button->widget.rect;
-	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
 	ltk_surface *s;
-	ltk_surface_cache_request_surface_size(button->key, self->rect.w, self->rect.h);
+	ltk_surface_cache_request_surface_size(button->key, lrect.w, lrect.h);
 	if (!ltk_surface_cache_get_surface(button->key, &s) || self->dirty)
 		ltk_button_redraw_surface(button, s);
-	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
+	ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
 }
 
 static void
 ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
-	ltk_rect rect = button->widget.rect;
+	ltk_rect rect = button->widget.lrect;
 	int bw = theme.border_width;
 	ltk_color *border = NULL, *fill = NULL;
 	/* FIXME: HOVERACTIVE STATE */
@@ -167,12 +170,9 @@ ltk_button_redraw_surface(ltk_button *button, ltk_surface *s) {
 }
 
 static int
-ltk_button_mouse_release(ltk_widget *self, ltk_button_event *event) {
-	if ((self->state & LTK_PRESSED) && event->button == LTK_BUTTONL && ltk_collide_rect(self->rect, event->x, event->y)) {
-		ltk_queue_specific_event(self, "button", LTK_PWEVENTMASK_BUTTON_PRESS, "press");
-		return 1;
-	}
-	return 0;
+ltk_button_release(ltk_widget *self) {
+	ltk_queue_specific_event(self, "button", LTK_PWEVENTMASK_BUTTON_PRESS, "press");
+	return 1;
 }
 
 static ltk_button *
@@ -183,9 +183,9 @@ ltk_button_create(ltk_window *window, const char *id, char *text) {
 	button->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1);
 	int text_w, text_h;
 	ltk_text_line_get_size(button->tl, &text_w, &text_h);
+	ltk_fill_widget_defaults(&button->widget, id, window, &vtable, button->widget.ideal_w, button->widget.ideal_h);
 	button->widget.ideal_w = text_w + theme.border_width * 2 + theme.pad * 2;
 	button->widget.ideal_h = text_h + theme.border_width * 2 + theme.pad * 2;
-	ltk_fill_widget_defaults(&button->widget, id, window, &vtable, button->widget.ideal_w, button->widget.ideal_h);
 	button->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, button->widget.ideal_w, button->widget.ideal_h);
 	button->widget.dirty = 1;
 
@@ -202,9 +202,6 @@ ltk_button_destroy(ltk_widget *self, int shallow) {
 	}
 	ltk_surface_cache_release_key(button->key);
 	ltk_text_line_destroy(button->tl);
-	ltk_remove_widget(self->id);
-	ltk_remove_widget(button->widget.id);
-	ltk_free(button->widget.id);
 	ltk_free(button);
 }
 
diff --git a/src/config.c b/src/config.c
@@ -0,0 +1,710 @@
+/* FIXME: This really is horrible. */
+
+#include <stdio.h>
+#include <ctype.h>
+#include <string.h>
+#include <limits.h>
+
+#include "util.h"
+#include "memory.h"
+#include "config.h"
+
+ltk_config *global_config = NULL;
+
+enum toktype {
+	STRING,
+	SECTION,
+	EQUALS,
+	NEWLINE,
+	ERROR,
+	END
+};
+
+#if 0
+static const char *
+toktype_str(enum toktype type) {
+	switch (type) {
+	case STRING:
+		return "string";
+		break;
+	case SECTION:
+		return "section";
+		break;
+	case EQUALS:
+		return "equals";
+		break;
+	case NEWLINE:
+		return "newline";
+		break;
+	case ERROR:
+		return "error";
+		break;
+	case END:
+		return "end of file";
+		break;
+	default:
+		return "unknown";
+	}
+}
+#endif
+
+struct token {
+	char *text;
+	size_t len;
+	enum toktype type;
+	size_t line;        /* line in original input */
+	size_t line_offset; /* offset from start of line */
+};
+
+struct lexstate {
+	const char *filename;
+	char *text;
+	size_t len;        /* length of text */
+	size_t cur;        /* current byte position */
+	size_t cur_line;   /* current line */
+	size_t line_start; /* byte offset of start of current line */
+};
+
+static struct token
+next_token(struct lexstate *s) {
+	char c;
+	struct token tok;
+	int finished = 0;
+	while (1) {
+		if (s->cur >= s->len)
+			return (struct token){NULL, 0, END, s->cur_line, s->cur - s->line_start + 1};
+		while (isspace(c = s->text[s->cur])) {
+			s->cur++;
+			if (c == '\n') {
+				struct token tok = (struct token){s->text + s->cur - 1, 1, NEWLINE, s->cur_line, s->cur - s->line_start};
+				s->cur_line++;
+				s->line_start = s->cur;
+				return tok;
+			}
+			if (s->cur >= s->len)
+				return (struct token){NULL, 0, END, s->cur_line, s->cur - s->line_start + 1};
+		}
+
+		switch (s->text[s->cur]) {
+		case '#':
+			s->cur++;
+			while (s->cur < s->len && s->text[s->cur] != '\n')
+				s->cur++;
+			continue;
+		case '[':
+			s->cur++;
+			tok = (struct token){s->text + s->cur, 0, SECTION, s->cur_line, s->cur - s->line_start + 1};
+			finished = 0;
+			while (s->cur < s->len) {
+				char c = s->text[s->cur];
+				if (c == '\n') {
+					break;
+				} else if (c == ']') {
+					s->cur++;
+					finished = 1;
+					break;
+				} else  {
+					tok.len++;
+				}
+				s->cur++;
+			}
+			if (!finished) {
+				tok.text = "Unfinished section name";
+				tok.len = strlen("Unfinished section name");
+				tok.type = ERROR;
+			}
+			break;
+		case '=':
+			tok = (struct token){s->text + s->cur, 1, EQUALS, s->cur_line, s->cur - s->line_start + 1};
+			s->cur++;
+			break;
+		case '"':
+			/* FIXME: error if next char is not whitespace or end */
+			s->cur++;
+			tok = (struct token){s->text + s->cur, 0, STRING, s->cur_line, s->cur - s->line_start + 1};
+			size_t shift = 0, bs = 0;
+			finished = 0;
+			while (s->cur < s->len) {
+				char c = s->text[s->cur];
+				if (c == '\n') {
+					break;
+				} else if (c == '\\') {
+					shift += bs;
+					tok.len += bs;
+					bs = (bs + 1) % 2;
+				} else if (c == '"') {
+					if (bs) {
+						shift++;
+						tok.len++;
+						bs = 0;
+					} else {
+						s->cur++;
+						finished = 1;
+						break;
+					}
+				} else {
+					tok.len++;
+				}
+				s->text[s->cur - shift] = s->text[s->cur];
+				s->cur++;
+			}
+			if (!finished) {
+				tok.text = "Unfinished string";
+				tok.len = strlen("Unfinished string");
+				tok.type = ERROR;
+			}
+			break;
+		default:
+			tok = (struct token){s->text + s->cur, 1, STRING, s->cur_line, s->cur - s->line_start + 1};
+			s->cur++;
+			while (s->cur < s->len) {
+				char c = s->text[s->cur];
+				if (isspace(c) || c == '=') {
+					break;
+				} else if (c == '"') {
+					tok.text = "Unexpected start of string";
+					tok.len = strlen("Unexpected start of string");
+					tok.type = ERROR;
+					tok.line_offset = s->cur - s->line_start + 1;
+				} else if (c == '[' || c == ']') {
+					tok.text = "Unexpected start or end of section name";
+					tok.len = strlen("Unexpected start or end of section name");
+					tok.type = ERROR;
+					tok.line_offset = s->cur - s->line_start + 1;
+				}
+				tok.len++;
+				s->cur++;
+			}
+		}
+		return tok;
+	}
+}
+
+/* FIXME: optimize - just copy from ledit; actually support all keysyms */
+static int
+parse_keysym(char *text, size_t len, ltk_keysym *sym_ret) {
+	if (str_array_equal("left", text, len))
+		*sym_ret = LTK_KEY_LEFT;
+	else if (str_array_equal("right", text, len))
+		*sym_ret = LTK_KEY_RIGHT;
+	else if (str_array_equal("up", text, len))
+		*sym_ret = LTK_KEY_UP;
+	else if (str_array_equal("down", text, len))
+		*sym_ret = LTK_KEY_DOWN;
+	else if (str_array_equal("backspace", text, len))
+		*sym_ret = LTK_KEY_BACKSPACE;
+	else if (str_array_equal("delete", text, len))
+		*sym_ret = LTK_KEY_DELETE;
+	else if (str_array_equal("space", text, len))
+		*sym_ret = LTK_KEY_SPACE;
+	else if (str_array_equal("return", text, len))
+		*sym_ret = LTK_KEY_RETURN;
+	else if (str_array_equal("tab", text, len))
+		*sym_ret = LTK_KEY_TAB;
+	else if (str_array_equal("escape", text, len))
+		*sym_ret = LTK_KEY_ESCAPE;
+	else
+		return 1;
+	return 0;
+}
+
+#undef MIN
+#define MIN(a, b) ((a) < (b) ? (a) : (b))
+
+static int
+parse_modmask(char *modmask_str, size_t len, ltk_mod_type *mask_ret) {
+	size_t cur = 0;
+	*mask_ret = 0;
+	while (cur < len) {
+		if (str_array_equal("shift", modmask_str + cur, MIN(5, len - cur))) {
+			cur += 5;
+			*mask_ret |= LTK_MOD_SHIFT;
+		} else if (str_array_equal("ctrl", modmask_str + cur, MIN(4, len - cur))) {
+			cur += 4;
+			*mask_ret |= LTK_MOD_CTRL;
+		} else if (str_array_equal("alt", modmask_str + cur, MIN(3, len - cur))) {
+			cur += 3;
+			*mask_ret |= LTK_MOD_ALT;
+		} else if (str_array_equal("super", modmask_str + cur, MIN(5, len - cur))) {
+			cur += 5;
+			*mask_ret |= LTK_MOD_SUPER;
+		} else if (str_array_equal("any", modmask_str + cur, MIN(3, len - cur))) {
+			cur += 3;
+			*mask_ret = UINT_MAX;
+		} else {
+			return 1;
+		}
+		if (cur < len && modmask_str[cur] != '|')
+			return 1;
+		else
+			cur++;
+	}
+	return 0;
+}
+
+static int
+parse_flags(char *text, size_t len, ltk_key_binding_flags *flags_ret) {
+	if (str_array_equal("run-always", text, len))
+		*flags_ret = LTK_KEY_BINDING_RUN_ALWAYS;
+	else
+		return 1;
+	return 0;
+}
+
+static int
+parse_keypress_binding(struct lexstate *s, struct token *tok, ltk_keypress_binding *binding_ret, char **errstr) {
+	ltk_keypress_binding b = {NULL, NULL, NULL, LTK_KEY_NONE, LTK_MOD_NONE, LTK_KEY_BINDING_NOFLAGS};
+	*tok = next_token(s);
+	char *msg = NULL;
+	if (tok->type != STRING) {
+		msg = "Invalid token type";
+		goto error;
+	}
+	b.callback = ltk_get_key_func(tok->text, tok->len);
+	if (!b.callback) {
+		msg = "Invalid function specification";
+		goto error;
+	}
+	struct token prevtok;
+	int text_init = 0, rawtext_init = 0, sym_init = 0, mods_init = 0, flags_init = 0;
+	while (1) {
+		*tok = next_token(s);
+		if (tok->type == NEWLINE || tok->type == END)
+			break;
+		prevtok = *tok;
+		*tok = next_token(s);
+		if (prevtok.type != STRING) {
+			msg = "Invalid token type";
+			*tok = prevtok;
+			goto error;
+		} else if (tok->type != STRING) {
+			msg = "Invalid token type";
+			goto error;
+		} else if (str_array_equal("text", prevtok.text, prevtok.len)) {
+			if (rawtext_init) {
+				msg = "Rawtext already specified";
+				goto error;
+			} else if (sym_init) {
+				msg = "Keysym already specified";
+				goto error;
+			} else if (text_init) {
+				msg = "Duplicate text specification";
+				goto error;
+			}
+			b.text = ltk_strndup(tok->text, tok->len);
+			text_init = 1;
+		} else if (str_array_equal("rawtext", prevtok.text, prevtok.len)) {
+			if (text_init) {
+				msg = "Text already specified";
+				goto error;
+			} else if (sym_init) {
+				msg = "Keysym already specified";
+				goto error;
+			} else if (rawtext_init) {
+				msg = "Duplicate rawtext specification";
+				goto error;
+			}
+			b.rawtext = ltk_strndup(tok->text, tok->len);
+			rawtext_init = 1;
+		} else if (str_array_equal("sym", prevtok.text, prevtok.len)) {
+			if (text_init) {
+				msg = "Text already specified";
+				goto error;
+			} else if (rawtext_init) {
+				msg = "Rawtext already specified";
+				goto error;
+			} else if (sym_init) {
+				msg = "Duplicate keysym specification";
+				goto error;
+			} else if (parse_keysym(tok->text, tok->len, &b.sym)) {
+				msg = "Invalid keysym specification";
+				goto error;
+			}
+			sym_init = 1;
+		} else if (str_array_equal("mods", prevtok.text, prevtok.len)) {
+			if (mods_init) {
+				msg = "Duplicate mods specification";
+				goto error;
+			} else if (parse_modmask(tok->text, tok->len, &b.mods)) {
+				msg = "Invalid mods specification";
+				goto error;
+			}
+			mods_init = 1;
+		} else if (str_array_equal("flags", prevtok.text, prevtok.len)) {
+			if (flags_init) {
+				msg = "Duplicate flags specification";
+				goto error;
+			} else if (parse_flags(tok->text, tok->len, &b.flags)) {
+				msg = "Invalid flags specification";
+				goto error;
+			}
+			flags_init = 1;
+		} else {
+			msg = "Invalid keyword";
+			*tok = prevtok;
+			goto error;
+		}
+	};
+	if (!text_init && !rawtext_init && !sym_init) {
+		msg = "One of text, rawtext, and sym must be initialized";
+		goto error;
+	}
+	*binding_ret = b;
+	return 0;
+error:
+	free(b.text);
+	free(b.rawtext);
+	if (msg) {
+		*errstr = ltk_print_fmt(
+		    "%s, line %zu, offset %zu: %s", s->filename, tok->line, tok->line_offset, msg
+		);
+	} else {
+		*errstr = NULL;
+	}
+	return 1;
+}
+
+static void
+push_keypress(ltk_config *c, ltk_keypress_binding b) {
+	if (c->keys.press_alloc == c->keys.press_len) {
+		c->keys.press_alloc = ideal_array_size(c->keys.press_alloc, c->keys.press_len + 1);
+		c->keys.press_bindings = ltk_reallocarray(c->keys.press_bindings, c->keys.press_alloc, sizeof(ltk_keypress_binding));
+	}
+	c->keys.press_bindings[c->keys.press_len] = b;
+	c->keys.press_len++;
+}
+
+static void
+push_keyrelease(ltk_config *c, ltk_keyrelease_binding b) {
+	if (c->keys.release_alloc == c->keys.release_len) {
+		c->keys.release_alloc = ideal_array_size(c->keys.release_alloc, c->keys.release_len + 1);
+		c->keys.release_bindings = ltk_reallocarray(c->keys.release_bindings, c->keys.release_alloc, sizeof(ltk_keyrelease_binding));
+	}
+	c->keys.release_bindings[c->keys.release_len] = b;
+	c->keys.release_len++;
+}
+
+static int
+parse_keybinding(struct lexstate *s, ltk_config *c, struct token *tok, char **errstr) {
+	char *msg = NULL;
+	*tok = next_token(s);
+	if (tok->type == SECTION || tok->type == END) {
+		return -1;
+	} else if (tok->type == NEWLINE) {
+		return 0; /* empty line */
+	} else if (tok->type != STRING) {
+		msg = "Invalid token type";
+		goto error;
+	} else if (str_array_equal("bind-keypress", tok->text, tok->len)) {
+		ltk_keypress_binding b;
+		if (parse_keypress_binding(s, tok, &b, errstr))
+			return 1;
+		push_keypress(c, b);
+	} else if (str_array_equal("bind-keyrelease", tok->text, tok->len)) {
+		ltk_keypress_binding b;
+		if (parse_keypress_binding(s, tok, &b, errstr))
+			return 1;
+		if (b.text || b.rawtext) {
+			free(b.text);
+			free(b.rawtext);
+			msg = "Text and rawtext may only be specified for keypress bindings";
+			goto error;
+		}
+		push_keyrelease(c, (ltk_keyrelease_binding){b.callback, b.sym, b.mods, b.flags});
+	} else {
+		msg = "Invalid statement";
+		goto error;
+	}
+	return 0;
+error:
+	if (msg) {
+		*errstr = ltk_print_fmt(
+		    "%s, line %zu, offset %zu: %s", s->filename, tok->line, tok->line_offset, msg
+		);
+	}
+	return 1;
+}
+
+static void
+push_lang_mapping(ltk_config *c) {
+	if (c->keys.mappings_alloc == c->keys.mappings_len) {
+		c->keys.mappings_alloc = ideal_array_size(c->keys.mappings_alloc, c->keys.mappings_len + 1);
+		c->keys.mappings = ltk_reallocarray(c->keys.mappings, c->keys.mappings_alloc, sizeof(ltk_language_mapping));
+	}
+	c->keys.mappings[c->keys.mappings_len].lang = NULL;
+	c->keys.mappings[c->keys.mappings_len].mappings = NULL;
+	c->keys.mappings[c->keys.mappings_len].mappings_alloc = 0;
+	c->keys.mappings[c->keys.mappings_len].mappings_len = 0;
+	c->keys.mappings_len++;
+}
+
+static void
+push_text_mapping(ltk_config *c, char *text1, size_t len1, char *text2, size_t len2) {
+	if (c->keys.mappings_len == 0)
+		return; /* I guess just fail silently... */
+	ltk_language_mapping *m = &c->keys.mappings[c->keys.mappings_len - 1];
+	if (m->mappings_alloc == m->mappings_len) {
+		m->mappings_alloc = ideal_array_size(m->mappings_alloc, m->mappings_len + 1);
+		m->mappings = ltk_reallocarray(m->mappings, m->mappings_alloc, sizeof(ltk_keytext_mapping));
+	}
+	m->mappings[m->mappings_len].from = ltk_strndup(text1, len1);
+	m->mappings[m->mappings_len].to = ltk_strndup(text2, len2);
+	m->mappings_len++;
+}
+
+static void
+destroy_config(ltk_config *c) {
+	for (size_t i = 0; i < c->keys.press_len; i++) {
+		ltk_free(c->keys.press_bindings[i].text);
+		ltk_free(c->keys.press_bindings[i].rawtext);
+	}
+	ltk_free(c->keys.press_bindings);
+	ltk_free(c->keys.release_bindings);
+	for (size_t i = 0; i < c->keys.mappings_len; i++) {
+		ltk_free(c->keys.mappings[i].lang);
+		for (size_t j = 0; j < c->keys.mappings[i].mappings_len; j++) {
+			ltk_free(c->keys.mappings[i].mappings[j].from);
+			ltk_free(c->keys.mappings[i].mappings[j].to);
+		}
+		ltk_free(c->keys.mappings[i].mappings);
+	}
+	ltk_free(c->keys.mappings);
+	ltk_free(c);
+}
+
+void
+ltk_config_cleanup(void) {
+	if (global_config)
+		destroy_config(global_config);
+	global_config = NULL;
+}
+
+ltk_config *
+ltk_config_get(void) {
+	return global_config;
+}
+
+int
+ltk_config_get_language_index(char *lang, size_t *idx_ret) {
+	if (!global_config)
+		return 1;
+	for (size_t i = 0; i < global_config->keys.mappings_len; i++) {
+		if (!strcmp(lang, global_config->keys.mappings[i].lang)) {
+			*idx_ret = i;
+			return 0;
+		}
+	}
+	return 1;
+}
+
+ltk_language_mapping *
+ltk_config_get_language_mapping(size_t idx) {
+	if (!global_config || idx >= global_config->keys.mappings_len)
+		return NULL;
+	return &global_config->keys.mappings[idx];
+}
+
+/* WARNING: errstr must be freed! */
+/* FIXME: make ltk_load_file give size_t; handle errors there (copy from ledit) */
+static int
+load_from_text(const char *filename, char *file_contents, size_t len, char **errstr) {
+	ltk_config *config = ltk_malloc(sizeof(ltk_config));
+	config->keys.press_bindings = NULL;
+	config->keys.release_bindings = NULL;
+	config->keys.mappings = NULL;
+	config->keys.press_alloc = config->keys.press_len = 0;
+	config->keys.release_alloc = config->keys.release_len = 0;
+	config->keys.mappings_alloc = config->keys.mappings_len = 0;
+	config->general.explicit_focus = 0;
+	config->general.all_activatable = 0;
+
+	struct lexstate s = {filename, file_contents, len, 0, 1, 0};
+	struct token tok = next_token(&s);
+	int start_of_line = 1;
+	char *msg = NULL;
+	struct token secttok;
+	while (tok.type != END) {
+		switch (tok.type) {
+		case SECTION:
+			if (!start_of_line) {
+				msg = "Section can only start at new line";
+				goto error;
+			}
+			secttok = tok;
+			tok = next_token(&s);
+			if (tok.type != NEWLINE && tok.type != END) {
+				msg = "Section must be alone on line";
+				goto error;
+			}
+			/* FIXME: generalize (at least once more options are added) */
+			if (str_array_equal("general", secttok.text, secttok.len)) {
+				struct token prev1tok, prev2tok;
+				while (1) {
+					tok = next_token(&s);
+					if (tok.type == SECTION || tok.type == END)
+						break;
+					else if (tok.type == NEWLINE)
+						continue;
+					prev2tok = tok;
+					tok = next_token(&s);
+					prev1tok = tok;
+					tok = next_token(&s);
+					if (prev2tok.type != STRING || prev1tok.type != EQUALS || tok.type != STRING) {
+						msg = "Invalid assignment statement";
+						goto error;
+					}
+					if (str_array_equal("explicit-focus", prev2tok.text, prev2tok.len)) {
+						if (str_array_equal("true", tok.text, tok.len)) {
+							config->general.explicit_focus = 1;
+						} else if (str_array_equal("false", tok.text, tok.len)) {
+							config->general.explicit_focus = 0;
+						} else {
+							msg = "Invalid boolean setting";
+							goto error;
+						}
+					} else if (str_array_equal("all-activatable", prev2tok.text, prev2tok.len)) {
+						if (str_array_equal("true", tok.text, tok.len)) {
+							config->general.all_activatable = 1;
+						} else if (str_array_equal("false", tok.text, tok.len)) {
+							config->general.all_activatable = 0;
+						} else {
+							msg = "Invalid boolean setting";
+							goto error;
+						}
+					} else {
+						msg = "Invalid setting";
+						goto error;
+					}
+					tok = next_token(&s);
+					if (tok.type == END) {
+						break;
+					} else if (tok.type != NEWLINE) {
+						msg = "Invalid assignment statement";
+						goto error;
+					}
+					start_of_line = 1;
+				}
+			} else if (str_array_equal("key-binding", secttok.text, secttok.len)) {
+				int ret = 0;
+				while (1) {
+					if ((ret = parse_keybinding(&s, config, &tok, errstr)) > 0) {
+						goto errornomsg;
+					} else if (ret < 0) {
+						start_of_line = 1;
+						break;
+					}
+				}
+			} else if (str_array_equal("key-mapping", secttok.text, secttok.len)) {
+				int lang_init = 0;
+				push_lang_mapping(config);
+				struct token prev1tok, prev2tok;
+				while (1) {
+					tok = next_token(&s);
+					if (tok.type == SECTION || tok.type == END)
+						break;
+					else if (tok.type == NEWLINE)
+						continue;
+					prev2tok = tok;
+					tok = next_token(&s);
+					prev1tok = tok;
+					tok = next_token(&s);
+					if (prev2tok.type != STRING) {
+						msg = "Invalid statement in language mapping";
+						goto error;
+					}
+					if (str_array_equal("language", prev2tok.text, prev2tok.len)) {
+						if (prev1tok.type != EQUALS || tok.type != STRING) {
+							msg = "Invalid language assignment";
+							goto error;
+						} else if (lang_init) {
+							msg = "Language already set";
+							goto error;
+						}
+						config->keys.mappings[config->keys.mappings_len - 1].lang = ltk_strndup(tok.text, tok.len);
+						lang_init = 1;
+					} else if (str_array_equal("map", prev2tok.text, prev2tok.len)) {
+						if (prev1tok.type != STRING || tok.type != STRING) {
+							msg = "Invalid map statement";
+							goto error;
+						}
+						push_text_mapping(config, prev1tok.text, prev1tok.len, tok.text, tok.len);
+					} else {
+						msg = "Invalid statement in language mapping";
+						goto error;
+					}
+					tok = next_token(&s);
+					if (tok.type == END) {
+						break;
+					} else if (tok.type != NEWLINE) {
+						msg = "Invalid statement in language mapping";
+						goto error;
+					}
+					start_of_line = 1;
+				}
+				if (!lang_init) {
+					msg = "Language not set for language mapping";
+					goto error;
+				}
+			} else {
+				msg = "Invalid section";
+				goto error;
+			}
+			break;
+		case NEWLINE:
+			start_of_line = 1;
+			break;
+		default:
+			msg = "Invalid token";
+			goto error;
+			break;
+		}
+	}
+	global_config = config;
+	return 0;
+error:
+	if (msg) {
+		*errstr = ltk_print_fmt(
+		    "%s, line %zu, offset %zu: %s", filename, tok.line, tok.line_offset, msg
+		);
+	}
+errornomsg:
+	destroy_config(config);
+	return 1;
+}
+
+int
+ltk_config_parsefile(const char *filename, char **errstr) {
+	unsigned long len = 0;
+	char *file_contents = ltk_read_file(filename, &len);
+	if (!file_contents) {
+		*errstr = ltk_print_fmt("Unable to open file \"%s\"", filename);
+		return 1;
+	}
+	int ret = load_from_text(filename, file_contents, len, errstr);
+	ltk_free(file_contents);
+	return ret;
+}
+
+const char *default_config = "[general]\n"
+"explicit-focus = true\n"
+"all-activatable = true\n"
+"[key-binding]\n"
+"bind-keypress move-next sym tab\n"
+"bind-keypress move-prev sym tab mods shift\n"
+"bind-keypress move-next text n\n"
+"bind-keypress move-prev text p\n"
+"bind-keypress focus-active sym return\n"
+"bind-keypress unfocus-active sym escape\n"
+"bind-keypress set-pressed sym return flags run-always\n"
+"bind-keyrelease unset-pressed sym return flags run-always\n"
+"[key-mapping]\n"
+"language = \"English (US)\"\n";
+
+/* FIXME: improve this configuration */
+int
+ltk_config_load_default(char **errstr) {
+	char *config_copied = ltk_strdup(default_config);
+	int ret = load_from_text("<default config>", config_copied, strlen(config_copied), errstr);
+	free(config_copied);
+	return ret;
+}
diff --git a/src/config.h b/src/config.h
@@ -0,0 +1,69 @@
+#ifndef LTK_CONFIG_H
+#define LTK_CONFIG_H
+
+#include <stddef.h>
+
+#include "eventdefs.h"
+#include "widget_config.h"
+
+typedef enum{
+	LTK_KEY_BINDING_NOFLAGS = 0,
+	LTK_KEY_BINDING_RUN_ALWAYS = 1,
+} ltk_key_binding_flags;
+
+typedef struct {
+	char *text;
+	char *rawtext;
+	/* FIXME: forward declaration to avoid having to pull everything in for these definitions */
+	ltk_key_callback *callback;
+	ltk_keysym sym;
+	ltk_mod_type mods;
+	ltk_key_binding_flags flags;
+} ltk_keypress_binding;
+
+typedef struct {
+	ltk_key_callback *callback;
+	ltk_keysym sym;
+	ltk_mod_type mods;
+	ltk_key_binding_flags flags;
+} ltk_keyrelease_binding;
+
+typedef struct{
+	char *from;
+	char *to;
+} ltk_keytext_mapping;
+
+typedef struct {
+	char *lang;
+	ltk_keytext_mapping *mappings;
+	size_t mappings_alloc, mappings_len;
+} ltk_language_mapping;
+
+/* FIXME: generic array */
+typedef struct {
+	ltk_keypress_binding *press_bindings;
+	ltk_keyrelease_binding *release_bindings;
+	ltk_language_mapping *mappings;
+	size_t press_alloc, press_len;
+	size_t release_alloc, release_len;
+	size_t mappings_alloc, mappings_len;
+} ltk_keys_config;
+
+typedef struct {
+	char explicit_focus;
+	char all_activatable;
+} ltk_general_config;
+
+typedef struct {
+	ltk_keys_config keys;
+	ltk_general_config general;
+} ltk_config;
+
+void ltk_config_cleanup(void);
+ltk_config *ltk_config_get(void);
+int ltk_config_get_language_index(char *lang, size_t *idx_ret);
+ltk_language_mapping *ltk_config_get_language_mapping(size_t idx);
+int ltk_config_parsefile(const char *filename, char **errstr);
+int ltk_config_load_default(char **errstr);
+
+#endif /* LTK_CONFIG_H */
diff --git a/src/event.h b/src/event.h
@@ -1,30 +1,7 @@
 #ifndef LTK_EVENT_H
 #define LTK_EVENT_H
 
-typedef enum {
-	LTK_UNKNOWN_EVENT, /* FIXME: a bit weird */
-	LTK_BUTTONPRESS_EVENT,
-	LTK_BUTTONRELEASE_EVENT,
-	LTK_MOTION_EVENT,
-	LTK_KEYPRESS_EVENT,
-	LTK_KEYRELEASE_EVENT,
-	LTK_CONFIGURE_EVENT,
-	LTK_EXPOSE_EVENT,
-	LTK_WINDOWCLOSE_EVENT
-} ltk_event_type;
-
-/* FIXME: button mask also in motion */
-
-typedef enum {
-	LTK_BUTTONL,
-	LTK_BUTTONM,
-	LTK_BUTTONR,
-	/* FIXME: dedicated scroll event */
-	LTK_BUTTON4,
-	LTK_BUTTON5,
-	LTK_BUTTON6,
-	LTK_BUTTON7
-} ltk_button_type;
+#include "eventdefs.h"
 
 typedef struct {
 	ltk_event_type type;
@@ -37,29 +14,8 @@ typedef struct {
 	int x, y;
 } ltk_motion_event;
 
-/* FIXME: just steal the definitions from X when using Xlib so no conversion is necessary? */
-typedef enum {
-	LTK_KEY_NONE = 0,
-	LTK_KEY_LEFT,
-	LTK_KEY_RIGHT,
-	LTK_KEY_UP,
-	LTK_KEY_DOWN,
-	LTK_KEY_BACKSPACE,
-	LTK_KEY_DELETE,
-	LTK_KEY_SPACE,
-	LTK_KEY_RETURN
-} ltk_keysym;
-
-typedef enum {
-	LTK_MOD_CTRL,
-	LTK_MOD_SHIFT,
-	LTK_MOD_ALT,
-	LTK_MOD_SUPER
-} ltk_mod_type;
-
 typedef struct {
 	ltk_event_type type;
-	int x, y;
 	ltk_mod_type modmask;
 	ltk_keysym sym;
 	char *text;
@@ -68,6 +24,11 @@ typedef struct {
 
 typedef struct {
 	ltk_event_type type;
+	char *new_kbd;
+} ltk_keyboard_event;
+
+typedef struct {
+	ltk_event_type type;
 	int x, y;
 	int w, h;
 } ltk_configure_event;
@@ -86,11 +47,15 @@ typedef union {
 	ltk_key_event key;
 	ltk_configure_event configure;
 	ltk_expose_event expose;
+	ltk_keyboard_event keyboard;
 } ltk_event;
 
 #include "ltk.h"
 
 int ltk_events_pending(ltk_renderdata *renderdata);
-void ltk_next_event(ltk_renderdata *renderdata, ltk_event *event);
+void ltk_events_cleanup(void);
+/* WARNING: Text returned in key and keyboard events must be copied before calling this function again! */
+void ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event);
+void ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event);
 
 #endif /* LTK_EVENT_H */
diff --git a/src/event_xlib.c b/src/event_xlib.c
@@ -1,11 +1,33 @@
+#include <stdio.h>
+
+#include <X11/XKBlib.h>
+#include <X11/extensions/XKBrules.h>
+
+#include "memory.h"
 #include "graphics.h"
 #include "xlib_shared.h"
+#include "config.h"
+
+#define TEXT_INITIAL_SIZE 128
+
+static char *text = NULL;
+static size_t text_alloc = 0;
+static char *cur_kbd = NULL;
 
 int
 ltk_events_pending(ltk_renderdata *renderdata) {
 	return XPending(renderdata->dpy);
 }
 
+void
+ltk_events_cleanup(void) {
+	ltk_free(text);
+	ltk_free(cur_kbd);
+	cur_kbd = NULL;
+	text = NULL;
+	text_alloc = 0;
+}
+
 static ltk_button_type
 get_button(unsigned int button) {
 	switch (button) {
@@ -20,11 +42,127 @@ get_button(unsigned int button) {
 	}
 }
 
+/* FIXME: make an actual exhaustive list here */
+static ltk_keysym
+get_keysym(KeySym sym) {
+	switch (sym) {
+	case XK_Left: return LTK_KEY_LEFT;
+	case XK_Right: return LTK_KEY_RIGHT;
+	case XK_Up: return LTK_KEY_UP;
+	case XK_Down: return LTK_KEY_DOWN;
+	case XK_BackSpace: return LTK_KEY_BACKSPACE;
+	case XK_space: return LTK_KEY_SPACE;
+	case XK_Return: return LTK_KEY_RETURN;
+	case XK_Delete: return LTK_KEY_DELETE;
+	/* FIXME: what other weird keys like this exist? */
+	/* why is it ISO_Left_Tab when shift is pressed? */
+	/* I mean, it makes sense, but I find it to be weird,
+	   and I'm not sure how standardized it is */
+	case XK_ISO_Left_Tab: case XK_Tab: return LTK_KEY_TAB;
+	case XK_Escape: return LTK_KEY_ESCAPE;
+	default: return LTK_KEY_NONE;
+	}
+}
+
+/* FIXME: properly implement modifiers - see SDL and GTK */
+static ltk_mod_type
+get_modmask(unsigned int state) {
+	ltk_mod_type t = 0;
+	if (state & ControlMask)
+		t |= LTK_MOD_CTRL;
+	if (state & ShiftMask)
+		t |= LTK_MOD_SHIFT;
+	if (state & Mod1Mask)
+		t |= LTK_MOD_ALT;
+	if (state & Mod4Mask)
+		t |= LTK_MOD_SUPER;
+	return t;
+}
+#ifdef X_HAVE_UTF8_STRING
+#define LOOKUP_STRING_FUNC Xutf8LookupString
+#else
+#define LOOKUP_STRING_FUNC XmbLookupString
+#endif
+
+static ltk_event
+process_key(ltk_renderdata *renderdata, size_t lang_index, XEvent *event, ltk_event_type type) {
+	ltk_language_mapping *map = ltk_config_get_language_mapping(lang_index);
+	/* FIXME: see comment in keys.c in ledit repository */
+	if (!text) {
+		text = ltk_malloc(TEXT_INITIAL_SIZE);
+		text_alloc = TEXT_INITIAL_SIZE;
+	}
+	unsigned int state = event->xkey.state;
+	event->xkey.state &= ~ControlMask;
+	KeySym sym;
+	int len = 0;
+	Status status;
+	if (renderdata->xic && type == LTK_KEYPRESS_EVENT) {
+		len = LOOKUP_STRING_FUNC(renderdata->xic, &event->xkey, text, text_alloc - 1, &sym, &status);
+		if (status == XBufferOverflow) {
+			text_alloc = ideal_array_size(text_alloc, len + 1);
+			text = ltk_realloc(text, text_alloc);
+			len = LOOKUP_STRING_FUNC(renderdata->xic, &event->xkey, text, text_alloc - 1, &sym, &status);
+		}
+	} else {
+		/* FIXME: anything equivalent to XBufferOverflow here? */
+		len = XLookupString(&event->xkey, text, text_alloc - 1, &sym, NULL);
+		status = XLookupBoth;
+	}
+	text[len >= (int)text_alloc ? (int)text_alloc - 1 : len] = '\0';
+	char *key_text = (status == XLookupChars || status == XLookupBoth) ? text : NULL;
+	char *mapped = key_text;
+	/* FIXME: BINARY SEARCH! */
+	if (key_text && map) {
+		for (size_t i = 0; i < map->mappings_len; i++) {
+			if (!strcmp(key_text, map->mappings[i].from)) {
+				mapped = map->mappings[i].to;
+				break;
+			}
+		}
+	}
+	return (ltk_event){.key = {
+		.type = type,
+		.modmask = get_modmask(state),
+		.sym = (status == XLookupKeySym || status == XLookupBoth) ? get_keysym(sym) : LTK_KEY_NONE,
+		.text = key_text,
+		.mapped = mapped
+	}};
+}
+
 void
-ltk_next_event(ltk_renderdata *renderdata, ltk_event *event) {
+ltk_generate_keyboard_event(ltk_renderdata *renderdata, ltk_event *event) {
+	XkbStateRec s;
+	XkbGetState(renderdata->dpy, XkbUseCoreKbd, &s);
+	XkbDescPtr desc = XkbGetKeyboard(
+	    renderdata->dpy, XkbAllComponentsMask, XkbUseCoreKbd
+	);
+	char *group = XGetAtomName(
+	    renderdata->dpy, desc->names->groups[s.group]
+	);
+	ltk_free(cur_kbd);
+	/* just so the interface is the same for all events and the
+	   caller doesn't have to free the contained string(s) */
+	cur_kbd = ltk_strdup(group);
+	*event = (ltk_event){.keyboard = {
+		.type = LTK_KEYBOARDCHANGE_EVENT,
+		.new_kbd = cur_kbd
+	}};
+	XFree(group);
+	XkbFreeKeyboard(desc, XkbAllComponentsMask, True);
+}
+
+void
+ltk_next_event(ltk_renderdata *renderdata, size_t lang_index, ltk_event *event) {
 	XEvent xevent;
 	XNextEvent(renderdata->dpy, &xevent);
+	if (renderdata->xkb_supported && xevent.type == renderdata->xkb_event_type) {
+		ltk_generate_keyboard_event(renderdata, event);
+		return;
+	}
 	*event = (ltk_event){.type = LTK_UNKNOWN_EVENT};
+	if (XFilterEvent(&xevent, None))
+		return;
 	switch (xevent.type) {
 	case ButtonPress:
 		*event = (ltk_event){.button = {
@@ -49,6 +187,12 @@ ltk_next_event(ltk_renderdata *renderdata, ltk_event *event) {
 			.y = xevent.xmotion.y
 		}};
 		break;
+	case KeyPress:
+		*event = process_key(renderdata, lang_index, &xevent, LTK_KEYPRESS_EVENT);
+		break;
+	case KeyRelease:
+		*event = process_key(renderdata, lang_index, &xevent, LTK_KEYRELEASE_EVENT);
+		break;
 	case ConfigureNotify:
 		*event = (ltk_event){.configure = {
 			.type = LTK_CONFIGURE_EVENT,
@@ -76,5 +220,7 @@ ltk_next_event(ltk_renderdata *renderdata, ltk_event *event) {
 		if ((Atom)xevent.xclient.data.l[0] == renderdata->wm_delete_msg)
 			*event = (ltk_event){.type = LTK_WINDOWCLOSE_EVENT};
 		break;
+	default:
+		break;
 	}
 }
diff --git a/src/eventdefs.h b/src/eventdefs.h
@@ -0,0 +1,53 @@
+#ifndef LTK_EVENTDEFS_H
+#define LTK_EVENTDEFS_H
+
+typedef enum {
+	LTK_UNKNOWN_EVENT, /* FIXME: a bit weird */
+	LTK_BUTTONPRESS_EVENT,
+	LTK_BUTTONRELEASE_EVENT,
+	LTK_MOTION_EVENT,
+	LTK_KEYPRESS_EVENT,
+	LTK_KEYRELEASE_EVENT,
+	LTK_CONFIGURE_EVENT,
+	LTK_EXPOSE_EVENT,
+	LTK_WINDOWCLOSE_EVENT,
+	LTK_KEYBOARDCHANGE_EVENT,
+} ltk_event_type;
+
+/* FIXME: button mask also in motion */
+
+typedef enum {
+	LTK_BUTTONL,
+	LTK_BUTTONM,
+	LTK_BUTTONR,
+	/* FIXME: dedicated scroll event */
+	LTK_BUTTON4,
+	LTK_BUTTON5,
+	LTK_BUTTON6,
+	LTK_BUTTON7
+} ltk_button_type;
+
+/* FIXME: just steal the definitions from X when using Xlib so no conversion is necessary? */
+typedef enum {
+	LTK_KEY_NONE = 0,
+	LTK_KEY_LEFT,
+	LTK_KEY_RIGHT,
+	LTK_KEY_UP,
+	LTK_KEY_DOWN,
+	LTK_KEY_BACKSPACE,
+	LTK_KEY_DELETE,
+	LTK_KEY_SPACE,
+	LTK_KEY_RETURN,
+	LTK_KEY_TAB,
+	LTK_KEY_ESCAPE,
+} ltk_keysym;
+
+typedef enum {
+	LTK_MOD_NONE = 0,
+	LTK_MOD_CTRL = 1,
+	LTK_MOD_SHIFT = 2,
+	LTK_MOD_ALT = 4,
+	LTK_MOD_SUPER = 8
+} ltk_mod_type;
+
+#endif /* LTK_EVENTDEFS_H */
diff --git a/src/graphics.h b/src/graphics.h
@@ -75,9 +75,10 @@ XftDraw *ltk_surface_get_xft_draw(ltk_surface *s);
 Drawable ltk_surface_get_drawable(ltk_surface *s);
 #endif
 
+void renderer_set_imspot(ltk_renderdata *renderdata, int x, int y);
 ltk_renderdata *renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned int h);
 void renderer_destroy_window(ltk_renderdata *renderdata);
-void renderer_set_window_properties(ltk_renderdata *renderdata, ltk_color *bg, ltk_color *border, unsigned int border_width);
+void renderer_set_window_properties(ltk_renderdata *renderdata, ltk_color *bg);
 /* FIXME: this is kind of out of place */
 void renderer_swap_buffers(ltk_renderdata *renderdata);
 /* FIXME: this is just for the socket name and is a bit weird */
diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
@@ -14,10 +14,14 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <stdio.h>
+#include <stdint.h>
+
 #include <X11/Xlib.h>
 #include <X11/Xutil.h>
 #include <X11/extensions/Xdbe.h>
-#include <stdint.h>
+#include <X11/XKBlib.h>
+#include <X11/extensions/XKBrules.h>
 
 #include "color.h"
 #include "rect.h"
@@ -351,6 +355,83 @@ ltk_surface_get_drawable(ltk_surface *s) {
 	return s->d;
 }
 
+/* FIXME: move this to a file where it makes more sense */
+/* blatantly stolen from st */
+static void ximinstantiate(Display *dpy, XPointer client, XPointer call);
+static void ximdestroy(XIM xim, XPointer client, XPointer call);
+static int xicdestroy(XIC xim, XPointer client, XPointer call);
+static int ximopen(ltk_renderdata *renderdata, Display *dpy);
+
+static void
+ximdestroy(XIM xim, XPointer client, XPointer call) {
+	(void)xim;
+	(void)call;
+	ltk_renderdata *renderdata = (ltk_renderdata *)client;
+	renderdata->xim = NULL;
+	XRegisterIMInstantiateCallback(
+	    renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)renderdata
+	);
+	XFree(renderdata->spotlist);
+}
+
+static int
+xicdestroy(XIC xim, XPointer client, XPointer call) {
+	(void)xim;
+	(void)call;
+	ltk_renderdata *renderdata = (ltk_renderdata *)client;
+	renderdata->xic = NULL;
+	return 1;
+}
+
+static int
+ximopen(ltk_renderdata *renderdata, Display *dpy) {
+	XIMCallback imdestroy = { .client_data = (XPointer)renderdata, .callback = ximdestroy };
+	XICCallback icdestroy = { .client_data = (XPointer)renderdata, .callback = xicdestroy };
+
+	renderdata->xim = XOpenIM(dpy, NULL, NULL, NULL);
+	if (renderdata->xim == NULL)
+		return 0;
+
+	if (XSetIMValues(renderdata->xim, XNDestroyCallback, &imdestroy, NULL))
+		ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
+
+	renderdata->spotlist = XVaCreateNestedList(0, XNSpotLocation, &renderdata->spot, NULL);
+
+	if (renderdata->xic == NULL) {
+		renderdata->xic = XCreateIC(
+		    renderdata->xim, XNInputStyle,
+		    XIMPreeditNothing | XIMStatusNothing,
+		    XNClientWindow, renderdata->xwindow,
+		    XNDestroyCallback, &icdestroy, NULL
+		);
+	}
+	if (renderdata->xic == NULL)
+		ltk_warn("XCreateIC: Could not create input context.\n");
+
+	return 1;
+}
+
+static void
+ximinstantiate(Display *dpy, XPointer client, XPointer call) {
+	(void)call;
+	ltk_renderdata *renderdata = (ltk_renderdata *)client;
+	if (ximopen(renderdata, dpy)) {
+		XUnregisterIMInstantiateCallback(
+		    dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)renderdata
+		);
+	}
+}
+
+void
+renderer_set_imspot(ltk_renderdata *renderdata, int x, int y) {
+	if (renderdata->xic == NULL)
+		return;
+	/* FIXME! */
+	renderdata->spot.x = x;
+	renderdata->spot.y = y;
+	XSetICValues(renderdata->xic, XNPreeditAttributes, renderdata->spotlist, NULL);
+}
+
 ltk_renderdata *
 renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned int h) {
 	XSetWindowAttributes attrs;
@@ -430,15 +511,48 @@ renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned
 		title, NULL, None, NULL, 0, NULL
 	);
 	XSetWMProtocols(renderdata->dpy, renderdata->xwindow, &renderdata->wm_delete_msg, 1);
+
+	renderdata->xim = NULL;
+	renderdata->xic = NULL;
+	if (!ximopen(renderdata, renderdata->dpy)) {
+		XRegisterIMInstantiateCallback(
+		    renderdata->dpy, NULL, NULL, NULL,
+		    ximinstantiate, (XPointer)renderdata
+		);
+	}
+
 	XClearWindow(renderdata->dpy, renderdata->xwindow);
 	XMapRaised(renderdata->dpy, renderdata->xwindow);
 
+	renderdata->xkb_supported = 1;
+	renderdata->xkb_event_type = 0;
+	if (!XkbQueryExtension(renderdata->dpy, 0, &renderdata->xkb_event_type, NULL, &major, &minor)) {
+		ltk_warn("XKB not supported.\n");
+		renderdata->xkb_supported = 0;
+	} else {
+		/* This should select the events when the keyboard mapping changes.
+		 * When e.g. 'setxkbmap us' is executed, two events are sent, but I
+		 * haven't figured out how to change that. When the xkb layout
+		 * switching is used (e.g. 'setxkbmap -option grp:shifts_toggle'),
+		 * this issue does not occur because only a state event is sent. */
+		XkbSelectEvents(
+		    renderdata->dpy, XkbUseCoreKbd,
+		    XkbNewKeyboardNotifyMask, XkbNewKeyboardNotifyMask
+		);
+		XkbSelectEventDetails(
+		    renderdata->dpy, XkbUseCoreKbd, XkbStateNotify,
+		    XkbAllStateComponentsMask, XkbGroupStateMask
+		);
+	}
+
 	return renderdata;
 }
 
 void
 renderer_destroy_window(ltk_renderdata *renderdata) {
 	XFreeGC(renderdata->dpy, renderdata->gc);
+	if (renderdata->spotlist)
+		XFree(renderdata->spotlist);
 	XDestroyWindow(renderdata->dpy, renderdata->xwindow);
 	XCloseDisplay(renderdata->dpy);
 	ltk_free(renderdata);
@@ -447,10 +561,8 @@ renderer_destroy_window(ltk_renderdata *renderdata) {
 /* FIXME: this is a completely random collection of properties and should be
    changed to a more sensible list */
 void
-renderer_set_window_properties(ltk_renderdata *renderdata, ltk_color *bg, ltk_color *border, unsigned int border_width) {
-	XSetWindowBorder(renderdata->dpy, renderdata->xwindow, border->xcolor.pixel);
+renderer_set_window_properties(ltk_renderdata *renderdata, ltk_color *bg) {
 	XSetWindowBackground(renderdata->dpy, renderdata->xwindow, bg->xcolor.pixel);
-	XSetWindowBorderWidth(renderdata->dpy, renderdata->xwindow, border_width);
 }
 
 void
diff --git a/src/grid.c b/src/grid.c
@@ -39,7 +39,7 @@
 
 static void ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight);
 static void ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight);
-static void ltk_grid_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
 static ltk_grid *ltk_grid_create(ltk_window *window, const char *id,
     int rows, int columns);
 static void ltk_grid_destroy(ltk_widget *self, int shallow);
@@ -52,6 +52,17 @@ 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);
 
+static ltk_widget *ltk_grid_prev_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_grid_next_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_grid_first_child(ltk_widget *self);
+static ltk_widget *ltk_grid_last_child(ltk_widget *self);
+
+static ltk_widget *ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect);
+static ltk_widget *ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget);
+
 static struct ltk_widget_vtable vtable = {
 	.draw = <k_grid_draw,
 	.destroy = <k_grid_destroy,
@@ -68,6 +79,15 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_enter = NULL,
 	.key_press = NULL,
 	.key_release = NULL,
+	.prev_child = <k_grid_prev_child,
+	.next_child = <k_grid_next_child,
+	.first_child = <k_grid_first_child,
+	.last_child = <k_grid_last_child,
+	.nearest_child = <k_grid_nearest_child,
+	.nearest_child_left = <k_grid_nearest_child_left,
+	.nearest_child_right = <k_grid_nearest_child_right,
+	.nearest_child_above = <k_grid_nearest_child_above,
+	.nearest_child_below = <k_grid_nearest_child_below,
 	.type = LTK_WIDGET_GRID,
 	.flags = 0,
 };
@@ -111,14 +131,15 @@ ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight) {
 }
 
 static void
-ltk_grid_draw(ltk_widget *self, ltk_rect clip) {
+ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
 	ltk_grid *grid = (ltk_grid *)self;
 	int i;
+	ltk_rect real_clip = ltk_rect_intersect((ltk_rect){0, 0, self->lrect.w, self->lrect.h}, clip);
 	for (i = 0; i < grid->rows * grid->columns; i++) {
 		if (!grid->widget_grid[i])
 			continue;
 		ltk_widget *ptr = grid->widget_grid[i];
-		ptr->vtable->draw(ptr, clip);
+		ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
 	}
 }
 
@@ -164,6 +185,7 @@ static void
 ltk_grid_destroy(ltk_widget *self, int shallow) {
 	ltk_grid *grid = (ltk_grid *)self;
 	ltk_widget *ptr;
+	ltk_error err;
 	for (int i = 0; i < grid->rows * grid->columns; i++) {
 		if (grid->widget_grid[i]) {
 			ptr = grid->widget_grid[i];
@@ -176,7 +198,7 @@ ltk_grid_destroy(ltk_widget *self, int shallow) {
 						grid->widget_grid[r * grid->columns + c] = NULL;
 					}
 				}
-				ptr->vtable->destroy(ptr, shallow);
+				ltk_widget_destroy(ptr, shallow, &err);
 			}
 		}
 	}
@@ -187,8 +209,6 @@ ltk_grid_destroy(ltk_widget *self, int shallow) {
 	ltk_free(grid->column_weights);
 	ltk_free(grid->row_pos);
 	ltk_free(grid->column_pos);
-	ltk_remove_widget(self->id);
-	ltk_free(self->id);
 	ltk_free(grid);
 }
 
@@ -213,10 +233,10 @@ ltk_recalculate_grid(ltk_widget *self) {
 		}
 	}
 	if (total_row_weight > 0) {
-		height_unit = (float) (grid->widget.rect.h - height_static) / (float) total_row_weight;
+		height_unit = (float) (grid->widget.lrect.h - height_static) / (float) total_row_weight;
 	}
 	if (total_column_weight > 0) {
-		width_unit = (float) (grid->widget.rect.w - width_static) / (float) total_column_weight;
+		width_unit = (float) (grid->widget.lrect.w - width_static) / (float) total_column_weight;
 	}
 	for (i = 0; i < grid->rows; i++) {
 		grid->row_pos[i] = currenty;
@@ -238,20 +258,18 @@ ltk_recalculate_grid(ltk_widget *self) {
 	int end_column, end_row;
 	for (i = 0; i < grid->rows; i++) {
 		for (j = 0; j < grid->columns; j++) {
-			if (!grid->widget_grid[i * grid->columns + j])
-				continue;
 			ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
-			if (ptr->row != i || ptr->column != j)
+			if (!ptr || ptr->row != i || ptr->column != j)
 				continue;
-			/*orig_width = ptr->rect.w;
-			orig_height = ptr->rect.h;*/
+			/*orig_width = ptr->lrect.w;
+			orig_height = ptr->lrect.h;*/
 			end_row = i + ptr->row_span;
 			end_column = j + ptr->column_span;
 			if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT) {
-				ptr->rect.w = grid->column_pos[end_column] - grid->column_pos[j];
+				ptr->lrect.w = grid->column_pos[end_column] - grid->column_pos[j];
 			}
 			if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM) {
-				ptr->rect.h = grid->row_pos[end_row] - grid->row_pos[i];
+				ptr->lrect.h = grid->row_pos[end_row] - grid->row_pos[i];
 			}
 			/* 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
@@ -261,24 +279,25 @@ ltk_recalculate_grid(ltk_widget *self) {
 			   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)*/
+			/*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
 				ltk_widget_resize(ptr);
 
 			if (ptr->sticky & LTK_STICKY_RIGHT) {
-				ptr->rect.x = grid->column_pos[end_column] - ptr->rect.w;
+				ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
 			} else if (ptr->sticky & LTK_STICKY_LEFT) {
-				ptr->rect.x = grid->column_pos[j];
+				ptr->lrect.x = grid->column_pos[j];
 			} else {
-				ptr->rect.x = grid->column_pos[j] + ((grid->column_pos[end_column] - grid->column_pos[j]) / 2 - ptr->rect.w / 2);
+				ptr->lrect.x = grid->column_pos[j] + ((grid->column_pos[end_column] - grid->column_pos[j]) / 2 - ptr->lrect.w / 2);
 			}
 
 			if (ptr->sticky & LTK_STICKY_BOTTOM) {
-				ptr->rect.y = grid->row_pos[end_row] - ptr->rect.h;
+				ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
 			} else if (ptr->sticky & LTK_STICKY_TOP) {
-				ptr->rect.y = grid->row_pos[i];
+				ptr->lrect.y = grid->row_pos[i];
 			} else {
-				ptr->rect.y = grid->row_pos[i] + ((grid->row_pos[end_row] - grid->row_pos[i]) / 2 - ptr->rect.h / 2);
+				ptr->lrect.y = grid->row_pos[i] + ((grid->row_pos[end_row] - grid->row_pos[i]) / 2 - ptr->lrect.h / 2);
 			}
+			ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
 		}
 	}
 }
@@ -288,29 +307,27 @@ static void
 ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
 	ltk_grid *grid = (ltk_grid *)self;
 	short size_changed = 0;
-	/* FIXME: this is kind of hacky right now because a size_change needs to be
-	   checked here as well so the surface (if needed) is resized */
-	int orig_w = widget->rect.w;
-	int orig_h = widget->rect.h;
-	widget->rect.w = widget->ideal_w;
-	widget->rect.h = widget->ideal_h;
+	int orig_w = widget->lrect.w;
+	int orig_h = widget->lrect.h;
+	widget->lrect.w = widget->ideal_w;
+	widget->lrect.h = widget->ideal_h;
 	if (grid->column_weights[widget->column] == 0 &&
-	    widget->rect.w > grid->column_widths[widget->column]) {
-		grid->widget.ideal_w += widget->rect.w - grid->column_widths[widget->column];
-		grid->column_widths[widget->column] = widget->rect.w;
+	    widget->lrect.w > grid->column_widths[widget->column]) {
+		grid->widget.ideal_w += widget->lrect.w - grid->column_widths[widget->column];
+		grid->column_widths[widget->column] = widget->lrect.w;
 		size_changed = 1;
 	}
 	if (grid->row_weights[widget->row] == 0 &&
-	    widget->rect.h > grid->row_heights[widget->row]) {
-		grid->widget.ideal_h += widget->rect.h - grid->row_heights[widget->row];
-		grid->row_heights[widget->row] = widget->rect.h;
+	    widget->lrect.h > grid->row_heights[widget->row]) {
+		grid->widget.ideal_h += widget->lrect.h - grid->row_heights[widget->row];
+		grid->row_heights[widget->row] = widget->lrect.h;
 		size_changed = 1;
 	}
 	if (size_changed && grid->widget.parent && grid->widget.parent->vtable->child_size_change)
 		grid->widget.parent->vtable->child_size_change(grid->widget.parent, (ltk_widget *)grid);
 	else
 		ltk_recalculate_grid((ltk_widget *)grid);
-	if (widget->rect.w != orig_w || widget->rect.h != orig_h)
+	if (widget->lrect.w != orig_w || widget->lrect.h != orig_h)
 		ltk_widget_resize(widget);
 }
 
@@ -338,7 +355,7 @@ ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
 	}
 	widget->parent = (ltk_widget *)grid;
 	ltk_grid_child_size_change((ltk_widget *)grid, widget);
-	ltk_window_invalidate_rect(window, grid->widget.rect);
+	ltk_window_invalidate_widget_rect(window, &grid->widget);
 
 	return 0;
 }
@@ -356,7 +373,7 @@ ltk_grid_ungrid(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
 			grid->widget_grid[i * grid->columns + j] = NULL;
 		}
 	}
-	ltk_window_invalidate_rect(widget->window, grid->widget.rect);
+	ltk_window_invalidate_widget_rect(self->window, &grid->widget);
 
 	return 0;
 }
@@ -383,6 +400,82 @@ ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
 	return -1;
 }
 
+/* FIXME: maybe come up with a more efficient method */
+static ltk_widget *
+ltk_grid_nearest_child(ltk_widget *self, ltk_rect rect) {
+	ltk_grid *grid = (ltk_grid *)self;
+	ltk_widget *minw = NULL;
+	int min_dist = INT_MAX;
+	int cx = rect.x + rect.w / 2;
+	int cy = rect.y + rect.h / 2;
+	ltk_rect r;
+	int dist;
+	/* FIXME: rows and columns shouldn't be int */
+	for (size_t i = 0; i < (size_t)(grid->rows * grid->columns); i++) {
+		if (!grid->widget_grid[i])
+			continue;
+		/* FIXME: this checks widgets with row/columnspan > 1 multiple times */
+		r = grid->widget_grid[i]->lrect;
+		dist = abs((r.x + r.w / 2) - cx) + abs((r.y + r.h / 2) - cy);
+		if (dist < min_dist) {
+			min_dist = dist;
+			minw = grid->widget_grid[i];
+		}
+	}
+	return minw;
+}
+
+/* FIXME: assertions to check that widget row/column are legal */
+static ltk_widget *
+ltk_grid_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
+	ltk_grid *grid = (ltk_grid *)self;
+	unsigned int col = widget->column;
+	ltk_widget *cur = NULL;
+	while (col-- > 0) {
+		cur = grid->widget_grid[widget->row * grid->columns + col];
+		if (cur && cur != widget)
+			return cur;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
+	ltk_grid *grid = (ltk_grid *)self;
+	ltk_widget *cur = NULL;
+	for (int col = widget->column + 1; col < grid->columns; col++) {
+		cur = grid->widget_grid[widget->row * grid->columns + col];
+		if (cur && cur != widget)
+			return cur;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
+	ltk_grid *grid = (ltk_grid *)self;
+	unsigned int row = widget->row;
+	ltk_widget *cur = NULL;
+	while (row-- > 0) {
+		cur = grid->widget_grid[row * grid->columns + widget->column];
+		if (cur && cur != widget)
+			return cur;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
+	ltk_grid *grid = (ltk_grid *)self;
+	ltk_widget *cur = NULL;
+	for (int row = widget->row + 1; row < grid->rows; row++) {
+		cur = grid->widget_grid[row * grid->columns + widget->column];
+		if (cur && cur != widget)
+			return cur;
+	}
+	return NULL;
+}
+
 static ltk_widget *
 ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y) {
 	ltk_grid *grid = (ltk_grid *)self;
@@ -391,11 +484,53 @@ ltk_grid_get_child_at_pos(ltk_widget *self, int x, int y) {
 	if (row == -1 || column == -1)
 		return 0;
 	ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
-	if (ptr && ltk_collide_rect(ptr->rect, x, y))
+	if (ptr && ltk_collide_rect(ptr->crect, x, y))
 		return ptr;
 	return NULL;
 }
 
+static ltk_widget *
+ltk_grid_prev_child(ltk_widget *self, ltk_widget *child) {
+	ltk_grid *grid = (ltk_grid *)self;
+	unsigned int start = child->row * grid->columns + child->column;
+	while (start-- > 0) {
+		if (grid->widget_grid[start])
+			return grid->widget_grid[start];
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_next_child(ltk_widget *self, ltk_widget *child) {
+	ltk_grid *grid = (ltk_grid *)self;
+	unsigned int start = child->row * grid->columns + child->column;
+	while (++start < (unsigned int)(grid->rows * grid->columns)) {
+		if (grid->widget_grid[start] && grid->widget_grid[start] != child)
+			return grid->widget_grid[start];
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_first_child(ltk_widget *self) {
+	ltk_grid *grid = (ltk_grid *)self;
+	for (unsigned int i = 0; i < (unsigned int)(grid->rows * grid->columns); i++) {
+		if (grid->widget_grid[i])
+			return grid->widget_grid[i];
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_grid_last_child(ltk_widget *self) {
+	ltk_grid *grid = (ltk_grid *)self;
+	for (unsigned int i = grid->rows * grid->columns; i-- > 0;) {
+		if (grid->widget_grid[i])
+			return grid->widget_grid[i];
+	}
+	return NULL;
+}
+
 /* grid <grid id> add <widget id> <row> <column> <row_span> <column_span> [sticky] */
 static int
 ltk_grid_cmd_add(
diff --git a/src/label.c b/src/label.c
@@ -35,7 +35,7 @@
 
 #define MAX_LABEL_PADDING 500
 
-static void ltk_label_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_label_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
 static ltk_label *ltk_label_create(ltk_window *window,
     const char *id, char *text);
 static void ltk_label_destroy(ltk_widget *self, int shallow);
@@ -58,12 +58,13 @@ static struct ltk_widget_vtable vtable = {
 	.mouse_leave = NULL,
 	.mouse_enter = NULL,
 	.type = LTK_WIDGET_LABEL,
-	.flags = LTK_NEEDS_REDRAW,
+	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_SPECIAL,
 };
 
 static struct {
 	ltk_color text_color;
 	ltk_color bg_color;
+	ltk_color bg_color_active;
 	int pad;
 } theme;
 
@@ -71,6 +72,7 @@ int parseinfo_sorted = 0;
 
 static ltk_theme_parseinfo parseinfo[] = {
 	{"bg-color", THEME_COLOR, {.color = &theme.bg_color}, {.color = "#000000"}, 0, 0, 0},
+	{"bg-color-active", THEME_COLOR, {.color = &theme.bg_color_active}, {.color = "#222222"}, 0, 0, 0},
 	{"pad", THEME_INT, {.i = &theme.pad}, {.i = 5}, 0, MAX_LABEL_PADDING, 0},
 	{"text-color", THEME_COLOR, {.color = &theme.text_color}, {.color = "#FFFFFF"}, 0, 0, 0},
 };
@@ -91,23 +93,25 @@ ltk_label_uninitialize_theme(ltk_window *window) {
 }
 
 static void
-ltk_label_draw(ltk_widget *self, ltk_rect clip) {
+ltk_label_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
 	ltk_label *label = (ltk_label *)self;
-	ltk_rect rect = label->widget.rect;
-	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
 	ltk_surface *s;
-	ltk_surface_cache_request_surface_size(label->key, self->rect.w, self->rect.h);
+	ltk_surface_cache_request_surface_size(label->key, lrect.w, lrect.h);
 	if (!ltk_surface_cache_get_surface(label->key, &s) || self->dirty)
 		ltk_label_redraw_surface(label, s);
-	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
+	ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
 }
 
 static void
 ltk_label_redraw_surface(ltk_label *label, ltk_surface *s) {
-	ltk_rect r = label->widget.rect;
+	ltk_rect r = label->widget.lrect;
 	r.x = 0;
 	r.y = 0;
-	ltk_surface_fill_rect(s, &theme.bg_color, r);
+	ltk_surface_fill_rect(s, (label->widget.state & LTK_ACTIVE) ? &theme.bg_color_active : &theme.bg_color, r);
 
 	int text_w, text_h;
 	ltk_text_line_get_size(label->tl, &text_w, &text_h);
@@ -124,9 +128,9 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
 	label->tl = ltk_text_line_create(window->text_context, font_size, text, 0, -1);
 	int text_w, text_h;
 	ltk_text_line_get_size(label->tl, &text_w, &text_h);
+	ltk_fill_widget_defaults(&label->widget, id, window, &vtable, label->widget.ideal_w, label->widget.ideal_h);
 	label->widget.ideal_w = text_w + theme.pad * 2;
 	label->widget.ideal_h = text_h + theme.pad * 2;
-	ltk_fill_widget_defaults(&label->widget, id, window, &vtable, label->widget.ideal_w, label->widget.ideal_h);
 	label->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, label->widget.ideal_w, label->widget.ideal_h);
 
 	return label;
@@ -142,8 +146,6 @@ ltk_label_destroy(ltk_widget *self, int shallow) {
 	}
 	ltk_surface_cache_release_key(label->key);
 	ltk_text_line_destroy(label->tl);
-	ltk_remove_widget(self->id);
-	ltk_free(self->id);
 	ltk_free(label);
 }
 
diff --git a/src/ltk.h b/src/ltk.h
@@ -60,6 +60,7 @@ struct ltk_window {
 	ltk_rect rect;
 	ltk_window_theme *theme;
 	ltk_rect dirty_rect;
+	size_t cur_kbd;
 	/* FIXME: generic array */
 	ltk_widget **popups;
 	size_t popups_num;
@@ -84,7 +85,7 @@ void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
 void ltk_queue_event(ltk_window *window, ltk_userevent_type type, const char *id, const char *data);
 void ltk_window_set_hover_widget(ltk_window *window, ltk_widget *widget, ltk_motion_event *event);
 void ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget);
-void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget);
+void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release);
 void ltk_quit(ltk_window *window);
 
 void ltk_unregister_timer(int timer_id);
@@ -97,4 +98,8 @@ int ltk_queue_specific_event(ltk_widget *widget, const char *type, uint32_t mask
 int ltk_queue_sock_write(int client, const char *str, int len);
 int ltk_queue_sock_write_fmt(int client, const char *fmt, ...);
 
+ltk_point ltk_widget_pos_to_global(ltk_widget *widget, int x, int y);
+ltk_point ltk_global_to_widget_pos(ltk_widget *widget, int x, int y);
+void ltk_window_invalidate_widget_rect(ltk_window *window, ltk_widget *widget);
+
 #endif
diff --git a/src/ltkd.c b/src/ltkd.c
@@ -29,6 +29,7 @@
 #include <unistd.h>
 #include <signal.h>
 #include <stdint.h>
+#include <locale.h>
 #include <inttypes.h>
 
 #include <sys/un.h>
@@ -57,8 +58,8 @@
 #include "box.h"
 #include "menu.h"
 #include "macros.h"
+#include "config.h"
 
-#define MAX_WINDOW_BORDER_WIDTH 100
 #define MAX_WINDOW_FONT_SIZE 200
 
 #define MAX_SOCK_CONNS 20
@@ -143,7 +144,10 @@ static char *sock_path = NULL;
    global originally, but that's just the way it is. */
 static ltk_window *main_window = NULL;
 
-int main(int argc, char *argv[]) {
+int
+main(int argc, char *argv[]) {
+	setlocale(LC_CTYPE, "");
+	XSetLocaleModifiers("");
 	int ch;
 	char *title = "LTK Window";
 	while ((ch = getopt(argc, argv, "dt:")) != -1) {
@@ -284,6 +288,10 @@ ltk_mainloop(ltk_window *window) {
 
 	/* FIXME: framerate limiting for draw */
 
+	/* initialize keyboard mapping */
+	ltk_generate_keyboard_event(window->renderdata, &event);
+	ltk_handle_event(window, &event);
+
 	while (running) {
 		rfds = sock_state.rallfds;
 		wfds = sock_state.wallfds;
@@ -295,7 +303,7 @@ ltk_mainloop(ltk_window *window) {
 		   necessary framerate-limiting delay is already done */
 		wretval = select(sock_state.maxfd + 1, NULL, &wfds, NULL, &tv);
 		while (ltk_events_pending(window->renderdata)) {
-			ltk_next_event(window->renderdata, &event);
+			ltk_next_event(window->renderdata, window->cur_kbd, &event);
 			ltk_handle_event(window, &event);
 		}
 
@@ -481,7 +489,9 @@ ltk_cleanup(void) {
 			ltk_free(sockets[i].tokens.tokens);
 	}
 
+	ltk_config_cleanup();
 	ltk_widgets_cleanup();
+	ltk_events_cleanup();
 	if (main_window) {
 		ltk_uninitialize_theme(main_window);
 		ltk_destroy_window(main_window);
@@ -530,9 +540,12 @@ ltk_set_root_widget_cmd(
 		return 1;
 	}
 	window->root_widget = widget;
-	ltk_window_invalidate_rect(window, widget->rect);
-	widget->rect.w = window->rect.w;
-	widget->rect.h = window->rect.h;
+	widget->lrect.x = 0;
+	widget->lrect.y = 0;
+	widget->lrect.w = window->rect.w;
+	widget->lrect.h = window->rect.h;
+	widget->crect = widget->lrect;
+	ltk_window_invalidate_rect(window, widget->lrect);
 	ltk_widget_resize(widget);
 
 	return 0;
@@ -546,6 +559,38 @@ ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
 		window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
 }
 
+ltk_point
+ltk_widget_pos_to_global(ltk_widget *widget, int x, int y) {
+	ltk_widget *cur = widget;
+	while (cur) {
+		x += cur->lrect.x;
+		y += cur->lrect.y;
+		if (cur->popup)
+			break;
+		cur = cur->parent;
+	}
+	return (ltk_point){x, y};
+}
+
+ltk_point
+ltk_global_to_widget_pos(ltk_widget *widget, int x, int y) {
+	ltk_widget *cur = widget;
+	while (cur) {
+		x -= cur->lrect.x;
+		y -= cur->lrect.y;
+		if (cur->popup)
+			break;
+		cur = cur->parent;
+	}
+	return (ltk_point){x, y};
+}
+
+void
+ltk_window_invalidate_widget_rect(ltk_window *window, ltk_widget *widget) {
+	ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
+	ltk_window_invalidate_rect(window, (ltk_rect){glob.x, glob.y, widget->lrect.w, widget->lrect.h});
+}
+
 /* FIXME: generic event handling functions that take the actual uint32_t event type, etc. */
 int
 ltk_queue_specific_event(ltk_widget *widget, const char *type, uint32_t mask, const char *data) {
@@ -586,12 +631,12 @@ ltk_redraw_window(ltk_window *window) {
 	ltk_surface_fill_rect(window->surface, &window->theme->bg, (ltk_rect){0, 0, window->rect.w, window->rect.h});
 	if (window->root_widget) {
 		ptr = window->root_widget;
-		ptr->vtable->draw(ptr, window->rect);
+		ptr->vtable->draw(ptr, window->surface, 0, 0, window->rect);
 	}
 	/* last popup is the newest one, so draw that last */
 	for (size_t i = 0; i < window->popups_num; i++) {
 		ptr = window->popups[i];
-		ptr->vtable->draw(ptr, window->rect);
+		ptr->vtable->draw(ptr, window->surface, ptr->lrect.x, ptr->lrect.y, ltk_rect_relative(ptr->lrect, window->rect));
 	}
 	renderer_swap_buffers(window->renderdata);
 }
@@ -612,8 +657,9 @@ ltk_window_other_event(ltk_window *window, ltk_event *event) {
 			ltk_window_invalidate_rect(window, window->rect);
 			ltk_surface_update_size(window->surface, w, h);
 			if (ptr) {
-				ptr->rect.w = w;
-				ptr->rect.h = h;
+				ptr->lrect.w = w;
+				ptr->lrect.h = h;
+				ptr->crect = ptr->lrect;
 				ltk_widget_resize(ptr);
 			}
 		}
@@ -696,6 +742,7 @@ ltk_window_register_popup(ltk_window *window, ltk_widget *popup) {
 		);
 	}
 	window->popups[window->popups_num++] = popup;
+	popup->popup = 1;
 }
 
 void
@@ -704,6 +751,7 @@ ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup) {
 		return;
 	for (size_t i = 0; i < window->popups_num; i++) {
 		if (window->popups[i] == popup) {
+			popup->popup = 0;
 			memmove(
 			    window->popups + i,
 			    window->popups + i + 1,
@@ -730,6 +778,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;
+		window->popups[i]->popup = 0;
 		ltk_widget_hide(window->popups[i]);
 	}
 	window->popups_num = 0;
@@ -747,7 +796,6 @@ ltk_window_unregister_all_popups(ltk_window *window) {
 
 ltk_window_theme window_theme;
 static ltk_theme_parseinfo theme_parseinfo[] = {
-	{"border-width", THEME_INT, {.i = &window_theme.border_width}, {.i = 0}, 0, MAX_WINDOW_BORDER_WIDTH, 0},
 	{"font-size", THEME_INT, {.i = &window_theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0},
 	{"font", THEME_STRING, {.str = &window_theme.font}, {.str = "Liberation Mono"}, 0, 0, 0},
 	{"bg", THEME_COLOR, {.color = &window_theme.bg}, {.color = "#000000"}, 0, 0, 0},
@@ -831,10 +879,24 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 	window->popups = NULL;
 	window->popups_num = window->popups_alloc = 0;
 	window->popups_locked = 0;
+	window->cur_kbd = 0;
 
 	ltk_renderdata *renderer_create_window(const char *title, int x, int y, unsigned int w, unsigned int h);
-	void renderer_set_window_properties(ltk_renderdata *renderdata, ltk_color *bg_pixel, ltk_color *border_pixel, unsigned int border_width);
 	window->renderdata = renderer_create_window(title, x, y, w, h);
+	/* FIXME: search different directories for config */
+	char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
+	char *errstr = NULL;
+	if (ltk_config_parsefile(config_path, &errstr)) {
+		if (errstr) {
+			ltk_warn("Unable to load config: %s\n", errstr);
+			ltk_free(errstr);
+		}
+		if (ltk_config_load_default(&errstr)) {
+			/* FIXME: I guess errstr isn't freed here, but whatever */
+			ltk_fatal("Unable to load default config: %s\n", errstr);
+		}
+	}
+	ltk_free(config_path);
 	theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
 	window->theme = &window_theme;
 	ltk_load_theme(window, theme_path);
@@ -842,7 +904,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
 
 	/* FIXME: fix theme memory leaks when exit happens between here and the end of this function */
 
-	renderer_set_window_properties(window->renderdata, &window->theme->bg, &window->theme->fg, window->theme->border_width);
+	renderer_set_window_properties(window->renderdata, &window->theme->bg);
 
 	window->root_widget = NULL;
 	window->hover_widget = NULL;
@@ -879,22 +941,34 @@ ltk_destroy_window(ltk_window *window) {
 	ltk_free(window);
 }
 
+/* event must have global coordinates! */
 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;
+	int orig_x = event->x, orig_y = event->y;
 	if (old) {
 		ltk_widget_state old_state = old->state;
 		old->state &= ~LTK_HOVER;
 		ltk_widget_change_state(old, old_state);
-		if (old->vtable->mouse_leave)
+		if (old->vtable->mouse_leave) {
+			ltk_point local = ltk_global_to_widget_pos(old, event->x, event->y);
+			event->x = local.x;
+			event->y = local.y;
 			old->vtable->mouse_leave(old, event);
+			event->x = orig_x;
+			event->y = orig_y;
+		}
 	}
 	window->hover_widget = widget;
 	if (widget) {
-		if (widget->vtable->mouse_enter)
+		if (widget->vtable->mouse_enter) {
+			ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+			event->x = local.x;
+			event->y = local.y;
 			widget->vtable->mouse_enter(widget, event);
+		}
 		ltk_widget_state old_state = widget->state;
 		widget->state |= LTK_HOVER;
 		ltk_widget_change_state(widget, old_state);
@@ -922,6 +996,9 @@ ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
 			}
 			ltk_widget_state old_state = cur->state;
 			cur->state |= LTK_ACTIVE;
+			/* FIXME: should all be set focused? */
+			if (cur == widget && !(cur->vtable->flags & LTK_NEEDS_KEYBOARD))
+				widget->state |= LTK_FOCUSED;
 			ltk_widget_change_state(cur, old_state);
 			cur = cur->parent;
 		}
@@ -948,21 +1025,25 @@ ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
 }
 
 void
-ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
+ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release) {
 	if (window->pressed_widget == widget)
 		return;
-	/* 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);
+		/* FIXME: this is a bit weird because the release handler for menuentry
+		   indirectly calls ltk_widget_hide, which messes with the pressed widget */
+		/* FIXME: isn't it redundant to check that state is pressed? */
+		if (release && window->pressed_widget->vtable->release && (old_state & LTK_PRESSED)) {
+			window->pressed_widget->vtable->release(window->pressed_widget);
+		}
 	}
 	window->pressed_widget = widget;
 	if (widget) {
+		if (widget->vtable->press)
+			widget->vtable->press(widget);
 		ltk_widget_state old_state = widget->state;
 		widget->state |= LTK_PRESSED;
 		ltk_widget_change_state(widget, old_state);
@@ -971,10 +1052,13 @@ ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
 
 static void
 ltk_handle_event(ltk_window *window, ltk_event *event) {
+	size_t kbd_idx;
 	switch (event->type) {
 	case LTK_KEYPRESS_EVENT:
+		ltk_window_key_press_event(window, &event->key);
 		break;
 	case LTK_KEYRELEASE_EVENT:
+		ltk_window_key_release_event(window, &event->key);
 		break;
 	case LTK_BUTTONPRESS_EVENT:
 		ltk_window_mouse_press_event(window, &event->button);
@@ -985,6 +1069,13 @@ ltk_handle_event(ltk_window *window, ltk_event *event) {
 	case LTK_MOTION_EVENT:
 		ltk_window_motion_notify_event(window, &event->motion);
 		break;
+	case LTK_KEYBOARDCHANGE_EVENT:
+		/* FIXME: emit event */
+		if (ltk_config_get_language_index(event->keyboard.new_kbd, &kbd_idx))
+			ltk_warn("No language mapping for language \"%s\".\n", event->keyboard.new_kbd);
+		else
+			window->cur_kbd = kbd_idx;
+		break;
 	default:
 		if (window->other_event)
 			window->other_event(window, event);
@@ -1317,6 +1408,7 @@ handle_mask_command(int client, char **tokens, size_t num_tokens, ltk_error *err
 	} else {
 		err->type = ERR_INVALID_ARGUMENT;
 		err->arg = 2;
+		return 1;
 	}
 	if (num_tokens == 5) {
 		if (!strcmp(tokens[4], "lock")) {
diff --git a/src/memory.c b/src/memory.c
@@ -40,6 +40,24 @@ ltk_strdup_debug(const char *s, const char *caller, const char *file, int line) 
 	return str;
 }
 
+char *
+ltk_strndup_impl(const char *s, size_t n) {
+	char *str = strndup(s, n);
+	if (!str)
+		ltk_fatal("Out of memory.\n");
+	return str;
+}
+
+char *
+ltk_strndup_debug(const char *s, size_t n, const char *caller, const char *file, int line) {
+	char *str = strndup(s, n);
+	fprintf(stderr, "DEBUG: strndup %p to %p in %s (%s:%d)\n",
+		(void *)s, (void *)str, caller, file, line);
+	if (!str)
+		ltk_fatal("Out of memory.\n");
+	return str;
+}
+
 void *
 ltk_malloc_impl(size_t size) {
 	void *ptr = malloc(size);
@@ -137,3 +155,20 @@ ideal_array_size(size_t old, size_t needed) {
 		ret = 1; /* not sure if this is necessary */
 	return ret;
 }
+
+char *
+ltk_print_fmt(char *fmt, ...) {
+	va_list args;
+	va_start(args, fmt);
+	int len = vsnprintf(NULL, 0, fmt, args);
+	/* FIXME: what should be done on error? */
+	if (len < 0)
+		ltk_fatal("Error in vsnprintf called from print_fmt");
+	/* FIXME: overflow */
+	char *str = ltk_malloc(len + 1);
+	va_end(args);
+	va_start(args, fmt);
+	vsnprintf(str, len + 1, fmt, args);
+	va_end(args);
+	return str;
+}
diff --git a/src/memory.h b/src/memory.h
@@ -14,21 +14,23 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
-#ifndef _LTK_MEMORY_H_
-#define _LTK_MEMORY_H_
+#ifndef LTK_MEMORY_H
+#define LTK_MEMORY_H
 
 /* FIXME: Move ltk_warn, etc. to util.* */
 
-/* Requires: <stdlib.h> */
+#include <stdlib.h>
 
-#if DEV == 1
+#if MEMDEBUG == 1
   #define ltk_strdup(s) ltk_strdup_debug(s, __func__, __FILE__, __LINE__)
+  #define ltk_strndup(s, n) ltk_strndup_debug(s, n, __func__, __FILE__, __LINE__)
   #define ltk_malloc(size) ltk_malloc_debug(size, __func__, __FILE__, __LINE__)
   #define ltk_calloc(nmemb, size) ltk_calloc_debug(nmemb, size, __func__, __FILE__, __LINE__)
   #define ltk_realloc(ptr, size) ltk_realloc_debug(ptr, size, __func__, __FILE__, __LINE__)
   #define ltk_free(ptr) ltk_free_debug(ptr, __func__, __FILE__, __LINE__)
 #else
   #define ltk_strdup(s) ltk_strdup_impl(s)
+  #define ltk_strndup(s, n) ltk_strndup_impl(s, n)
   #define ltk_malloc(size) ltk_malloc_impl(size);
   #define ltk_calloc(nmemb, size) ltk_calloc_impl(nmemb, size);
   #define ltk_realloc(ptr, size) ltk_realloc_impl(ptr, size);
@@ -36,11 +38,13 @@
 #endif
 
 char *ltk_strdup_impl(const char *s);
+char *ltk_strndup_impl(const char *s, size_t n);
 void *ltk_malloc_impl(size_t size);
 void *ltk_calloc_impl(size_t nmemb, size_t size);
 void *ltk_realloc_impl(void *ptr, size_t size);
 
 char *ltk_strdup_debug(const char *s, const char *caller, const char *file, int line);
+char *ltk_strndup_debug(const char *s, size_t n, const char *caller, const char *file, int line);
 void *ltk_malloc_debug(size_t size, const char *caller, const char *file, int line);
 void *ltk_calloc_debug(size_t nmemb, size_t size, const char *caller, const char *file, int line);
 void *ltk_realloc_debug(void *ptr, size_t size, const char *caller, const char *file, int line);
@@ -49,4 +53,9 @@ void *ltk_reallocarray(void *optr, size_t nmemb, size_t size);
 
 size_t ideal_array_size(size_t old, size_t needed);
 
-#endif /* _LTK_MEMORY_H_ */
+/* This acts like snprintf but automatically allocates
+   a string of the appropriate size.
+   Like the other functions here, it exits on error. */
+char *ltk_print_fmt(char *fmt, ...);
+
+#endif /* LTK_MEMORY_H */
diff --git a/src/menu.c b/src/menu.c
@@ -85,8 +85,9 @@ static struct entry_theme {
 	ltk_color fill_disabled;
 } menu_entry_theme, submenu_entry_theme;
 
+static void ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r);
 static void ltk_menu_resize(ltk_widget *self);
-static void ltk_menu_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
 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);
@@ -106,16 +107,29 @@ 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_widget *ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect);
+static ltk_widget *ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget *widget);
+static ltk_widget *ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget *widget);
+
 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_draw(ltk_widget *self, ltk_surface *s, int x, int y, 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 int ltk_menuentry_release(ltk_widget *self);
 static void ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry);
 static int ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, ltk_error *err);
 static void ltk_menuentry_detach_submenu(ltk_menuentry *e);
 
 static int ltk_menu_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err);
+static int ltk_menuentry_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err);
+
+static ltk_widget *ltk_menu_prev_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_menu_next_child(ltk_widget *self, ltk_widget *child);
+static ltk_widget *ltk_menu_first_child(ltk_widget *self);
+static ltk_widget *ltk_menu_last_child(ltk_widget *self);
+static ltk_widget *ltk_menuentry_get_child(ltk_widget *self);
 
 #define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)e->widget.parent)->is_submenu)
 
@@ -135,6 +149,16 @@ static struct ltk_widget_vtable vtable = {
 	.destroy = <k_menu_destroy,
 	.child_size_change = &recalc_ideal_menu_size,
 	.remove_child = <k_menu_remove_child,
+	.prev_child = <k_menu_prev_child,
+	.next_child = <k_menu_next_child,
+	.first_child = <k_menu_first_child,
+	.last_child = <k_menu_last_child,
+	.nearest_child = <k_menu_nearest_child,
+	.nearest_child_left = <k_menu_nearest_child_left,
+	.nearest_child_right = <k_menu_nearest_child_right,
+	.nearest_child_above = <k_menu_nearest_child_above,
+	.nearest_child_below = <k_menu_nearest_child_below,
+	.ensure_rect_shown = <k_menu_ensure_rect_shown,
 	.type = LTK_WIDGET_MENU,
 	.flags = LTK_NEEDS_REDRAW,
 };
@@ -144,7 +168,8 @@ static struct ltk_widget_vtable entry_vtable = {
 	.key_release = NULL,
 	.mouse_press = NULL,
 	.motion_notify = NULL,
-	.mouse_release = <k_menuentry_mouse_release,
+	.mouse_release = NULL,
+	.release = <k_menuentry_release,
 	.mouse_enter = NULL,
 	.mouse_leave = NULL,
 	.get_child_at_pos = NULL,
@@ -154,7 +179,9 @@ static struct ltk_widget_vtable entry_vtable = {
 	.draw = <k_menuentry_draw,
 	.destroy = <k_menuentry_destroy,
 	.child_size_change = NULL,
-	.remove_child = NULL,
+	.remove_child = <k_menuentry_remove_child,
+	.first_child = <k_menuentry_get_child,
+	.last_child = <k_menuentry_get_child,
 	.type = LTK_WIDGET_MENUENTRY,
 	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
 };
@@ -312,8 +339,16 @@ ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
 	}
 }
 
+static ltk_widget *
+ltk_menuentry_get_child(ltk_widget *self) {
+	ltk_menuentry *e = (ltk_menuentry *)self;
+	if (e->submenu && !e->submenu->widget.hidden)
+		return &e->submenu->widget;
+	return NULL;
+}
+
 static void
-ltk_menuentry_draw(ltk_widget *self, ltk_rect clip) {
+ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
 	/* FIXME: figure out how hidden should work */
 	if (self->hidden)
 		return;
@@ -338,11 +373,12 @@ ltk_menuentry_draw(ltk_widget *self, ltk_rect clip) {
 		border = &t->border;
 		fill = &t->fill;
 	}
-	ltk_rect rect = self->rect;
-	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
 	if (clip_final.w <= 0 || clip_final.h <= 0)
 		return;
-	ltk_surface_fill_rect(self->window->surface, fill, clip_final);
+	ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
+	ltk_surface_fill_rect(draw_surf, fill, surf_clip);
 
 	ltk_surface *s;
 	int text_w, text_h;
@@ -352,70 +388,77 @@ ltk_menuentry_draw(ltk_widget *self, ltk_rect clip) {
 		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;
+	int text_x = t->text_pad + t->border_width;
+	int text_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
+	    s, draw_surf,
+	    (ltk_rect){text_clip.x - text_x, text_clip.y - text_y, text_clip.w, text_clip.h}, x + text_clip.x, y + 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}
+		    {x + lrect.w - t->arrow_pad - t->border_width, y + lrect.h / 2},
+		    {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 - t->arrow_size / 2},
+		    {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 + t->arrow_size / 2}
 		};
-		ltk_surface_fill_polygon_clipped(self->window->surface, text, arrow_points, LENGTH(arrow_points), clip_final);
+		ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), surf_clip);
 	}
-	ltk_surface_draw_border_clipped(self->window->surface, border, rect, clip_final, t->border_width, t->border_sides);
+	ltk_surface_draw_border_clipped(draw_surf, border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, t->border_width, t->border_sides);
 }
 
 static void
-ltk_menu_draw(ltk_widget *self, ltk_rect clip) {
+ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
 	if (self->hidden)
 		return;
 	ltk_menu *menu = (ltk_menu *)self;
-	ltk_rect rect = self->rect;
-	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
-	ltk_surface_fill_rect(self->window->surface, &t->background, self->rect);
+	ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
+	ltk_surface_fill_rect(s, &t->background, surf_clip);
+	ltk_widget *ptr = NULL;
 	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);
+			ptr = &menu->entries[i + 1]->widget;
+			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
+			ptr = &menu->entries[i]->widget;
+			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
 			i++;
 		} else {
-			ltk_menuentry_draw(&menu->entries[i]->widget, clip_final);
+			ptr = &menu->entries[i]->widget;
+			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, 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 ww = self->lrect.w;
+	int wh = self->lrect.h;
+	int wx = x, wy = 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});
+	/* -> this is currently a mess because some parts handle clipping properly, but the scroll arrow drawing doesn't */
+	if (lrect.w < (int)self->ideal_w) {
+		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, sz, wh - mbw * 2});
+		ltk_surface_fill_rect(s, &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);
+		ltk_surface_fill_polygon(s, &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);
+		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
 	}
-	if (rect.h < (int)self->ideal_h) {
+	if (lrect.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] = {
@@ -423,13 +466,13 @@ ltk_menu_draw(ltk_widget *self, ltk_rect clip) {
 		    {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);
+		ltk_surface_fill_polygon(s, &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_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
 	}
-	ltk_surface_draw_border(self->window->surface, &t->border, rect, mbw, LTK_BORDER_ALL);
+	ltk_surface_draw_border_clipped(s, &t->border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, mbw, LTK_BORDER_ALL);
 
 	self->dirty = 0;
 }
@@ -445,41 +488,65 @@ ltk_menu_resize(ltk_widget *self) {
 	if (menu->y_scroll_offset > max_y)
 		menu->y_scroll_offset = max_y;
 
-	ltk_rect rect = self->rect;
+	ltk_rect lrect = self->lrect;
 	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;
+	int start_x = lrect.w < ideal_w ? arrow_size : 0;
+	int start_y = lrect.h < ideal_h ? arrow_size : 0;
 	start_x += t->border_width;
 	start_y += t->border_width;
 
 	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;
+	int cur_abs_x = -(int)menu->x_scroll_offset + start_x + t->pad;
+	int cur_abs_y = -(int)menu->y_scroll_offset + start_y + t->pad;
 
 	for (size_t i = 0; i < menu->num_entries; i++) {
 		ltk_menuentry *e = menu->entries[i];
-		e->widget.rect.x = cur_abs_x;
-		e->widget.rect.y = cur_abs_y;
+		e->widget.lrect.x = cur_abs_x;
+		e->widget.lrect.y = cur_abs_y;
 		if (menu->is_submenu) {
-			e->widget.rect.w = ideal_w - 2 * t->pad - 2 * mbw;
-			e->widget.rect.h = e->widget.ideal_h;
+			e->widget.lrect.w = ideal_w - 2 * t->pad - 2 * mbw;
+			e->widget.lrect.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 {
-			e->widget.rect.w = e->widget.ideal_w;
-			e->widget.rect.h = ideal_h - 2 * t->pad - 2 * mbw;
+			e->widget.lrect.w = e->widget.ideal_w;
+			e->widget.lrect.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;
 		}
+		e->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, e->widget.lrect);
 	}
 	self->dirty = 1;
-	ltk_window_invalidate_rect(self->window, self->rect);
+	ltk_window_invalidate_widget_rect(self->window, self);
+}
+
+static void
+ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
+	ltk_menu *menu = (ltk_menu *)self;
+	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
+	int extra_size = theme->arrow_size + theme->arrow_pad * 2 + theme->border_width;
+	int delta = 0;
+	if (self->lrect.w < (int)self->ideal_w && !menu->is_submenu) {
+		if (r.x + r.w > self->lrect.w - extra_size && r.w <= self->lrect.w - 2 * extra_size)
+			delta = r.x - (self->lrect.w - extra_size - r.w);
+		else if (r.x < extra_size || r.w > self->lrect.w - 2 * extra_size)
+			delta = r.x - extra_size;
+		if (delta)
+			ltk_menu_scroll(menu, 0, 0, 0, 1, delta);
+	} else if (self->lrect.h < (int)self->ideal_h && menu->is_submenu) {
+		if (r.y + r.h > self->lrect.h - extra_size && r.h <= self->lrect.h - 2 * extra_size)
+			delta = r.y - (self->lrect.h - extra_size - r.h);
+		else if (r.y < extra_size || r.h > self->lrect.h - 2 * extra_size)
+			delta = r.y - extra_size;
+		if (delta)
+			ltk_menu_scroll(menu, 0, 1, 0, 0, delta);
+	}
 }
 
 static void
@@ -488,11 +555,11 @@ ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) {
 	int extra_size = theme->arrow_size * 2 + theme->arrow_pad * 4;
 	*x_ret = 0;
 	*y_ret = 0;
-	if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
-		*x_ret = menu->widget.ideal_w - (menu->widget.rect.w - extra_size);
+	if (menu->widget.lrect.w < (int)menu->widget.ideal_w) {
+		*x_ret = menu->widget.ideal_w - (menu->widget.lrect.w - extra_size);
 	}
-	if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
-		*y_ret = menu->widget.ideal_h - (menu->widget.rect.h - extra_size);
+	if (menu->widget.lrect.h < (int)menu->widget.ideal_h) {
+		*y_ret = menu->widget.ideal_h - (menu->widget.lrect.h - extra_size);
 	}
 }
 
@@ -523,7 +590,7 @@ ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
 	    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);
+		ltk_window_invalidate_widget_rect(menu->widget.window, &menu->widget);
 	}
 }
 
@@ -555,21 +622,22 @@ ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
 	int arrow_size = t->arrow_size + t->arrow_pad * 2;
 	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) {
+	int start_x = mbw, end_x = self->lrect.w - mbw;
+	int start_y = mbw, end_y = self->lrect.h - mbw;
+	if (self->lrect.w < (int)self->ideal_w) {
 		start_x += arrow_size;
 		end_x -= arrow_size;
 	}
-	if (self->rect.h < (int)self->ideal_h) {
+	if (self->lrect.h < (int)self->ideal_h) {
 		start_y += arrow_size;
 		end_y -= arrow_size;
 	}
+	/* FIXME: use crect for this */
 	if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
 		return NULL;
 
 	for (size_t i = 0; i < menu->num_entries; i++) {
-		if (ltk_collide_rect(menu->entries[i]->widget.rect, x, y))
+		if (ltk_collide_rect(menu->entries[i]->widget.crect, x, y))
 			return &menu->entries[i]->widget;
 	}
 	return NULL;
@@ -578,21 +646,21 @@ ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
 /* FIXME: make sure timers are always destroyed when widget is destroyed */
 static int
 set_scroll_timer(ltk_menu *menu, int x, int y) {
-	if (!ltk_collide_rect(menu->widget.rect, x, y))
+	if (!ltk_collide_rect(menu->widget.lrect, x, y))
 		return 0;
 	int t = 0, b = 0, l = 0,r = 0;
 	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
 	int arrow_size = theme->arrow_size + theme->arrow_pad * 2;
-	if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
-		if (x < menu->widget.rect.x + arrow_size)
+	if (menu->widget.lrect.w < (int)menu->widget.ideal_w) {
+		if (x < arrow_size)
 			l = 1;
-		else if (x > menu->widget.rect.x + menu->widget.rect.w - arrow_size)
+		else if (x > menu->widget.lrect.w - arrow_size)
 			r = 1;
 	}
-	if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
-		if (y < menu->widget.rect.y + arrow_size)
+	if (menu->widget.lrect.h < (int)menu->widget.ideal_h) {
+		if (y < arrow_size)
 			t = 1;
-		else if (y > menu->widget.rect.y + menu->widget.rect.h - arrow_size)
+		else if (y > menu->widget.lrect.h - arrow_size)
 			b = 1;
 	}
 	if (t == menu->scroll_top_hover &&
@@ -610,43 +678,42 @@ set_scroll_timer(ltk_menu *menu, int x, int y) {
 	return 1;
 }
 
+/* FIXME: The mouse release handler checks if the mouse collides with the rect of the widget
+   before calling this, but that doesn't work with menuentries because part of their rect may
+   be hidden when scrolling in a menu. Maybe widgets also need a "visible rect"? */
 static int
-ltk_menuentry_mouse_release(ltk_widget *self, ltk_button_event *event) {
-	(void)event;
+ltk_menuentry_release(ltk_widget *self) {
 	ltk_menuentry *e = (ltk_menuentry *)self;
 	int in_submenu = IN_SUBMENU(e);
 	int keep_popup = self->parent && self->parent->vtable->type == LTK_WIDGET_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_specific_event(self, "menu", LTK_PWEVENTMASK_MENUENTRY_PRESS, "press");
-		return 1;
+	if (in_submenu || !keep_popup) {
+		ltk_window_unregister_all_popups(self->window);
 	}
-	return 0;
+	ltk_queue_specific_event(self, "menu", LTK_PWEVENTMASK_MENUENTRY_PRESS, "press");
+	return 1;
 }
 
 static int
 ltk_menu_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	ltk_menu *menu = (ltk_menu *)self;
 	/* FIXME: configure scroll step */
+	ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
 	switch (event->button) {
 	case LTK_BUTTON4:
 		ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
-		ltk_window_fake_motion_event(self->window, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		break;
 	case LTK_BUTTON5:
 		ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
-		ltk_window_fake_motion_event(self->window, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		break;
 	case LTK_BUTTON6:
 		ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
-		ltk_window_fake_motion_event(self->window, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		break;
 	case LTK_BUTTON7:
 		ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
-		ltk_window_fake_motion_event(self->window, event->x, event->y);
+		ltk_window_fake_motion_event(self->window, glob.x, glob.y);
 		break;
 	default:
 		return 0;
@@ -662,7 +729,7 @@ ltk_menu_hide(ltk_widget *self) {
 	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);
+	ltk_window_invalidate_widget_rect(self->window, self);
 	/* FIXME: this is really ugly/hacky */
 	if (menu->unpopup_submenus_on_hide && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY &&
 	    self->parent->parent && self->parent->parent->vtable->type == LTK_WIDGET_MENU) {
@@ -677,13 +744,17 @@ popup_active_menu(ltk_menuentry *e) {
 	if (!e->submenu)
 		return;
 	int in_submenu = 0, was_opened_left = 0;
-	ltk_rect menu_rect = e->widget.rect;
-	ltk_rect entry_rect = e->widget.rect;
+	ltk_rect menu_rect = e->widget.lrect;
+	ltk_point entry_global = ltk_widget_pos_to_global(&e->widget, 0, 0);
+	ltk_point menu_global;
 	if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_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;
+		menu_rect = menu->widget.lrect;
+		menu_global = ltk_widget_pos_to_global(e->widget.parent, 0, 0);
+	} else {
+		menu_global = ltk_widget_pos_to_global(&e->widget, 0, 0);
 	}
 	int win_w = e->widget.window->rect.w;
 	int win_h = e->widget.window->rect.h;
@@ -692,10 +763,10 @@ popup_active_menu(ltk_menuentry *e) {
 	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;
+		int space_left = menu_global.x;
+		int space_right = win_w - (menu_global.x + menu_rect.w);
+		int x_right = menu_global.x + menu_rect.w;
+		int x_left = menu_global.x - ideal_w;
 		if (submenu_theme.compress_borders) {
 			x_right -= submenu_theme.border_width;
 			x_left += submenu_theme.border_width;
@@ -730,7 +801,7 @@ popup_active_menu(ltk_menuentry *e) {
 			}
 		}
 		/* 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;
+		y_final = entry_global.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) {
@@ -738,10 +809,10 @@ popup_active_menu(ltk_menuentry *e) {
 			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;
+		int space_top = menu_global.y;
+		int space_bottom = win_h - (menu_global.y + menu_rect.h);
+		int y_top = menu_global.y - ideal_h;
+		int y_bottom = menu_global.y + menu_rect.h;
 		if (menu_theme.compress_borders) {
 			y_top += menu_theme.border_width;
 			y_bottom -= menu_theme.border_width;
@@ -752,10 +823,12 @@ popup_active_menu(ltk_menuentry *e) {
 				y_final = 0;
 				h_final = menu_rect.y;
 			}
+			submenu->was_opened_above = 1;
 		} else {
 			y_final = y_bottom;
 			if (space_bottom < ideal_h)
 				h_final = space_bottom;
+			submenu->was_opened_above = 0;
 		}
 		/* FIXME: maybe threshold so there's always at least a part of
 		   the menu contents shown (instead of maybe just a few pixels) */
@@ -764,7 +837,7 @@ popup_active_menu(ltk_menuentry *e) {
 			y_final = 0;
 			h_final = win_h;
 		}
-		x_final = entry_rect.x;
+		x_final = entry_global.x;
 		if (x_final + ideal_w > win_w)
 			x_final = win_w - ideal_w;
 		if (x_final < 0) {
@@ -776,17 +849,18 @@ popup_active_menu(ltk_menuentry *e) {
 	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.lrect.x = x_final;
+	submenu->widget.lrect.y = y_final;
+	submenu->widget.lrect.w = w_final;
+	submenu->widget.lrect.h = h_final;
+	submenu->widget.crect = submenu->widget.lrect;
 	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);
+	ltk_window_invalidate_widget_rect(submenu->widget.window, &submenu->widget);
 }
 
 static void
@@ -829,6 +903,7 @@ ltk_menu_create(ltk_window *window, const char *id, int is_submenu) {
 	menu->x_scroll_offset = menu->y_scroll_offset = 0;
 	menu->is_submenu = is_submenu;
 	menu->was_opened_left = 0;
+	menu->was_opened_above = 0;
 	menu->scroll_timer_id = -1;
 	menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
 	menu->scroll_left_hover = menu->scroll_right_hover = 0;
@@ -894,7 +969,7 @@ recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget) {
 	}
 	menu->widget.dirty = 1;
 	if (!menu->widget.hidden)
-		ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
+		ltk_window_invalidate_widget_rect(menu->widget.window, &menu->widget);
 }
 
 static void
@@ -930,20 +1005,31 @@ ltk_menuentry_create(ltk_window *window, const char *id, const char *text) {
 	return e;
 }
 
+static int
+ltk_menuentry_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
+	ltk_menuentry *e = (ltk_menuentry *)self;
+	if (widget != &e->submenu->widget) {
+		err->type = ERR_WIDGET_NOT_IN_CONTAINER;
+		return 1;
+	}
+	widget->parent = NULL;
+	e->submenu = NULL;
+	ltk_menuentry_recalc_ideal_size(e);
+	return 0;
+}
+
 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_error err;
+			ltk_widget_destroy(&e->submenu->widget, shallow, &err);
 		}
 	}
 	ltk_free(e);
@@ -989,7 +1075,7 @@ ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, ltk_error *err
 		submenu->widget.parent = &e->widget;
 	}
 	if (!e->widget.hidden)
-		ltk_window_invalidate_rect(e->widget.window, e->widget.rect);
+		ltk_window_invalidate_widget_rect(e->widget.window, &e->widget);
 	return 0;
 }
 
@@ -1066,6 +1152,162 @@ ltk_menu_remove_all_entries(ltk_menu *menu) {
 	recalc_ideal_menu_size(&menu->widget, NULL);
 }
 
+static ltk_widget *
+ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect) {
+	ltk_menu *menu = (ltk_menu *)self;
+	ltk_widget *minw = NULL;
+	int min_dist = INT_MAX;
+	int cx = rect.x + rect.w / 2;
+	int cy = rect.y + rect.h / 2;
+	ltk_rect r;
+	int dist;
+	for (size_t i = 0; i < menu->num_entries; i++) {
+		r = menu->entries[i]->widget.lrect;
+		dist = abs((r.x + r.w / 2) - cx) + abs((r.y + r.h / 2) - cy);
+		if (dist < min_dist) {
+			min_dist = dist;
+			minw = &menu->entries[i]->widget;
+		}
+	}
+	return minw;
+}
+
+/* FIXME: These need to be updated if all menus are allowed to be horizontal or vertical! */
+/* FIXME: this doesn't work properly when parent and child are both on the same side,
+   but I guess there's no good way to fix that */
+/* FIXME: behavior is a bit weird when e.g. moving down when every active menu entry in hierarchy
+   is already at bottom of respective menu - the top-level menu will give the first submenu in
+   the current active hierarchy as child widget again, and nearest_child on that submenu will
+   (probably) give the bottom widget again, so nothing changes except that all submenus except
+   for the first and second one disappeare */
+static ltk_widget *
+ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
+	ltk_menu *menu = (ltk_menu *)self;
+	ltk_widget *left = NULL;
+	if (!menu->is_submenu) {
+		left = ltk_menu_prev_child(self, widget);
+	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY &&
+	           ((ltk_menuentry *)widget)->submenu &&
+	           !((ltk_menuentry *)widget)->submenu->widget.hidden &&
+	           ((ltk_menuentry *)widget)->submenu->was_opened_left) {
+		left =  &((ltk_menuentry *)widget)->submenu->widget;
+	} else if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
+		ltk_menuentry *e = (ltk_menuentry *)self->parent;
+		if (!menu->was_opened_left && IN_SUBMENU(e)) {
+			left = self->parent;
+		} else if (!IN_SUBMENU(e) && e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
+			left = ltk_menu_prev_child(e->widget.parent, &e->widget);
+		}
+	}
+	return left;
+}
+
+static ltk_widget *
+ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
+	ltk_menu *menu = (ltk_menu *)self;
+	ltk_widget *right = NULL;
+	if (!menu->is_submenu) {
+		right = ltk_menu_next_child(self, widget);
+	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY &&
+	           ((ltk_menuentry *)widget)->submenu &&
+	           !((ltk_menuentry *)widget)->submenu->widget.hidden &&
+	           !((ltk_menuentry *)widget)->submenu->was_opened_left) {
+		right =  &((ltk_menuentry *)widget)->submenu->widget;
+	} else if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
+		ltk_menuentry *e = (ltk_menuentry *)self->parent;
+		if (menu->was_opened_left && IN_SUBMENU(e)) {
+			right = self->parent;
+		} else if (!IN_SUBMENU(e) && e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
+			right = ltk_menu_next_child(e->widget.parent, &e->widget);
+		}
+	}
+	return right;
+}
+
+static ltk_widget *
+ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
+	ltk_menu *menu = (ltk_menu *)self;
+	ltk_widget *above = NULL;
+	if (menu->is_submenu) {
+		above = ltk_menu_prev_child(self, widget);
+		if (!above && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
+			ltk_menuentry *e = (ltk_menuentry *)self->parent;
+			if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
+				ltk_menu *pmenu = (ltk_menu *)e->widget.parent;
+				if (!menu->was_opened_above && !pmenu->is_submenu) {
+					above = self->parent;
+				} else if (pmenu->is_submenu) {
+					above = ltk_menu_prev_child(e->widget.parent, &e->widget);
+				}
+			}
+		}
+	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY) {
+		ltk_menuentry *e = (ltk_menuentry *)widget;
+		if (e->submenu && !e->submenu->widget.hidden && e->submenu->was_opened_above) {
+			above =  &e->submenu->widget;
+		}
+	}
+	return above;
+}
+
+static ltk_widget *
+ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
+	ltk_menu *menu = (ltk_menu *)self;
+	ltk_widget *below = NULL;
+	if (menu->is_submenu) {
+		below = ltk_menu_next_child(self, widget);
+		if (!below && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
+			ltk_menuentry *e = (ltk_menuentry *)self->parent;
+			if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
+				ltk_menu *pmenu = (ltk_menu *)e->widget.parent;
+				if (menu->was_opened_above && !pmenu->is_submenu) {
+					below = self->parent;
+				} else if (pmenu->is_submenu) {
+					below = ltk_menu_next_child(e->widget.parent, &e->widget);
+				}
+			}
+		}
+	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY) {
+		ltk_menuentry *e = (ltk_menuentry *)widget;
+		if (e->submenu && !e->submenu->widget.hidden && !e->submenu->was_opened_above) {
+			below = &e->submenu->widget;
+		}
+	}
+	return below;
+}
+
+static ltk_widget *
+ltk_menu_prev_child(ltk_widget *self, ltk_widget *child) {
+	ltk_menu *menu = (ltk_menu *)self;
+	for (size_t i = menu->num_entries; i-- > 0;) {
+		if (&menu->entries[i]->widget == child)
+			return i > 0 ? &menu->entries[i-1]->widget : NULL;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_menu_next_child(ltk_widget *self, ltk_widget *child) {
+	ltk_menu *menu = (ltk_menu *)self;
+	for (size_t i = 0; i < menu->num_entries; i++) {
+		if (&menu->entries[i]->widget == child)
+			return i < menu->num_entries - 1 ? &menu->entries[i+1]->widget : NULL;
+	}
+	return NULL;
+}
+
+static ltk_widget *
+ltk_menu_first_child(ltk_widget *self) {
+	ltk_menu *menu = (ltk_menu *)self;
+	return menu->num_entries > 0 ? &menu->entries[0]->widget : NULL;
+}
+
+static ltk_widget *
+ltk_menu_last_child(ltk_widget *self) {
+	ltk_menu *menu = (ltk_menu *)self;
+	return menu->num_entries > 0 ? &menu->entries[menu->num_entries-1]->widget : NULL;
+}
+
 /* FIXME: unregister from window popups? */
 static void
 ltk_menuentry_detach_submenu(ltk_menuentry *e) {
@@ -1084,17 +1326,19 @@ ltk_menu_destroy(ltk_widget *self, int shallow) {
 	}
 	if (menu->scroll_timer_id >= 0)
 		ltk_unregister_timer(menu->scroll_timer_id);
+	ltk_window_unregister_popup(self->window, self);
 	if (!shallow) {
+		ltk_error err;
 		for (size_t i = 0; i < menu->num_entries; i++) {
-			ltk_menuentry_destroy(&menu->entries[i]->widget, shallow);
+			/* for efficiency - to avoid ltk_widget_destroy calling
+			   ltk_menu_remove_child for each of the entries */
+			menu->entries[i]->widget.parent = NULL;
+			ltk_widget_destroy(&menu->entries[i]->widget, shallow, &err);
 		}
+		ltk_free(menu->entries);
+	} else {
+		ltk_menu_remove_all_entries(menu);
 	}
-	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? */
-	ltk_remove_widget(self->id);
-	ltk_free(self->id);
 	ltk_free(menu);
 }
 
diff --git a/src/menu.h b/src/menu.h
@@ -33,6 +33,7 @@ typedef struct {
 	int scroll_timer_id;
 	char is_submenu;
 	char was_opened_left;
+	char was_opened_above;
 	/* FIXME: better names */
 	char popup_submenus;
 	char unpopup_submenus_on_hide;
diff --git a/src/scrollbar.c b/src/scrollbar.c
@@ -33,7 +33,7 @@
 
 #define MAX_SCROLLBAR_WIDTH 100 /* completely arbitrary */
 
-static void ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip);
+static void ltk_scrollbar_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
 static int ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event);
 static int ltk_scrollbar_motion_notify(ltk_widget *self, ltk_motion_event *event);
 static void ltk_scrollbar_destroy(ltk_widget *self, int shallow);
@@ -102,10 +102,11 @@ ltk_scrollbar_set_virtual_size(ltk_scrollbar *scrollbar, int virtual_size) {
 	scrollbar->virtual_size = virtual_size;
 }
 
+/* get rekt */
 static ltk_rect
-get_handle_rect(ltk_scrollbar *sc) {
+handle_get_rect(ltk_scrollbar *sc) {
 	ltk_rect r;
-	ltk_rect sc_rect = sc->widget.rect;
+	ltk_rect sc_rect = sc->widget.lrect;
 	if (sc->orient == LTK_HORIZONTAL) {
 		r.y = 0;
 		r.h = sc_rect.h;
@@ -126,12 +127,16 @@ get_handle_rect(ltk_scrollbar *sc) {
 	return r;
 }
 
+/* FIXME: implement clipping directly without extra surface */
 static void
-ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
+ltk_scrollbar_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
 	/* FIXME: dirty attribute */
 	ltk_scrollbar *scrollbar = (ltk_scrollbar *)self;
 	ltk_color *bg = NULL, *fg = NULL;
-	ltk_rect rect = scrollbar->widget.rect;
+	ltk_rect lrect = self->lrect;
+	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
+	if (clip_final.w <= 0 || clip_final.h <= 0)
+		return;
 	/* FIXME: proper theme for hover */
 	if (self->state & LTK_DISABLED) {
 		bg = &theme.bg_disabled;
@@ -147,14 +152,13 @@ ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip) {
 		fg = &theme.fg_normal;
 	}
 	ltk_surface *s;
-	ltk_surface_cache_request_surface_size(scrollbar->key, self->rect.w, self->rect.h);
+	ltk_surface_cache_request_surface_size(scrollbar->key, lrect.w, lrect.h);
 	ltk_surface_cache_get_surface(scrollbar->key, &s);
-	ltk_surface_fill_rect(s, bg, (ltk_rect){0, 0, rect.w, rect.h});
+	ltk_surface_fill_rect(s, bg, (ltk_rect){0, 0, lrect.w, lrect.h});
 	/* FIXME: maybe too much calculation in draw function - move to
 	   resizing function? */
-	ltk_surface_fill_rect(s, fg, get_handle_rect(scrollbar));
-	ltk_rect clip_final = ltk_rect_intersect(clip, rect);
-	ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
+	ltk_surface_fill_rect(s, fg, handle_get_rect(scrollbar));
+	ltk_surface_copy(s, draw_surf, clip_final, x + clip_final.x, y + clip_final.y);
 }
 
 static int
@@ -164,17 +168,17 @@ ltk_scrollbar_mouse_press(ltk_widget *self, ltk_button_event *event) {
 	if (event->button != LTK_BUTTONL)
 		return 0;
 	int ex = event->x, ey = event->y;
-	ltk_rect handle_rect = get_handle_rect(sc);
+	ltk_rect handle_rect = handle_get_rect(sc);
 	if (sc->orient == LTK_HORIZONTAL) {
 		if (ex < handle_rect.x || ex > handle_rect.x + handle_rect.w) {
-			sc->cur_pos = (sc->virtual_size / (double)sc->widget.rect.w) * (ex - handle_rect.w / 2 - sc->widget.rect.x);
+			sc->cur_pos = (sc->virtual_size / (double)sc->widget.lrect.w) * (ex - handle_rect.w / 2 - sc->widget.lrect.x);
 		}
-		max_pos = sc->virtual_size > sc->widget.rect.w ? sc->virtual_size - sc->widget.rect.w : 0;
+		max_pos = sc->virtual_size > sc->widget.lrect.w ? sc->virtual_size - sc->widget.lrect.w : 0;
 	} else {
 		if (ey < handle_rect.y || ey > handle_rect.y + handle_rect.h) {
-			sc->cur_pos = (sc->virtual_size / (double)sc->widget.rect.h) * (ey - handle_rect.h / 2 - sc->widget.rect.y);
+			sc->cur_pos = (sc->virtual_size / (double)sc->widget.lrect.h) * (ey - handle_rect.h / 2 - sc->widget.lrect.y);
 		}
-		max_pos = sc->virtual_size > sc->widget.rect.h ? sc->virtual_size - sc->widget.rect.h : 0;
+		max_pos = sc->virtual_size > sc->widget.lrect.h ? sc->virtual_size - sc->widget.lrect.h : 0;
 	}
 	if (sc->cur_pos < 0)
 		sc->cur_pos = 0;
@@ -194,11 +198,11 @@ ltk_scrollbar_scroll(ltk_widget *self, int delta, int scaled) {
 	int max_pos;
 	double scale;
 	if (sc->orient == LTK_HORIZONTAL) {
-		max_pos = sc->virtual_size > sc->widget.rect.w ? sc->virtual_size - sc->widget.rect.w : 0;
-		scale = sc->virtual_size / (double)sc->widget.rect.w;
+		max_pos = sc->virtual_size > sc->widget.lrect.w ? sc->virtual_size - sc->widget.lrect.w : 0;
+		scale = sc->virtual_size / (double)sc->widget.lrect.w;
 	} else {
-		max_pos = sc->virtual_size > sc->widget.rect.h ? sc->virtual_size - sc->widget.rect.h : 0;
-		scale = sc->virtual_size / (double)sc->widget.rect.h;
+		max_pos = sc->virtual_size > sc->widget.lrect.h ? sc->virtual_size - sc->widget.lrect.h : 0;
+		scale = sc->virtual_size / (double)sc->widget.lrect.h;
 	}
 	if (scaled)
 		sc->cur_pos += scale * delta;
@@ -238,9 +242,9 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
 	sc->cur_pos = 0;
 	sc->orient = orient;
 	if (orient == LTK_HORIZONTAL)
-		sc->widget.rect.h = theme.size;
+		sc->widget.ideal_h = theme.size;
 	else
-		sc->widget.rect.w = theme.size;
+		sc->widget.ideal_w = theme.size;
 	sc->callback = callback;
 	sc->callback_data = data;
 	sc->key = ltk_surface_cache_get_unnamed_key(window->surface_cache, sc->widget.ideal_w, sc->widget.ideal_h);
diff --git a/src/theme.h b/src/theme.h
@@ -44,6 +44,4 @@ int ltk_theme_handle_value(ltk_window *window, char *debug_name, const char *pro
 int ltk_theme_fill_defaults(ltk_window *window, char *debug_name, ltk_theme_parseinfo *parseinfo, size_t len);
 void ltk_theme_uninitialize(ltk_window *window, ltk_theme_parseinfo *parseinfo, size_t len);
 
-#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
-
 #endif /* _LTK_THEME_H_ */
diff --git a/src/util.c b/src/util.c
@@ -152,3 +152,13 @@ ltk_fatal_errno(const char *format, ...) {
 	va_end(args);
 	ltk_fatal("system error: %s\n", errstr);
 }
+
+int
+str_array_equal(char *terminated, char *array, size_t len) {
+	if (!strncmp(terminated, array, len)) {
+		/* this is kind of inefficient, but there's no way to know
+		   otherwise if strncmp just stopped comparing after a '\0' */
+		return strlen(terminated) == len;
+	}
+	return 0;
+}
diff --git a/src/util.h b/src/util.h
@@ -18,6 +18,7 @@
 #define _LTK_UTIL_H_
 
 #include <stdarg.h>
+#include <stddef.h>
 
 long long ltk_strtonum(
     const char *numstr, long long minval,
@@ -39,4 +40,14 @@ void ltk_warn_errno(const char *format, ...);
 void ltk_fatal(const char *format, ...);
 void ltk_warn(const char *format, ...);
 
+/*
+ * Compare the nul-terminated string 'terminated' with the char
+ * array 'array' with length 'len'.
+ * Returns non-zero if they are equal, 0 otherwise.
+ */
+/* Note: this doesn't work if array contains '\0'. */
+int str_array_equal(char *terminated, char *array, size_t len);
+
+#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
+
 #endif /* _LTK_UTIL_H_ */
diff --git a/src/widget.c b/src/widget.c
@@ -1,5 +1,3 @@
-/* FIXME: store coordinates relative to parent widget */
-/* FIXME: Destroy function for widget to destroy pixmap! */
 /*
  * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
  *
@@ -28,6 +26,13 @@
 #include "util.h"
 #include "khash.h"
 #include "surface_cache.h"
+#include "widget_config.h"
+#include "config.h"
+
+struct ltk_key_callback {
+	char *func_name;
+	int (*callback)(ltk_window *, ltk_key_event *, int handled);
+};
 
 static void ltk_destroy_widget_hash(void);
 
@@ -37,6 +42,11 @@ static khash_t(widget) *widget_hash = NULL;
 /* FIXME: any better way to do this? */
 static int hash_locked = 0;
 
+/* needed for passing keyboard events down the hierarchy */
+static ltk_widget **widget_stack = NULL;
+static size_t widget_stack_alloc = 0;
+static size_t widget_stack_len = 0;
+
 static void
 ltk_destroy_widget_hash(void) {
 	hash_locked = 1;
@@ -174,6 +184,7 @@ ltk_widgets_init() {
 
 void
 ltk_widgets_cleanup() {
+	free(widget_stack);
 	if (widget_hash)
 		ltk_destroy_widget_hash();
 }
@@ -193,10 +204,17 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
 
 	widget->state = LTK_NORMAL;
 	widget->row = 0;
-	widget->rect.x = 0;
-	widget->rect.y = 0;
-	widget->rect.w = w;
-	widget->rect.h = h;
+	widget->lrect.x = 0;
+	widget->lrect.y = 0;
+	widget->lrect.w = w;
+	widget->lrect.h = h;
+	widget->crect.x = 0;
+	widget->crect.y = 0;
+	widget->crect.w = w;
+	widget->crect.h = h;
+	widget->popup = 0;
+
+	widget->ideal_w = widget->ideal_h = 0;
 
 	widget->event_masks = NULL;
 	widget->masks_num = widget->masks_alloc = 0;
@@ -241,6 +259,7 @@ ltk_widget_hide(ltk_widget *widget) {
 	while (active) {
 		if (active == widget) {
 			set_next = 1;
+		/* FIXME: use config values for all_activatable */
 		} else if (set_next && (active->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
 			ltk_window_set_active_widget(active->window, active);
 			break;
@@ -253,6 +272,7 @@ ltk_widget_hide(ltk_widget *widget) {
 
 /* FIXME: Maybe pass the new width as arg here?
    That would make a bit more sense */
+/* FIXME: maybe give global and local position in event */
 void
 ltk_widget_resize(ltk_widget *widget) {
 	int lock_client = -1;
@@ -261,16 +281,16 @@ ltk_widget_resize(ltk_widget *widget) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "eventl %s widget configure %d %d %d %d\n",
-			    widget->id, widget->rect.x, widget->rect.y,
-			    widget->rect.w, widget->rect.h
+			    widget->id, widget->lrect.x, widget->lrect.y,
+			    widget->lrect.w, widget->lrect.h
 			);
 			lock_client = widget->event_masks[i].client;
 		} else if (widget->event_masks[i].mask & LTK_PEVENTMASK_CONFIGURE) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "event %s widget configure %d %d %d %d\n",
-			    widget->id, widget->rect.x, widget->rect.y,
-			    widget->rect.w, widget->rect.h
+			    widget->id, widget->lrect.x, widget->lrect.y,
+			    widget->lrect.w, widget->lrect.h
 			);
 		}
 	}
@@ -285,6 +305,8 @@ ltk_widget_resize(ltk_widget *widget) {
 
 void
 ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
+	if (old_state == widget->state)
+		return;
 	int lock_client = -1;
 	/* FIXME: give old and new state in event */
 	for (size_t i = 0; i < widget->masks_num; i++) {
@@ -309,19 +331,31 @@ ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
 		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);
+		ltk_window_invalidate_widget_rect(widget->window, widget);
 	}
 }
 
+/* x and y are global! */
 static ltk_widget *
-get_widget_under_pointer(ltk_widget *widget, int x, int y) {
+get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret) {
+	ltk_point glob = ltk_widget_pos_to_global(widget, 0, 0);
 	ltk_widget *next = NULL;
+	*local_x_ret = x - glob.x;
+	*local_y_ret = y - glob.y;
 	while (widget && widget->vtable->get_child_at_pos) {
-		next = widget->vtable->get_child_at_pos(widget, x, y);
-		if (!next)
+		next = widget->vtable->get_child_at_pos(widget, *local_x_ret, *local_y_ret);
+		if (!next) {
 			break;
-		else
+		} else {
 			widget = next;
+			if (next->popup) {
+				*local_x_ret = x - next->lrect.x;
+				*local_y_ret = y - next->lrect.y;
+			} else {
+				*local_x_ret -= next->lrect.x;
+				*local_y_ret -= next->lrect.y;
+			}
+		}
 	}
 	return widget;
 }
@@ -329,7 +363,7 @@ get_widget_under_pointer(ltk_widget *widget, int x, int y) {
 static ltk_widget *
 get_hover_popup(ltk_window *window, int x, int y) {
 	for (size_t i = window->popups_num; i-- > 0;) {
-		if (ltk_collide_rect(window->popups[i]->rect, x, y))
+		if (ltk_collide_rect(window->popups[i]->crect, x, y))
 			return window->popups[i];
 	}
 	return NULL;
@@ -343,6 +377,7 @@ is_parent(ltk_widget *parent, ltk_widget *child) {
 	return child != NULL;
 }
 
+/* FIXME: fix global and local coordinates! */
 static int
 queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
 	int lock_client = -1;
@@ -351,16 +386,16 @@ queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "eventl %s widget %s %d %d %d %d\n",
-			    widget->id, type, x, y,
-			    x - widget->rect.x, y - widget->rect.y
+			    widget->id, type, x, y, x, y
+			    /* x - widget->rect.x, y - widget->rect.y */
 			);
 			lock_client = widget->event_masks[i].client;
 		} else if (widget->event_masks[i].mask & mask) {
 			ltk_queue_sock_write_fmt(
 			    widget->event_masks[i].client,
 			    "event %s widget %s %d %d %d %d\n",
-			    widget->id, type, x, y,
-			    x - widget->rect.x, y - widget->rect.y
+			    widget->id, type, x, y, x, y
+			    /* x - widget->rect.x, y - widget->rect.y */
 			);
 		}
 	}
@@ -371,6 +406,515 @@ queue_mouse_event(ltk_widget *widget, char *type, uint32_t mask, int x, int y) {
 	return 0;
 }
 
+static void
+ensure_active_widget_shown(ltk_window *window) {
+	ltk_widget *widget = window->active_widget;
+	if (!widget)
+		return;
+	ltk_rect r = widget->lrect;
+	while (widget->parent) {
+		if (widget->parent->vtable->ensure_rect_shown)
+			widget->parent->vtable->ensure_rect_shown(widget->parent, r);
+		widget = widget->parent;
+		r.x += widget->lrect.x;
+		r.y += widget->lrect.y;
+		/* FIXME: this currently just aborts if a widget is positioned
+		   absolutely because I'm not sure what the best action would
+		   be in that case */
+		if (widget->popup)
+			break;
+	}
+	ltk_window_invalidate_widget_rect(window, widget);
+}
+
+/* FIXME: come up with a more elegant way to handle this? */
+/* FIXME: Handle hidden state here instead of in widgets */
+/* FIXME: handle disabled state */
+static int
+prev_child(ltk_window *window) {
+	if (!window->root_widget)
+		return 0;
+	ltk_config *config = ltk_config_get();
+	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_widget *new, *cur = window->active_widget;
+	int changed = 0;
+	ltk_widget *prevcur = cur;
+	while (1) {
+		if (cur) {
+			while (cur->parent) {
+				new = NULL;
+				if (cur->parent->vtable->prev_child)
+					new = cur->parent->vtable->prev_child(cur->parent, cur);
+				if (new) {
+					cur = new;
+					ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+					while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
+						cur = new;
+						if (cur->vtable->flags & act_flags)
+							last_activatable = cur;
+					}
+					if (last_activatable) {
+						cur = last_activatable;
+						changed = 1;
+						break;
+					}
+				} else {
+					cur = cur->parent;
+					if (cur->vtable->flags & act_flags) {
+						changed = 1;
+						break;
+					}
+				}
+			}
+		}
+		if (!changed) {
+			cur = window->root_widget;
+			ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+			while (cur->vtable->last_child && (new = cur->vtable->last_child(cur))) {
+				cur = new;
+				if (cur->vtable->flags & act_flags)
+					last_activatable = cur;
+			}
+			if (last_activatable)
+				cur = last_activatable;
+		}
+		if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
+			break;
+		prevcur = cur;
+	}
+	/* FIXME: What exactly should be done if no activatable widget exists? */
+	if (cur != window->active_widget) {
+		ltk_window_set_active_widget(window, cur);
+		ensure_active_widget_shown(window);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+next_child(ltk_window *window) {
+	if (!window->root_widget)
+		return 0;
+	ltk_config *config = ltk_config_get();
+	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_widget *new, *cur = window->active_widget;
+	int changed = 0;
+	ltk_widget *prevcur = cur;
+	while (1) {
+		if (cur) {
+			while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+				cur = new;
+				if (cur->vtable->flags & act_flags) {
+					changed = 1;
+					break;
+				}
+			}
+			if (!changed) {
+				while (cur->parent) {
+					new = NULL;
+					if (cur->parent->vtable->next_child)
+						new = cur->parent->vtable->next_child(cur->parent, cur);
+					if (new) {
+						cur = new;
+						if (cur->vtable->flags & act_flags) {
+							changed = 1;
+							break;
+						}
+						while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+							cur = new;
+							if (cur->vtable->flags & act_flags) {
+								changed = 1;
+								break;
+							}
+						}
+						if (changed)
+							break;
+					} else {
+						cur = cur->parent;
+					}
+				}
+			}
+		}
+		if (!changed) {
+			cur = window->root_widget;
+			if (!(cur->vtable->flags & act_flags)) {
+				while (cur->vtable->first_child && (new = cur->vtable->first_child(cur))) {
+					cur = new;
+					if (cur->vtable->flags & act_flags)
+						break;
+				}
+			}
+			if (!(cur->vtable->flags & act_flags))
+				cur = window->root_widget;
+		}
+		if (prevcur == cur || (cur && (cur->vtable->flags & act_flags)))
+			break;
+		prevcur = cur;
+	}
+	if (cur != window->active_widget) {
+		ltk_window_set_active_widget(window, cur);
+		ensure_active_widget_shown(window);
+		return 1;
+	}
+	return 0;
+}
+
+/* FIXME: moving up/down/left/right needs to be rethought
+   it generally is a bit weird, and in particular, nearest_child always searches for the child
+   that has the smallest distance to the given rect, so it may not be the child that the user
+   expects when going down (e.g. a vertical box with one widget closer vertically but on the
+   other side horizontally, thus possibly leading to a different widget that is farther away
+   vertically to be chosen instead) - what would be logical here? */
+static ltk_widget *
+nearest_child(ltk_widget *widget, ltk_rect r) {
+	ltk_point local = ltk_global_to_widget_pos(widget, r.x, r.y);
+	return widget->vtable->nearest_child(widget, (ltk_rect){local.x, local.y, r.w, r.h});
+}
+
+/* FIXME: maybe wrap around in these two functions? */
+static int
+left_top_child(ltk_window *window, int left) {
+	if (!window->root_widget)
+		return 0;
+	ltk_config *config = ltk_config_get();
+	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_widget *new, *cur = window->active_widget;
+	ltk_rect old_rect = {0, 0, 0, 0};
+	if (cur) {
+		ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
+		old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
+	}
+	if (cur) {
+		while (cur->parent) {
+			new = NULL;
+			if (left) {
+				if (cur->parent->vtable->nearest_child_left)
+					new = cur->parent->vtable->nearest_child_left(cur->parent, cur);
+			} else {
+				if (cur->parent->vtable->nearest_child_above)
+					new = cur->parent->vtable->nearest_child_above(cur->parent, cur);
+			}
+			if (new) {
+				cur = new;
+				ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+				while (cur->vtable->nearest_child && (new = nearest_child(cur, old_rect))) {
+					cur = new;
+					if (cur->vtable->flags & act_flags)
+						last_activatable = cur;
+				}
+				if (last_activatable) {
+					cur = last_activatable;
+					break;
+				}
+			} else {
+				cur = cur->parent;
+				if (cur->vtable->flags & act_flags) {
+					break;
+				}
+			}
+		}
+	} else {
+		cur = window->root_widget;
+		ltk_widget *last_activatable = (cur->vtable->flags & act_flags) ? cur : NULL;
+		ltk_rect r = {cur->lrect.w, cur->lrect.h, 0, 0};
+		while (cur->vtable->nearest_child && (new = nearest_child(cur, r))) {
+			cur = new;
+			if (cur->vtable->flags & act_flags)
+				last_activatable = cur;
+		}
+		if (last_activatable)
+			cur = last_activatable;
+	}
+	/* FIXME: What exactly should be done if no activatable widget exists? */
+	if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
+		ltk_window_set_active_widget(window, cur);
+		ensure_active_widget_shown(window);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+right_bottom_child(ltk_window *window, int right) {
+	if (!window->root_widget)
+		return 0;
+	ltk_config *config = ltk_config_get();
+	ltk_widget_flags act_flags = config->general.all_activatable ? LTK_ACTIVATABLE_ALWAYS : LTK_ACTIVATABLE_NORMAL;
+	ltk_widget *new, *cur = window->active_widget;
+	int changed = 0;
+	ltk_rect old_rect = {0, 0, 0, 0};
+	ltk_rect corner = {0, 0, 0, 0};
+	if (cur) {
+		ltk_point glob = cur->parent ? ltk_widget_pos_to_global(cur->parent, cur->lrect.x, cur->lrect.y) : (ltk_point){cur->lrect.x, cur->lrect.y};
+		corner = (ltk_rect){glob.x, glob.y, 0, 0};
+		old_rect = (ltk_rect){glob.x, glob.y, cur->lrect.w, cur->lrect.h};
+		while (cur->vtable->nearest_child && (new = nearest_child(cur, corner))) {
+			cur = new;
+			if (cur->vtable->flags & act_flags) {
+				changed = 1;
+				break;
+			}
+		}
+		if (!changed) {
+			while (cur->parent) {
+				new = NULL;
+				if (right) {
+					if (cur->parent->vtable->nearest_child_right)
+						new = cur->parent->vtable->nearest_child_right(cur->parent, cur);
+				} else {
+					if (cur->parent->vtable->nearest_child_below)
+						new = cur->parent->vtable->nearest_child_below(cur->parent, cur);
+				}
+				if (new) {
+					cur = new;
+					if (cur->vtable->flags & act_flags) {
+						changed = 1;
+						break;
+					}
+					while (cur->vtable->nearest_child && (new = nearest_child(cur, old_rect))) {
+						cur = new;
+						if (cur->vtable->flags & act_flags) {
+							changed = 1;
+							break;
+						}
+					}
+					if (changed)
+						break;
+				} else {
+					cur = cur->parent;
+				}
+			}
+		}
+	} else {
+		cur = window->root_widget;
+		if (!(cur->vtable->flags & act_flags)) {
+			while (cur->vtable->nearest_child && (new = nearest_child(cur, (ltk_rect){0, 0, 0, 0}))) {
+				cur = new;
+				if (cur->vtable->flags & act_flags)
+					break;
+			}
+		}
+		if (!(cur->vtable->flags & act_flags))
+			cur = window->root_widget;
+	}
+	if (cur && cur != window->active_widget && (cur->vtable->flags & act_flags)) {
+		ltk_window_set_active_widget(window, cur);
+		ensure_active_widget_shown(window);
+		return 1;
+	}
+	return 0;
+}
+
+/* FIXME: maybe just set this when active widget changes */
+/* -> but would also need to change it when widgets are created/destroyed or parents change */
+static void
+gen_widget_stack(ltk_widget *bottom) {
+	widget_stack_len = 0;
+	while (bottom) {
+		if (widget_stack_len + 1 > widget_stack_alloc) {
+			widget_stack_alloc = ideal_array_size(widget_stack_alloc, widget_stack_len + 1);
+			widget_stack = ltk_reallocarray(widget_stack, widget_stack_alloc, sizeof(ltk_widget *));
+		}
+		widget_stack[widget_stack_len++] = bottom;
+		bottom = bottom->parent;
+	}
+}
+
+/* FIXME: The focus behavior needs to be rethought. It's currently hard-coded in the vtable for each
+   widget type, but what if the program using ltk wants to catch keyboard events even if the widget
+   doesn't do that by default? */
+static int
+cb_focus_active(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	if (window->active_widget && !(window->active_widget->state & LTK_FOCUSED)) {
+		/* FIXME: maybe also set widgets above in hierarchy? */
+		ltk_widget_state old_state = window->active_widget->state;
+		window->active_widget->state |= LTK_FOCUSED;
+		ltk_widget_change_state(window->active_widget, old_state);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED) && (window->active_widget->vtable->flags & LTK_NEEDS_KEYBOARD)) {
+		ltk_widget_state old_state = window->active_widget->state;
+		window->active_widget->state &= ~LTK_FOCUSED;
+		ltk_widget_change_state(window->active_widget, old_state);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+cb_move_prev(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return prev_child(window);
+}
+
+static int
+cb_move_next(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return next_child(window);
+}
+
+static int
+cb_move_left(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return left_top_child(window, 1);
+}
+
+static int
+cb_move_right(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return right_bottom_child(window, 1);
+}
+
+static int
+cb_move_up(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return left_top_child(window, 0);
+}
+
+static int
+cb_move_down(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	return right_bottom_child(window, 0);
+}
+
+static int
+cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+		/* FIXME: only set pressed if needs keyboard? */
+		ltk_window_set_pressed_widget(window, window->active_widget, 0);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	if (window->pressed_widget) {
+		ltk_window_set_pressed_widget(window, NULL, 1);
+		return 1;
+	}
+	return 0;
+}
+
+static int
+cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled) {
+	(void)event;
+	(void)handled;
+	if (window->popups_num > 0) {
+		ltk_window_unregister_all_popups(window);
+		return 1;
+	}
+	return 0;
+}
+
+static ltk_key_callback key_callbacks[] = {
+	{"move-next", &cb_move_next},
+	{"move-prev", &cb_move_prev},
+	{"move-up", &cb_move_up},
+	{"move-down", &cb_move_down},
+	{"move-left", &cb_move_left},
+	{"move-right", &cb_move_right},
+	{"focus-active", &cb_focus_active},
+	{"unfocus-active", &cb_unfocus_active},
+	{"set-pressed", &cb_set_pressed},
+	{"unset-pressed", &cb_unset_pressed},
+	{"remove-popups", &cb_remove_popups},
+};
+
+/* FIXME: binary search (copy from ledit) */
+ltk_key_callback *
+ltk_get_key_func(char *name, size_t len) {
+	for (size_t i = 0; i < LENGTH(key_callbacks); i++) {
+		if (str_array_equal(key_callbacks[i].func_name, name, len))
+			return &key_callbacks[i];
+	}
+	return NULL;
+}
+
+/* FIXME: should keyrelease events be ignored if the corresponding keypress event
+   was consumed for movement? */
+/* FIXME: check if there's any weirdness when combining return and mouse press */
+/* FIXME: maybe it doesn't really make sense to make e.g. text entry pressed when enter is pressed? */
+/* FIXME: implement key binding flag to run before widget handler is called */
+void
+ltk_window_key_press_event(ltk_window *window, ltk_key_event *event) {
+	/* FIXME: how to handle config being NULL? */
+	ltk_config *config = ltk_config_get();
+	int handled = 0;
+	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+		gen_widget_stack(window->active_widget);
+		for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
+			/* FIXME: send event to socket! */
+			if (widget_stack[i]->vtable->key_press && widget_stack[i]->vtable->key_press(widget_stack[i], event)) {
+				handled = 1;
+				break;
+			}
+		}
+	}
+	ltk_keypress_binding *b = NULL;
+	for (size_t i = 0; i < config->keys.press_len; i++) {
+		b = &config->keys.press_bindings[i];
+		if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
+			continue;
+		} else if (b->text) {
+			if (event->mapped && !strcmp(b->text, event->mapped))
+				handled |= b->callback->callback(window, event, handled);
+		} else if (b->rawtext) {
+			if (event->text && !strcmp(b->text, event->text))
+				handled |= b->callback->callback(window, event, handled);
+		} else if (b->sym != LTK_KEY_NONE) {
+			if (event->sym == b->sym)
+				handled |= b->callback->callback(window, event, handled);
+		}
+	}
+
+}
+
+/* FIXME: need to actually check if any of parent widgets are focused and still pass to them even if bottom widget not focused? */
+void
+ltk_window_key_release_event(ltk_window *window, ltk_key_event *event) {
+	/* FIXME: emit event */
+	ltk_config *config = ltk_config_get();
+	int handled = 0;
+	if (window->active_widget && (window->active_widget->state & LTK_FOCUSED)) {
+		gen_widget_stack(window->active_widget);
+		for (size_t i = widget_stack_len; i-- > 0 && !handled;) {
+			if (widget_stack[i]->vtable->key_release && widget_stack[i]->vtable->key_release(widget_stack[i], event)) {
+				handled = 1;
+				break;
+			}
+		}
+	}
+	ltk_keyrelease_binding *b = NULL;
+	for (size_t i = 0; i < config->keys.release_len; i++) {
+		b = &config->keys.release_bindings[i];
+		if (b->mods != event->modmask || (!(b->flags & LTK_KEY_BINDING_RUN_ALWAYS) && handled)) {
+			continue;
+		} else if (b->sym != LTK_KEY_NONE && event->sym == b->sym) {
+			handled |= b->callback->callback(window, event, handled);
+		}
+	}
+}
+
 /* FIXME: This is still weird. */
 void
 ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
@@ -384,9 +928,11 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 		ltk_window_unregister_all_popups(window);
 		return;
 	}
-	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y);
+	int orig_x = event->x, orig_y = event->y;
+	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
 	/* FIXME: need to add more flags for more fine-grained control
 	   -> also, should the widget still get mouse_press even if state doesn't change? */
+	/* FIXME: doesn't work with e.g. disabled menu entries */
 	if (!(cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
 		ltk_window_unregister_all_popups(window);
 	}
@@ -401,6 +947,9 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 	int first = 1;
 	while (cur_widget) {
 		int handled = 0;
+		ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+		event->x = local.x;
+		event->y = local.y;
 		if (cur_widget->state != LTK_DISABLED) {
 			/* 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 */
@@ -409,8 +958,9 @@ ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event) {
 			else if (cur_widget->vtable->mouse_press)
 				handled = cur_widget->vtable->mouse_press(cur_widget, event);
 			/* set first non-disabled widget to pressed widget */
+			/* FIXME: use config values for all_activatable */
 			if (first && event->button == LTK_BUTTONL && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
-				ltk_window_set_pressed_widget(window, cur_widget);
+				ltk_window_set_pressed_widget(window, cur_widget, 0);
 				first = 0;
 			}
 		}
@@ -430,9 +980,10 @@ ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
 void
 ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event) {
 	ltk_widget *widget = window->pressed_widget;
+	int orig_x = event->x, orig_y = event->y;
 	if (!widget) {
 		widget = get_hover_popup(window, event->x, event->y);
-		widget = get_widget_under_pointer(widget, event->x, event->y);
+		widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
 	}
 	/* FIXME: loop up to top of hierarchy if not handled */
 	if (widget && queue_mouse_event(widget, "mouserelease", LTK_PEVENTMASK_MOUSERELEASE, event->x, event->y)) {
@@ -441,19 +992,30 @@ ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event) {
 		widget->vtable->mouse_release(widget, event);
 	}
 	if (event->button == LTK_BUTTONL) {
-		ltk_window_set_pressed_widget(window, NULL);
+		int release = 0;
+		if (window->pressed_widget) {
+			ltk_rect prect = window->pressed_widget->lrect;
+			ltk_point pglob = ltk_widget_pos_to_global(window->pressed_widget, 0, 0);
+			if (ltk_collide_rect((ltk_rect){pglob.x, pglob.y, prect.w, prect.h}, orig_x, orig_y))
+				release = 1;
+		}
+		ltk_window_set_pressed_widget(window, NULL, release);
 		/* send motion notify to widget under pointer */
-		/* FIXME: only when not collide with rect */
-		ltk_window_fake_motion_event(window, event->x, event->y);
+		/* FIXME: only when not collide with rect? */
+		ltk_window_fake_motion_event(window, orig_x, orig_y);
 	}
 }
 
 void
 ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 	ltk_widget *widget = get_hover_popup(window, event->x, event->y);
+	int orig_x = event->x, orig_y = event->y;
 	if (!widget) {
 		widget = window->pressed_widget;
 		if (widget) {
+			ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+			event->x = local.x;
+			event->y = local.y;
 			if (widget->vtable->motion_notify)
 				widget->vtable->motion_notify(widget, event);
 			return;
@@ -462,14 +1024,18 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 	}
 	if (!widget)
 		return;
-	if (!ltk_collide_rect(widget->rect, event->x, event->y)) {
+	ltk_point local = ltk_global_to_widget_pos(widget, event->x, event->y);
+	if (!ltk_collide_rect((ltk_rect){0, 0, widget->lrect.w, widget->lrect.h}, local.x, local.y)) {
 		ltk_window_set_hover_widget(widget->window, NULL, event);
 		return;
 	}
-	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y);
+	ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
 	int first = 1;
 	while (cur_widget) {
 		int handled = 0;
+		ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
+		event->x = local.x;
+		event->y = local.y;
 		if (cur_widget->state != LTK_DISABLED) {
 			if (queue_mouse_event(cur_widget, "mousemotion", LTK_PEVENTMASK_MOUSEMOTION, event->x, event->y))
 				handled = 1;
@@ -478,7 +1044,10 @@ 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? */
+			/* FIXME: use config values for all_activatable */
 			if (first && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
+				event->x = orig_x;
+				event->y = orig_y;
 				ltk_window_set_hover_widget(window, cur_widget, event);
 				first = 0;
 			}
@@ -488,8 +1057,11 @@ ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event) {
 		else
 			break;
 	}
-	if (first)
+	if (first) {
+		event->x = orig_x;
+		event->y = orig_y;
 		ltk_window_set_hover_widget(window, NULL, event);
+	}
 }
 
 int
@@ -551,6 +1123,11 @@ ltk_widget_destroy(ltk_widget *widget, int shallow, ltk_error *err) {
 		    widget, widget->parent, err
 		);
 	}
+	ltk_remove_widget(widget->id);
+	ltk_free(widget->id);
+	widget->id = NULL;
+	ltk_free(widget->event_masks);
+	widget->event_masks = NULL;
 	widget->vtable->destroy(widget, shallow);
 
 	return invalid;
diff --git a/src/widget.h b/src/widget.h
@@ -31,7 +31,7 @@ typedef enum {
 	LTK_ACTIVATABLE_NORMAL = 1,
 	LTK_ACTIVATABLE_SPECIAL = 2,
 	LTK_ACTIVATABLE_ALWAYS = 1|2,
-	LTK_GRABS_INPUT = 4,
+	LTK_NEEDS_KEYBOARD = 4,
 	LTK_NEEDS_REDRAW = 8,
 	LTK_HOVER_IS_ACTIVE = 16,
 } ltk_widget_flags;
@@ -54,7 +54,8 @@ typedef enum {
 	LTK_PRESSED = 2,
 	LTK_ACTIVE = 4,
 	LTK_HOVERACTIVE = 1 | 4,
-	LTK_DISABLED = 8,
+	LTK_FOCUSED = 8,
+	LTK_DISABLED = 16,
 } ltk_widget_state;
 
 #include "surface_cache.h"
@@ -78,7 +79,19 @@ struct ltk_widget {
 
 	struct ltk_widget_vtable *vtable;
 
-	ltk_rect rect;
+	/* FIXME: crect and lrect are a bit weird still */
+	/* FIXME: especially the relative positioning is really weird for
+	   popups because they're positioned globally but still have a
+	   parent-child relationship - weird things can probably happen */
+	/* both rects relative to parent (except for popups) */
+	/* collision rect is only part that is actually shown and used for
+	   collision with mouse (but may still not be drawn if hidden by
+	   something else) - e.g. in a box with scrolling, a widget that
+	   is half cut off by a side of the box will have the logical rect
+	   going past the side of the box, but the collision rect will only
+	   be the part inside the box */
+	ltk_rect crect; /* collision rect */
+	ltk_rect lrect; /* logical rect */
 	unsigned int ideal_w;
 	unsigned int ideal_h;
 
@@ -93,6 +106,9 @@ struct ltk_widget {
 	unsigned short column;
 	unsigned short row_span;
 	unsigned short column_span;
+	/* needed to properly handle handle local coordinates since
+	   popups are positioned globally instead of locally */
+	char popup;
 	char dirty;
 	char hidden;
 };
@@ -100,32 +116,42 @@ 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 (*key_press)(struct ltk_widget *, ltk_key_event *);
+	int (*key_release)(struct ltk_widget *, ltk_key_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 *);
+	int (*press)(struct ltk_widget *);
+	int (*release)(struct ltk_widget *);
 
 	void (*resize)(struct ltk_widget *);
 	void (*hide)(struct ltk_widget *);
-	void (*draw)(struct ltk_widget *, ltk_rect);
+	/* draw_surface: surface to draw it on
+	   x, y: position of logical rectangle on surface
+	   clip: clipping rectangle, relative to logical rectangle */
+	void (*draw)(struct ltk_widget *self, ltk_surface *draw_surface, int x, int y, ltk_rect clip);
 	void (*change_state)(struct ltk_widget *, ltk_widget_state);
 	void (*destroy)(struct ltk_widget *, int);
 
+	/* rect is in self's coordinate system */
 	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);
-	struct ltk_widget *(*nearest_child_right)(struct ltk_widget *self, ltk_rect rect);
-	struct ltk_widget *(*nearest_child_above)(struct ltk_widget *self, ltk_rect rect);
-	struct ltk_widget *(*nearest_child_below)(struct ltk_widget *self, ltk_rect rect);
+	struct ltk_widget *(*nearest_child_left)(struct ltk_widget *self, ltk_widget *widget);
+	struct ltk_widget *(*nearest_child_right)(struct ltk_widget *self, ltk_widget *widget);
+	struct ltk_widget *(*nearest_child_above)(struct ltk_widget *self, ltk_widget *widget);
+	struct ltk_widget *(*nearest_child_below)(struct ltk_widget *self, ltk_widget *widget);
 	struct ltk_widget *(*next_child)(struct ltk_widget *self, ltk_widget *child);
 	struct ltk_widget *(*prev_child)(struct ltk_widget *self, ltk_widget *child);
 	struct ltk_widget *(*first_child)(struct ltk_widget *self);
+	struct ltk_widget *(*last_child)(struct ltk_widget *self);
 
-	void (*child_size_change) (struct ltk_widget *, struct ltk_widget *);
+	void (*child_size_change)(struct ltk_widget *, struct ltk_widget *);
 	int (*remove_child)(struct ltk_widget *, struct ltk_widget *, ltk_error *);
+	/* x and y relative to widget's lrect! */
 	struct ltk_widget *(*get_child_at_pos)(struct ltk_widget *, int x, int y);
+	/* r is in self's coordinate system */
+	void (*ensure_rect_shown)(struct ltk_widget *self, ltk_rect r);
 
 	ltk_widget_type type;
 	ltk_widget_flags flags;
@@ -138,6 +164,8 @@ void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, struct ltk_win
     struct ltk_widget_vtable *vtable, int w, int h);
 void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
 /* FIXME: move to separate window.h */
+void ltk_window_key_press_event(ltk_window *window, ltk_key_event *event);
+void ltk_window_key_release_event(ltk_window *window, ltk_key_event *event);
 void ltk_window_mouse_press_event(ltk_window *window, ltk_button_event *event);
 void ltk_window_mouse_release_event(ltk_window *window, ltk_button_event *event);
 void ltk_window_motion_notify_event(ltk_window *window, ltk_motion_event *event);
diff --git a/src/widget_config.h b/src/widget_config.h
@@ -0,0 +1,7 @@
+#ifndef LTK_WIDGET_CONFIG_H
+#define LTK_WIDGET_CONFIG_H
+
+typedef struct ltk_key_callback ltk_key_callback;
+ltk_key_callback *ltk_get_key_func(char *name, size_t len);
+
+#endif /* LTK_WIDGET_CONFIG_H */
diff --git a/src/xlib_shared.h b/src/xlib_shared.h
@@ -15,6 +15,12 @@ struct ltk_renderdata {
         XdbeBackBuffer back_buf;
         Drawable drawable;
         int depth;
+	XIM xim;
+	XIC xic;
+	XPoint spot;
+	XVaNestedList spotlist;
+	int xkb_event_type;
+	int xkb_supported;
 };
 
 #endif /* XLIB_SHARED_H */
diff --git a/test.gui b/test.gui
@@ -1,7 +1,8 @@
-grid grd1 create 2 1
+grid grd1 create 2 2
 grid grd1 set-row-weight 0 1
 grid grd1 set-row-weight 1 1
 grid grd1 set-column-weight 0 1
+grid grd1 set-column-weight 1 1
 set-root-widget grd1
 box box1 create vertical
 grid grd1 add box1 0 0 1 1 nsew
@@ -19,4 +20,8 @@ button btn6 create "2 I'm another boring button."
 box box2 add btn4 ew
 box box2 add btn5 e
 box box2 add btn6
+button btn7 create "Button 7"
+button btn8 create "Button 8"
+grid grd1 add btn7 0 1 1 1 nsew
+grid grd1 add btn8 1 1 1 1 ew
 mask-add btn1 button press
diff --git a/test.sh b/test.sh
@@ -1,10 +1,6 @@
 #!/bin/sh
 
 # This is very hacky.
-#
-# All events are still printed to the terminal currently because
-# the second './ltkc' still prints everything - event masks aren't
-# supported yet.
 
 export LTKDIR="`pwd`/.ltk"
 ltk_id=`./src/ltkd -t "Cool Window"`
diff --git a/test2.gui b/test2.gui
@@ -42,3 +42,4 @@ submenu submenu3 create
 menu submenu3 add-entry entry16
 menuentry entry15 attach-submenu submenu3
 grid grd1 add menu1 0 0 1 1 ew
+mask-add entry10 menuentry press
diff --git a/test3.gui b/test3.gui
@@ -0,0 +1,13 @@
+grid grd1 create 3 1
+grid grd1 set-row-weight 0 1
+grid grd1 set-row-weight 1 1
+grid grd1 set-row-weight 2 1
+grid grd1 set-column-weight 0 1
+set-root-widget grd1
+button btn1 create "I'm a button!"
+button btn2 create "I'm also a button!"
+button btn3 create "I'm another boring button."
+grid grd1 add btn1 0 0 1 1
+grid grd1 add btn2 1 0 1 1
+grid grd1 add btn3 2 0 1 1
+mask-add btn1 button press
diff --git a/test3.sh b/test3.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+
+export LTKDIR="`pwd`/.ltk"
+ltk_id=`./src/ltkd -t "Cool Window"`
+if [ $? -ne 0 ]; then
+	echo "Unable to start ltkd." >&2
+	exit 1
+fi
+
+cat test3.gui | ./src/ltkc $ltk_id | while read cmd
+do
+	case "$cmd" in
+	*"event btn1 button press")
+		echo "quit"
+		;;
+	*)
+		printf "%s\n" "$cmd" >&2
+		;;
+	esac
+done | ./src/ltkc $ltk_id
diff --git a/testbox.sh b/testbox.sh
@@ -7,7 +7,7 @@ if [ $? -ne 0 ]; then
 	exit 1
 fi
 
-cmds="box box1 create vertical\nset-root-widget box1\nbutton exit_btn create \"Exit\"\nmask-add exit_btn button press\nbox box1 add exit_btn
+cmds="box box1 create vertical\nset-root-widget box1\nlabel lblbla create \"Hi\"\nbox box1 add lblbla w\nbutton exit_btn create \"Exit\"\nmask-add exit_btn button press\nbox box1 add exit_btn
 $(curl -s gopher://lumidify.org | awk -F'\t' '
 BEGIN {btn = 0; lbl = 0;}
 /^i/ { printf "label lbl%s create \"%s\"\nbox box1 add lbl%s w\n", lbl, substr($1, 2), lbl; lbl++ }