URI: 
       tImplement key events and mappings - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit 6b058581e1cb02dd6d4775d8ab3f008bcc87829c
   DIR parent 610c7a836cb111096f1ae31d5190a2964ba25310
  HTML 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:
         A .ltk/ltk.cfg                        |      65 +++++++++++++++++++++++++++++++
         M .ltk/theme.ini                      |       1 -
         M Makefile                            |      13 +++++++++----
         M src/box.c                           |     245 ++++++++++++++++++++++++-------
         M src/button.c                        |      35 ++++++++++++++-----------------
         A src/config.c                        |     710 +++++++++++++++++++++++++++++++
         A src/config.h                        |      69 ++++++++++++++++++++++++++++++
         M src/event.h                         |      57 ++++++-------------------------
         M src/event_xlib.c                    |     148 ++++++++++++++++++++++++++++++-
         A src/eventdefs.h                     |      53 ++++++++++++++++++++++++++++++
         M src/graphics.h                      |       3 ++-
         M src/graphics_xlib.c                 |     120 ++++++++++++++++++++++++++++++--
         M src/grid.c                          |     211 +++++++++++++++++++++++++------
         M src/label.c                         |      26 ++++++++++++++------------
         M src/ltk.h                           |       7 ++++++-
         M src/ltkd.c                          |     132 ++++++++++++++++++++++++++-----
         M src/memory.c                        |      35 +++++++++++++++++++++++++++++++
         M src/memory.h                        |      19 ++++++++++++++-----
         M src/menu.c                          |     470 +++++++++++++++++++++++--------
         M src/menu.h                          |       1 +
         M src/scrollbar.c                     |      46 +++++++++++++++++--------------
         M src/theme.h                         |       2 --
         M src/util.c                          |      10 ++++++++++
         M src/util.h                          |      11 +++++++++++
         M src/widget.c                        |     635 +++++++++++++++++++++++++++++--
         M src/widget.h                        |      50 ++++++++++++++++++++++++-------
         A src/widget_config.h                 |       7 +++++++
         M src/xlib_shared.h                   |       6 ++++++
         M test.gui                            |       7 ++++++-
         M test.sh                             |       4 ----
         M test2.gui                           |       1 +
         A test3.gui                           |      13 +++++++++++++
         A test3.sh                            |      20 ++++++++++++++++++++
         M testbox.sh                          |       2 +-
       
       34 files changed, 2850 insertions(+), 384 deletions(-)
       ---
   DIR diff --git a/.ltk/ltk.cfg b/.ltk/ltk.cfg
       t@@ -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"
   DIR diff --git a/.ltk/theme.ini b/.ltk/theme.ini
       t@@ -1,6 +1,5 @@
        [window]
        font-size = 15
       -border-width = 0
        bg = #000000
        fg = #FFFFFF
        font = Liberation Mono
   DIR diff --git a/Makefile b/Makefile
       t@@ -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
        
       t@@ -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)
       t@@ -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 = \
       t@@ -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
       t@@ -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
        
   DIR diff --git a/src/box.c b/src/box.c
       t@@ -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);
       t@@ -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,
       t@@ -59,6 +71,16 @@ static struct ltk_widget_vtable vtable = {
                .get_child_at_pos = &ltk_box_get_child_at_pos,
                .mouse_leave = NULL,
                .mouse_enter = NULL,
       +        .prev_child = &ltk_box_prev_child,
       +        .next_child = &ltk_box_next_child,
       +        .first_child = &ltk_box_first_child,
       +        .last_child = &ltk_box_last_child,
       +        .nearest_child = &ltk_box_nearest_child,
       +        .nearest_child_left = &ltk_box_nearest_child_left,
       +        .nearest_child_right = &ltk_box_nearest_child_right,
       +        .nearest_child_above = &ltk_box_nearest_child_above,
       +        .nearest_child_below = &ltk_box_nearest_child_below,
       +        .ensure_rect_shown = &ltk_box_ensure_rect_shown,
                .type = LTK_WIDGET_BOX,
                .flags = 0,
        };
       t@@ -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 *
       t@@ -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, &ltk_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);
        }
        
       t@@ -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);
        }
        
       t@@ -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;
       t@@ -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);
        }
        
       t@@ -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) {
       t@@ -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;
        }
       t@@ -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;
       t@@ -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 */
       t@@ -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;
       t@@ -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;
   DIR diff --git a/src/button.c b/src/button.c
       t@@ -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);
       t@@ -48,7 +48,8 @@ static struct ltk_widget_vtable vtable = {
                .key_press = NULL,
                .key_release = NULL,
                .mouse_press = NULL,
       -        .mouse_release = &ltk_button_mouse_release,
       +        .mouse_release = NULL,
       +        .release = &ltk_button_release,
                .motion_notify = NULL,
                .mouse_leave = NULL,
                .mouse_enter = NULL,
       t@@ -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 */
       t@@ -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 *
       t@@ -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;
        
       t@@ -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);
        }
        
   DIR diff --git a/src/config.c b/src/config.c
       t@@ -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;
       +}
   DIR diff --git a/src/config.h b/src/config.h
       t@@ -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 */
   DIR diff --git a/src/event.h b/src/event.h
       t@@ -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;
       t@@ -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;
       t@@ -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;
       t@@ -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 */
   DIR diff --git a/src/event_xlib.c b/src/event_xlib.c
       t@@ -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) {
       t@@ -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 = {
       t@@ -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,
       t@@ -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;
                }
        }
   DIR diff --git a/src/eventdefs.h b/src/eventdefs.h
       t@@ -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 */
   DIR diff --git a/src/graphics.h b/src/graphics.h
       t@@ -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 */
   DIR diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
       t@@ -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"
       t@@ -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;
       t@@ -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);
       t@@ -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
   DIR diff --git a/src/grid.c b/src/grid.c
       t@@ -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);
       t@@ -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 = &ltk_grid_draw,
                .destroy = &ltk_grid_destroy,
       t@@ -68,6 +79,15 @@ static struct ltk_widget_vtable vtable = {
                .mouse_enter = NULL,
                .key_press = NULL,
                .key_release = NULL,
       +        .prev_child = &ltk_grid_prev_child,
       +        .next_child = &ltk_grid_next_child,
       +        .first_child = &ltk_grid_first_child,
       +        .last_child = &ltk_grid_last_child,
       +        .nearest_child = &ltk_grid_nearest_child,
       +        .nearest_child_left = &ltk_grid_nearest_child_left,
       +        .nearest_child_right = &ltk_grid_nearest_child_right,
       +        .nearest_child_above = &ltk_grid_nearest_child_above,
       +        .nearest_child_below = &ltk_grid_nearest_child_below,
                .type = LTK_WIDGET_GRID,
                .flags = 0,
        };
       t@@ -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));
                }
        }
        
       t@@ -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];
       t@@ -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);
                                }
                        }
                }
       t@@ -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);
        }
        
       t@@ -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;
       t@@ -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
       t@@ -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);
                        }
                }
        }
       t@@ -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);
        }
        
       t@@ -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;
        }
       t@@ -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;
        }
       t@@ -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;
       t@@ -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(
   DIR diff --git a/src/label.c b/src/label.c
       t@@ -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);
       t@@ -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;
        
       t@@ -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},
        };
       t@@ -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);
       t@@ -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;
       t@@ -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);
        }
        
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -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;
       t@@ -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);
       t@@ -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
   DIR diff --git a/src/ltkd.c b/src/ltkd.c
       t@@ -29,6 +29,7 @@
        #include <unistd.h>
        #include <signal.h>
        #include <stdint.h>
       +#include <locale.h>
        #include <inttypes.h>
        
        #include <sys/un.h>
       t@@ -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
       t@@ -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) {
       t@@ -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;
       t@@ -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);
                        }
        
       t@@ -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);
       t@@ -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;
       t@@ -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) {
       t@@ -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);
        }
       t@@ -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);
                                }
                        }
       t@@ -696,6 +742,7 @@ ltk_window_register_popup(ltk_window *window, ltk_widget *popup) {
                        );
                }
                window->popups[window->popups_num++] = popup;
       +        popup->popup = 1;
        }
        
        void
       t@@ -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,
       t@@ -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;
       t@@ -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},
       t@@ -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);
       t@@ -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;
       t@@ -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);
       t@@ -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;
                        }
       t@@ -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);
       t@@ -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);
       t@@ -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);
       t@@ -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")) {
   DIR diff --git a/src/memory.c b/src/memory.c
       t@@ -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);
       t@@ -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;
       +}
   DIR diff --git a/src/memory.h b/src/memory.h
       t@@ -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);
       t@@ -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);
       t@@ -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 */
   DIR diff --git a/src/menu.c b/src/menu.c
       t@@ -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);
       t@@ -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)
        
       t@@ -135,6 +149,16 @@ static struct ltk_widget_vtable vtable = {
                .destroy = &ltk_menu_destroy,
                .child_size_change = &recalc_ideal_menu_size,
                .remove_child = &ltk_menu_remove_child,
       +        .prev_child = &ltk_menu_prev_child,
       +        .next_child = &ltk_menu_next_child,
       +        .first_child = &ltk_menu_first_child,
       +        .last_child = &ltk_menu_last_child,
       +        .nearest_child = &ltk_menu_nearest_child,
       +        .nearest_child_left = &ltk_menu_nearest_child_left,
       +        .nearest_child_right = &ltk_menu_nearest_child_right,
       +        .nearest_child_above = &ltk_menu_nearest_child_above,
       +        .nearest_child_below = &ltk_menu_nearest_child_below,
       +        .ensure_rect_shown = &ltk_menu_ensure_rect_shown,
                .type = LTK_WIDGET_MENU,
                .flags = LTK_NEEDS_REDRAW,
        };
       t@@ -144,7 +168,8 @@ static struct ltk_widget_vtable entry_vtable = {
                .key_release = NULL,
                .mouse_press = NULL,
                .motion_notify = NULL,
       -        .mouse_release = &ltk_menuentry_mouse_release,
       +        .mouse_release = NULL,
       +        .release = &ltk_menuentry_release,
                .mouse_enter = NULL,
                .mouse_leave = NULL,
                .get_child_at_pos = NULL,
       t@@ -154,7 +179,9 @@ static struct ltk_widget_vtable entry_vtable = {
                .draw = &ltk_menuentry_draw,
                .destroy = &ltk_menuentry_destroy,
                .child_size_change = NULL,
       -        .remove_child = NULL,
       +        .remove_child = &ltk_menuentry_remove_child,
       +        .first_child = &ltk_menuentry_get_child,
       +        .last_child = &ltk_menuentry_get_child,
                .type = LTK_WIDGET_MENUENTRY,
                .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
        };
       t@@ -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;
       t@@ -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;
       t@@ -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] = {
       t@@ -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;
        }
       t@@ -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
       t@@ -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);
                }
        }
        
       t@@ -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);
                }
        }
        
       t@@ -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;
       t@@ -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 &&
       t@@ -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;
       t@@ -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) {
       t@@ -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;
       t@@ -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;
       t@@ -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) {
       t@@ -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;
       t@@ -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) */
       t@@ -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) {
       t@@ -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
       t@@ -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;
       t@@ -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
       t@@ -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);
       t@@ -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;
        }
        
       t@@ -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) {
       t@@ -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);
        }
        
   DIR diff --git a/src/menu.h b/src/menu.h
       t@@ -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;
   DIR diff --git a/src/scrollbar.c b/src/scrollbar.c
       t@@ -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);
       t@@ -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;
       t@@ -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;
       t@@ -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
       t@@ -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;
       t@@ -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;
       t@@ -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);
   DIR diff --git a/src/theme.h b/src/theme.h
       t@@ -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_ */
   DIR diff --git a/src/util.c b/src/util.c
       t@@ -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;
       +}
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -18,6 +18,7 @@
        #define _LTK_UTIL_H_
        
        #include <stdarg.h>
       +#include <stddef.h>
        
        long long ltk_strtonum(
            const char *numstr, long long minval,
       t@@ -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_ */
   DIR diff --git a/src/widget.c b/src/widget.c
       t@@ -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>
         *
       t@@ -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);
        
       t@@ -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;
       t@@ -174,6 +184,7 @@ ltk_widgets_init() {
        
        void
        ltk_widgets_cleanup() {
       +        free(widget_stack);
                if (widget_hash)
                        ltk_destroy_widget_hash();
        }
       t@@ -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;
       t@@ -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;
       t@@ -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;
       t@@ -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
                                );
                        }
                }
       t@@ -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++) {
       t@@ -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;
        }
       t@@ -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;
       t@@ -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;
       t@@ -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 */
                                );
                        }
                }
       t@@ -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) {
       t@@ -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);
                }
       t@@ -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 */
       t@@ -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;
                                }
                        }
       t@@ -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)) {
       t@@ -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;
       t@@ -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;
       t@@ -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;
                                }
       t@@ -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
       t@@ -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;
   DIR diff --git a/src/widget.h b/src/widget.h
       t@@ -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;
       t@@ -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"
       t@@ -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;
        
       t@@ -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;
        };
       t@@ -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;
       t@@ -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);
   DIR diff --git a/src/widget_config.h b/src/widget_config.h
       t@@ -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 */
   DIR diff --git a/src/xlib_shared.h b/src/xlib_shared.h
       t@@ -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 */
   DIR diff --git a/test.gui b/test.gui
       t@@ -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
       t@@ -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
   DIR diff --git a/test.sh b/test.sh
       t@@ -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"`
   DIR diff --git a/test2.gui b/test2.gui
       t@@ -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
   DIR diff --git a/test3.gui b/test3.gui
       t@@ -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
   DIR diff --git a/test3.sh b/test3.sh
       t@@ -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
   DIR diff --git a/testbox.sh b/testbox.sh
       t@@ -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++ }