URI: 
       tRe-add client-server functionality - ltk - GUI toolkit 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 9de2d4ac1734343f1de02c9ec29e408ccc05689c
   DIR parent 43bb385257c126c200662ed207f27a7a285f113e
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Mon, 25 Mar 2024 19:16:53 +0100
       
       Re-add client-server functionality
       
       Diffstat:
         M .gitignore                          |       1 -
         M LICENSE                             |       4 ++--
         M Makefile                            |     190 +++++++++++++++++++------------
         M README.md                           |      17 ++++++++++++++++-
         D examples/.gitignore                 |       2 --
         A examples/ltk/.gitignore             |       2 ++
         A examples/ltk/test.c                 |     100 +++++++++++++++++++++++++++++++
         R examples/test.jpg -> examples/ltk/… |       0 
         A examples/ltkd/.gitignore            |       1 +
         A examples/ltkd/test.gui              |      27 +++++++++++++++++++++++++++
         A examples/ltkd/test.sh               |      23 +++++++++++++++++++++++
         A examples/ltkd/test2.gui             |      45 +++++++++++++++++++++++++++++++
         A examples/ltkd/test2.sh              |      11 +++++++++++
         A examples/ltkd/test3.gui             |      16 ++++++++++++++++
         A examples/ltkd/test3.sh              |      21 +++++++++++++++++++++
         A examples/ltkd/testbox.sh            |      30 ++++++++++++++++++++++++++++++
         A examples/ltkd/testimg.sh            |      14 ++++++++++++++
         D examples/test.c                     |      87 -------------------------------
         D src/box.c                           |     470 -------------------------------
         D src/graphics.h                      |      76 -------------------------------
         D src/graphics_xlib.c                 |     600 -------------------------------
         D src/grid.c                          |     546 -------------------------------
         D src/ltk.c                           |     672 -------------------------------
         D src/ltk.h                           |      51 -------------------------------
         R src/.gitignore -> src/ltk/.gitigno… |       0 
         R src/array.h -> src/ltk/array.h      |       0 
         A src/ltk/box.c                       |     470 +++++++++++++++++++++++++++++++
         R src/box.h -> src/ltk/box.h          |       0 
         R src/button.c -> src/ltk/button.c    |       0 
         R src/button.h -> src/ltk/button.h    |       0 
         R src/clipboard.h -> src/ltk/clipboa… |       0 
         R src/clipboard_xlib.c -> src/ltk/cl… |       0 
         R src/clipboard_xlib.h -> src/ltk/cl… |       0 
         R src/color.h -> src/ltk/color.h      |       0 
         R src/color_xlib.c -> src/ltk/color_… |       0 
         R src/config.c -> src/ltk/config.c    |       0 
         R src/config.h -> src/ltk/config.h    |       0 
         R src/ctrlsel.c -> src/ltk/ctrlsel.c  |       0 
         R src/ctrlsel.h -> src/ltk/ctrlsel.h  |       0 
         R src/entry.c -> src/ltk/entry.c      |       0 
         R src/entry.h -> src/ltk/entry.h      |       0 
         R src/event.h -> src/ltk/event.h      |       0 
         R src/event_xlib.c -> src/ltk/event_… |       0 
         R src/eventdefs.h -> src/ltk/eventde… |       0 
         A src/ltk/graphics.h                  |      77 +++++++++++++++++++++++++++++++
         A src/ltk/graphics_xlib.c             |     600 +++++++++++++++++++++++++++++++
         R src/graphics_xlib.h -> src/ltk/gra… |       0 
         A src/ltk/grid.c                      |     546 +++++++++++++++++++++++++++++++
         R src/grid.h -> src/ltk/grid.h        |       0 
         R src/image.h -> src/ltk/image.h      |       0 
         R src/image_imlib.c -> src/ltk/image… |       0 
         R src/image_widget.c -> src/ltk/imag… |       0 
         R src/image_widget.h -> src/ltk/imag… |       0 
         R src/ini.c -> src/ltk/ini.c          |       0 
         R src/ini.h -> src/ltk/ini.h          |       0 
         R src/keys.h -> src/ltk/keys.h        |       0 
         R src/label.c -> src/ltk/label.c      |       0 
         R src/label.h -> src/ltk/label.h      |       0 
         A src/ltk/ltk.c                       |     667 +++++++++++++++++++++++++++++++
         A src/ltk/ltk.h                       |      58 ++++++++++++++++++++++++++++++
         R src/macros.h -> src/ltk/macros.h    |       0 
         R src/memory.c -> src/ltk/memory.c    |       0 
         R src/memory.h -> src/ltk/memory.h    |       0 
         R src/menu.c -> src/ltk/menu.c        |       0 
         R src/menu.h -> src/ltk/menu.h        |       0 
         R src/rect.c -> src/ltk/rect.c        |       0 
         R src/rect.h -> src/ltk/rect.h        |       0 
         R src/scrollbar.c -> src/ltk/scrollb… |       0 
         R src/scrollbar.h -> src/ltk/scrollb… |       0 
         R src/stb_truetype.c -> src/ltk/stb_… |       0 
         R src/stb_truetype.h -> src/ltk/stb_… |       0 
         R src/strtonum.c -> src/ltk/strtonum… |       0 
         R src/surface_cache.c -> src/ltk/sur… |       0 
         R src/surface_cache.h -> src/ltk/sur… |       0 
         R src/text.h -> src/ltk/text.h        |       0 
         R src/text_pango.c -> src/ltk/text_p… |       0 
         R src/text_stb.c -> src/ltk/text_stb… |       0 
         R src/theme.c -> src/ltk/theme.c      |       0 
         R src/theme.h -> src/ltk/theme.h      |       0 
         R src/txtbuf.c -> src/ltk/txtbuf.c    |       0 
         R src/txtbuf.h -> src/ltk/txtbuf.h    |       0 
         A src/ltk/util.c                      |     394 +++++++++++++++++++++++++++++++
         A src/ltk/util.h                      |     100 +++++++++++++++++++++++++++++++
         A src/ltk/widget.c                    |     295 ++++++++++++++++++++++++++++++
         A src/ltk/widget.h                    |     346 +++++++++++++++++++++++++++++++
         A src/ltk/window.c                    |    1319 +++++++++++++++++++++++++++++++
         R src/window.h -> src/ltk/window.h    |       0 
         A src/ltkd/.gitignore                 |       4 ++++
         A src/ltkd/box.c                      |     108 +++++++++++++++++++++++++++++++
         A src/ltkd/button.c                   |      72 +++++++++++++++++++++++++++++++
         A src/ltkd/cmd.c                      |     185 ++++++++++++++++++++++++++++++
         A src/ltkd/cmd.h                      |     157 +++++++++++++++++++++++++++++++
         A src/ltkd/cmd_helpers.h              |      31 +++++++++++++++++++++++++++++++
         A src/ltkd/entry.c                    |      60 +++++++++++++++++++++++++++++++
         A src/ltkd/err.c                      |      46 +++++++++++++++++++++++++++++++
         A src/ltkd/err.h                      |      49 +++++++++++++++++++++++++++++++
         A src/ltkd/grid.c                     |     174 +++++++++++++++++++++++++++++++
         A src/ltkd/image_widget.c             |      68 +++++++++++++++++++++++++++++++
         A src/ltkd/khash.h                    |     627 +++++++++++++++++++++++++++++++
         A src/ltkd/label.c                    |      59 +++++++++++++++++++++++++++++++
         A src/ltkd/ltkc.c                     |     272 +++++++++++++++++++++++++++++++
         A src/ltkd/ltkc_img.c                 |      42 +++++++++++++++++++++++++++++++
         A src/ltkd/ltkd.c                     |    1097 +++++++++++++++++++++++++++++++
         A src/ltkd/ltkd.h                     |      43 ++++++++++++++++++++++++++++++
         A src/ltkd/menu.c                     |     272 +++++++++++++++++++++++++++++++
         A src/ltkd/proto_types.h              |      51 +++++++++++++++++++++++++++++++
         A src/ltkd/socket_format.txt          |     106 ++++++++++++++++++++++++++++++
         A src/ltkd/util.c                     |      31 +++++++++++++++++++++++++++++++
         A src/ltkd/util.h                     |      22 ++++++++++++++++++++++
         A src/ltkd/widget.c                   |     553 +++++++++++++++++++++++++++++++
         A src/ltkd/widget.h                   |      87 +++++++++++++++++++++++++++++++
         D src/util.c                          |     436 -------------------------------
         D src/util.h                          |      62 -------------------------------
         D src/widget.c                        |     241 -------------------------------
         D src/widget.h                        |     320 -------------------------------
         D src/window.c                        |    1270 -------------------------------
       
       116 files changed, 9514 insertions(+), 4909 deletions(-)
       ---
   DIR diff --git a/.gitignore b/.gitignore
       t@@ -1,3 +1,2 @@
        *.o
        *.core
       -ltkd
   DIR diff --git a/LICENSE b/LICENSE
       t@@ -1,5 +1,5 @@
       -See src/khash.h, src/ini.*, src/stb_truetype.*, src/strtonum.c,
       -src/ctrlsel.*, and src/macros.h for third-party licenses.
       +See src/ltkd/khash.h, src/ltk/ini.*, src/ltk/stb_truetype.*, src/ltk/strtonum.c,
       +src/ltk/ctrlsel.*, and src/ltk/macros.h for third-party licenses.
        
        ISC License
        
   DIR diff --git a/Makefile b/Makefile
       t@@ -1,3 +1,5 @@
       +# Yes, I know this is a mess.
       +
        .POSIX:
        .SUFFIXES: .c .o
        
       t@@ -24,10 +26,10 @@ DEV_CFLAGS_0 = $(CFLAGS)
        DEV_LDFLAGS_0 = $(LDFLAGS)
        
        # stb rendering
       -EXTRA_OBJ_0 = src/stb_truetype.o src/text_stb.o
       +EXTRA_OBJ_0 = src/ltk/stb_truetype.o src/ltk/text_stb.o
        
        # pango rendering
       -EXTRA_OBJ_1 = src/text_pango.o
       +EXTRA_OBJ_1 = src/ltk/text_pango.o
        EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft`
        EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft`
        
       t@@ -38,80 +40,124 @@ EXTRA_LDFLAGS = $(SANITIZE_FLAGS_$(SANITIZE)) $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFL
        LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -DMEMDEBUG=$(MEMDEBUG) -I ./src -std=c99 `pkg-config --cflags x11 fontconfig xext xcursor imlib2` -D_POSIX_C_SOURCE=200809L
        LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext xcursor imlib2`
        
       -OBJ = \
       -        examples/test.o \
       -        src/strtonum.o \
       -        src/util.o \
       -        src/memory.o \
       -        src/window.o \
       -        src/color_xlib.o \
       -        src/rect.o \
       -        src/widget.o \
       -        src/ltk.o \
       -        src/ini.o \
       -        src/button.o \
       -        src/theme.o \
       -        src/graphics_xlib.o \
       -        src/surface_cache.o \
       -        src/event_xlib.o \
       -        src/grid.o \
       -        src/config.o \
       -        src/clipboard_xlib.o \
       -        src/txtbuf.o \
       -        src/ctrlsel.o \
       -        src/label.o \
       -        src/image_imlib.o \
       -        src/image_widget.o \
       -        src/entry.o \
       -        src/menu.o \
       -        src/box.o \
       -        src/scrollbar.o \
       +OBJ_LTK = \
       +        src/ltk/strtonum.o \
       +        src/ltk/util.o \
       +        src/ltk/memory.o \
       +        src/ltk/window.o \
       +        src/ltk/color_xlib.o \
       +        src/ltk/rect.o \
       +        src/ltk/widget.o \
       +        src/ltk/ltk.o \
       +        src/ltk/ini.o \
       +        src/ltk/button.o \
       +        src/ltk/theme.o \
       +        src/ltk/graphics_xlib.o \
       +        src/ltk/surface_cache.o \
       +        src/ltk/event_xlib.o \
       +        src/ltk/grid.o \
       +        src/ltk/config.o \
       +        src/ltk/clipboard_xlib.o \
       +        src/ltk/txtbuf.o \
       +        src/ltk/ctrlsel.o \
       +        src/ltk/label.o \
       +        src/ltk/image_imlib.o \
       +        src/ltk/image_widget.o \
       +        src/ltk/entry.o \
       +        src/ltk/menu.o \
       +        src/ltk/box.o \
       +        src/ltk/scrollbar.o \
                $(EXTRA_OBJ)
        
       +OBJ_LTKD = \
       +        src/ltkd/box.o \
       +        src/ltkd/button.o \
       +        src/ltkd/cmd.o \
       +        src/ltkd/entry.o \
       +        src/ltkd/err.o \
       +        src/ltkd/grid.o \
       +        src/ltkd/image_widget.o \
       +        src/ltkd/label.o \
       +        src/ltkd/ltkd.o \
       +        src/ltkd/menu.o \
       +        src/ltkd/util.o \
       +        src/ltkd/widget.o
       +
       +OBJ_TEST = examples/ltk/test.o
        # Note: This could be improved so a change in a header only causes the .c files
        # which include that header to be recompiled, but the compile times are
        # currently so short that I don't really care.
       -HDR = \
       -        src/button.h \
       -        src/color.h \
       -        src/ini.h \
       -        src/label.h \
       -        src/rect.h \
       -        src/widget.h \
       -        src/ltk.h \
       -        src/grid.h \
       -        src/memory.h \
       -        src/stb_truetype.h \
       -        src/text.h \
       -        src/util.h \
       -        src/theme.h \
       -        src/graphics.h \
       -        src/surface_cache.h \
       -        src/macros.h \
       -        src/event.h \
       -        src/eventdefs.h \
       -        src/graphics_xlib.h \
       -        src/label.h \
       -        src/config.h \
       -        src/array.h \
       -        src/keys.h \
       -        src/clipboard_xlib.h \
       -        src/clipboard.h \
       -        src/txtbuf.h \
       -        src/ctrlsel.h \
       -        src/image.h \
       -        src/image_widget.h \
       -        src/entry.h \
       -        src/menu.h \
       -        src/box.h \
       -        src/scrollbar.h
       -
       -all: examples/test
       -
       -examples/test: $(OBJ)
       -        $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
       -
       -$(OBJ) : $(HDR)
       +HDR_LTK = \
       +        src/ltk/button.h \
       +        src/ltk/color.h \
       +        src/ltk/ini.h \
       +        src/ltk/label.h \
       +        src/ltk/rect.h \
       +        src/ltk/widget.h \
       +        src/ltk/ltk.h \
       +        src/ltk/grid.h \
       +        src/ltk/memory.h \
       +        src/ltk/stb_truetype.h \
       +        src/ltk/text.h \
       +        src/ltk/util.h \
       +        src/ltk/theme.h \
       +        src/ltk/graphics.h \
       +        src/ltk/surface_cache.h \
       +        src/ltk/macros.h \
       +        src/ltk/event.h \
       +        src/ltk/eventdefs.h \
       +        src/ltk/graphics_xlib.h \
       +        src/ltk/label.h \
       +        src/ltk/config.h \
       +        src/ltk/array.h \
       +        src/ltk/keys.h \
       +        src/ltk/clipboard_xlib.h \
       +        src/ltk/clipboard.h \
       +        src/ltk/txtbuf.h \
       +        src/ltk/ctrlsel.h \
       +        src/ltk/image.h \
       +        src/ltk/image_widget.h \
       +        src/ltk/entry.h \
       +        src/ltk/menu.h \
       +        src/ltk/box.h \
       +        src/ltk/scrollbar.h
       +
       +HDR_LTKD = \
       +        src/ltkd/cmd.h \
       +        src/ltkd/cmd_helpers.h \
       +        src/ltkd/err.h \
       +        src/ltkd/khash.h \
       +        src/ltkd/ltkd.h \
       +        src/ltkd/proto_types.h \
       +        src/ltkd/widget.h
       +
       +all: examples/ltk/test src/ltkd/ltkd src/ltkd/ltkc src/ltkd/ltkc_img
       +
       +test: examples/ltk/test
       +
       +ltkd: src/ltkd/ltkd
       +
       +ltkc: src/ltkd/ltkc
       +
       +ltkc_img: src/ltkd/ltkc_img
       +
       +examples/ltk/test: $(OBJ_LTK) $(OBJ_TEST)
       +        $(CC) -o $@ $(OBJ_LTK) $(OBJ_TEST) $(LTK_LDFLAGS)
       +
       +src/ltkd/ltkd: $(OBJ_LTK) $(OBJ_LTKD)
       +        $(CC) -o $@ $(OBJ_LTK) $(OBJ_LTKD) $(LTK_LDFLAGS)
       +
       +src/ltkd/ltkc: $(OBJ_LTK) src/ltkd/ltkc.o src/ltkd/util.o
       +        $(CC) -o $@ $(OBJ_LTK) src/ltkd/ltkc.o src/ltkd/util.o $(LTK_LDFLAGS)
       +
       +src/ltkd/ltkc_img: $(OBJ_LTK) src/ltkd/ltkc_img.o
       +        $(CC) -o $@ $(OBJ_LTK) src/ltkd/ltkc_img.o $(LTK_LDFLAGS)
       +
       +$(OBJ_LTK) : $(HDR_LTK)
       +
       +$(OBJ_TEST) : $(HDR_LTK)
       +
       +$(OBJ_LTKD) : $(HDR_LTK) $(HDR_LTKD)
        
        .c.o:
                $(CC) -c -o $@ $< $(LTK_CFLAGS)
       t@@ -119,4 +165,4 @@ $(OBJ) : $(HDR)
        .PHONY: clean
        
        clean:
       -        rm -f src/*.o examples/test examples/*.o
       +        rm -f src/ltkd/*.o src/ltk/*.o src/ltkd/ltkd src/ltkd/ltkc src/ltkd/ltkc_img examples/ltk/test examples/ltk/*.o
   DIR diff --git a/README.md b/README.md
       t@@ -8,10 +8,25 @@ To build with or without pango: Follow instructions in Makefile.
        Note: The basic (non-pango) text doesn't work properly on all systems.
        Note: The basic (non-pango) text is currently completely broken.
        
       +The toolkit has now been split into two parts:
       +
       +* ltk is a regular GUI toolkit.
       +* ltkd is the client-server based toolkit that was previously the only
       +  way to use ltk.
       +
       +The current plan is to focus on ltk before continuing work on ltkd.
       +
        To test:
        
        make
       -cd examples && LTKDIR=../config.example/ ./test
       +cd examples/ltk && LTKDIR=../../config.example/ ./test
       +
       +Alternatively, run one of the shell scripts in examples/ltkd to test the
       +client-server functionality.
       +
       +You can also run 'make test' to only compile the test that does not use
       +the client-server functionality or 'make ltkd ltkc ltkc_img' to compile
       +only the binaries needed for the client-server functionality.
        
        Note: I know the default theme is butt-ugly at the moment. It is mainly
        to test things, not to look pretty.
   DIR diff --git a/examples/.gitignore b/examples/.gitignore
       t@@ -1,2 +0,0 @@
       -*.o
       -test
   DIR diff --git a/examples/ltk/.gitignore b/examples/ltk/.gitignore
       t@@ -0,0 +1,2 @@
       +test
       +*.o
   DIR diff --git a/examples/ltk/test.c b/examples/ltk/test.c
       t@@ -0,0 +1,100 @@
       +#include <stdio.h>
       +
       +#include <ltk/ltk.h>
       +#include <ltk/label.h>
       +#include <ltk/button.h>
       +#include <ltk/image.h>
       +#include <ltk/image_widget.h>
       +#include <ltk/grid.h>
       +#include <ltk/entry.h>
       +#include <ltk/menu.h>
       +#include <ltk/box.h>
       +
       +int
       +quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)self;
       +        (void)args;
       +        (void)data;
       +        ltk_mainloop_quit();
       +        return 1;
       +}
       +
       +int
       +printstuff(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)self;
       +        (void)args;
       +        printf("%d\n", LTK_CAST_ARG_INT(data));
       +        return 1;
       +}
       +
       +int
       +printstate(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)self;
       +        (void)data;
       +        int state = LTK_GET_ARG_INT(args, 0);
       +        printf("%d\n", state);
       +        return 0;
       +}
       +
       +int
       +main(int argc, char *argv[]) {
       +        (void)argc;
       +        (void)argv;
       +        ltk_init();
       +
       +        ltk_window *window = ltk_window_create("Hi", 0, 0, 500, 500);
       +        ltk_grid *grid = ltk_grid_create(window, 5, 2);
       +        ltk_grid_set_column_weight(grid, 0, 1);
       +        ltk_grid_set_column_weight(grid, 1, 1);
       +        ltk_grid_set_row_weight(grid, 4, 1);
       +        ltk_button *button = ltk_button_create(window, "I'm a button!");
       +        ltk_button *button1 = ltk_button_create(window, "I'm also a button!");
       +        ltk_label *label = ltk_label_create(window, "I'm a label!");
       +        ltk_image *img = ltk_image_create_from_path("test.jpg");
       +        if (!img) {
       +                fprintf(stderr, "Unable to load image.\n");
       +                return 1;
       +        }
       +        ltk_image_widget *iw = ltk_image_widget_create(window, img);
       +        ltk_entry *entry = ltk_entry_create(window, "");
       +        ltk_menu *menu = ltk_menu_create(window);
       +        ltk_menuentry *e1 = ltk_menuentry_create(window, "Hi");
       +        ltk_menuentry *e2 = ltk_menuentry_create(window, "I'm a submenu");
       +        ltk_menu_add_entry(menu, e1);
       +        ltk_menu_add_entry(menu, e2);
       +        ltk_menu *submenu = ltk_submenu_create(window);
       +        ltk_menuentry *e3 = ltk_menuentry_create(window, "Menu Entry 1");
       +        ltk_menuentry *e4 = ltk_menuentry_create(window, "Quit");
       +        ltk_menu_add_entry(submenu, e3);
       +        ltk_menu_add_entry(submenu, e4);
       +        ltk_menuentry_attach_submenu(e2, submenu);
       +
       +        ltk_box *box = ltk_box_create(window, LTK_VERTICAL);
       +        ltk_button *btn1 = ltk_button_create(window, "Bla1");
       +        ltk_button *btn2 = ltk_button_create(window, "Bla2");
       +        ltk_button *btn3 = ltk_button_create(window, "Bla3");
       +        ltk_button *btn4 = ltk_button_create(window, "Bla4");
       +        ltk_button *btn5 = ltk_button_create(window, "Bla5");
       +        ltk_box_add(box, LTK_CAST_WIDGET(btn1), LTK_STICKY_LEFT);
       +        ltk_box_add(box, LTK_CAST_WIDGET(btn2), LTK_STICKY_LEFT);
       +        ltk_box_add(box, LTK_CAST_WIDGET(btn3), LTK_STICKY_LEFT);
       +        ltk_box_add(box, LTK_CAST_WIDGET(btn4), LTK_STICKY_LEFT);
       +        ltk_box_add(box, LTK_CAST_WIDGET(btn5), LTK_STICKY_LEFT);
       +
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(iw), 2, 1, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_PRESERVE_ASPECT_RATIO);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(entry), 3, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       +        ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM);
       +        ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid));
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5));
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID);
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_WIDGET_SIGNAL_CHANGE_STATE, &printstate, LTK_ARG_VOID);
       +
       +        ltk_mainloop();
       +        return 0;
       +}
   DIR diff --git a/examples/test.jpg b/examples/ltk/test.jpg
       Binary files differ.
   DIR diff --git a/examples/ltkd/.gitignore b/examples/ltkd/.gitignore
       t@@ -0,0 +1 @@
       +.ltkd
   DIR diff --git a/examples/ltkd/test.gui b/examples/ltkd/test.gui
       t@@ -0,0 +1,27 @@
       +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 lrtb
       +button btn1 create "I'm a button!"
       +button btn2 create "I'm also a button!"
       +button btn3 create "I'm another boring button."
       +box box1 add btn1 lr
       +box box1 add btn2 r
       +box box1 add btn3
       +box box2 create vertical
       +grid grd1 add box2 1 0 1 1 lrtb
       +button btn4 create "2 I'm a button!"
       +button btn5 create "2 I'm also a button!"
       +button btn6 create "2 I'm another boring button."
       +box box2 add btn4 lr
       +box box2 add btn5 r
       +box box2 add btn6
       +button btn7 create "Button 7"
       +button btn8 create "Button 8"
       +grid grd1 add btn7 0 1 1 1 lrtb
       +grid grd1 add btn8 1 1 1 1 lr
       +mask-add btn1 button press
   DIR diff --git a/examples/ltkd/test.sh b/examples/ltkd/test.sh
       t@@ -0,0 +1,23 @@
       +#!/bin/sh
       +
       +# This is very hacky.
       +
       +export LTKDDIR=".ltkd"
       +export LTKDIR="../../config.example"
       +ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
       +if [ $? -ne 0 ]; then
       +        echo "Unable to start ltkd." >&2
       +        exit 1
       +fi
       +
       +cat test.gui | ../../src/ltkd/ltkc $ltk_id | while read cmd
       +do
       +        case "$cmd" in
       +        *"event btn1 button press")
       +                echo "quit"
       +                ;;
       +        *)
       +                printf "%s\n" "$cmd" >&2
       +                ;;
       +        esac
       +done | ../../src/ltkd/ltkc $ltk_id
   DIR diff --git a/examples/ltkd/test2.gui b/examples/ltkd/test2.gui
       t@@ -0,0 +1,45 @@
       +grid grd1 create 2 1
       +grid grd1 set-row-weight 1 1
       +grid grd1 set-column-weight 0 1
       +set-root-widget grd1
       +menu menu1 create
       +menuentry entry1 create "Entry 1"
       +menuentry entry2 create "Entry 2"
       +menuentry entry3 create "Entry 3"
       +menuentry entry4 create "Entry 4"
       +menuentry entry5 create "Entry 5"
       +menuentry entry6 create "Entry 6"
       +menuentry entry7 create "Entry 7"
       +menuentry entry8 create "Entry 8"
       +menuentry entry9 create "Entry 9"
       +menuentry entry10 create "Entry 10"
       +menuentry entry11 create "Entry 11"
       +menuentry entry12 create "Entry 12"
       +menuentry entry13 create "Entry 13"
       +menuentry entry14 create "Entry 14"
       +menuentry entry15 create "Entry 15"
       +menuentry entry16 create "Entry 16"
       +menu menu1 add-entry entry1
       +menu menu1 add-entry entry2
       +menu menu1 add-entry entry12
       +submenu submenu1 create
       +menu submenu1 add-entry entry3
       +menu submenu1 add-entry entry4
       +menu submenu1 add-entry entry5
       +menu submenu1 add-entry entry6
       +menu submenu1 add-entry entry7
       +menu submenu1 add-entry entry8
       +menu submenu1 add-entry entry9
       +menu submenu1 add-entry entry10
       +menu submenu1 add-entry entry11
       +menuentry entry12 attach-submenu submenu1
       +submenu submenu2 create
       +menu submenu2 add-entry entry13
       +menu submenu2 add-entry entry15
       +menu submenu1 add-entry entry14
       +menuentry entry14 attach-submenu submenu2
       +submenu submenu3 create
       +menu submenu3 add-entry entry16
       +menuentry entry15 attach-submenu submenu3
       +grid grd1 add menu1 0 0 1 1 lr
       +mask-add entry10 menuentry press
   DIR diff --git a/examples/ltkd/test2.sh b/examples/ltkd/test2.sh
       t@@ -0,0 +1,11 @@
       +#!/bin/sh
       +
       +export LTKDDIR=".ltkd"
       +export LTKDIR="../../config.example"
       +ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
       +if [ $? -ne 0 ]; then
       +        echo "Unable to start ltkd." >&2
       +        exit 1
       +fi
       +
       +cat test2.gui | ../../src/ltkd/ltkc $ltk_id
   DIR diff --git a/examples/ltkd/test3.gui b/examples/ltkd/test3.gui
       t@@ -0,0 +1,16 @@
       +grid grd1 create 4 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-row-weight 3 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
       +entry entry1 create "Hi"
       +grid grd1 add entry1 3 0 1 1 w
   DIR diff --git a/examples/ltkd/test3.sh b/examples/ltkd/test3.sh
       t@@ -0,0 +1,21 @@
       +#!/bin/sh
       +
       +export LTKDDIR=".ltkd"
       +export LTKDIR="../../config.example"
       +ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
       +#if [ $? -ne 0 ]; then
       +#        echo "Unable to start ltkd." >&2
       +#        exit 1
       +#fi
       +
       +cat test3.gui | ../../src/ltkd/ltkc $ltk_id | while read cmd
       +do
       +        case "$cmd" in
       +        *"event btn1 button press")
       +                echo "quit"
       +                ;;
       +        *)
       +                printf "client1: %s\n" "$cmd" >&2
       +                ;;
       +        esac
       +done | ../../src/ltkd/ltkc $ltk_id
   DIR diff --git a/examples/ltkd/testbox.sh b/examples/ltkd/testbox.sh
       t@@ -0,0 +1,30 @@
       +#!/bin/sh
       +
       +export LTKDDIR=".ltkd"
       +export LTKDIR="../../config.example"
       +ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
       +if [ $? -ne 0 ]; then
       +        echo "Unable to start ltkd." >&2
       +        exit 1
       +fi
       +
       +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++ }
       +/^[1gI]/ { printf "button btn%s create \"%s\"\nbox box1 add btn%s w\n", btn, substr($1, 2), btn; btn++ }')
       +mask-add btn0 button press"
       +echo "$cmds" | ../../src/ltkd/ltkc $ltk_id | while read cmd
       +do
       +        case "$cmd" in
       +        *"event exit_btn button press")
       +                echo "quit"
       +                ;;
       +        *"event btn0 button press")
       +                echo "button bla create \"safhaskfldshk\"\nbox box1 add bla w"
       +                ;;
       +        *)
       +                printf "%s\n" "$cmd" >&2
       +                ;;
       +        esac
       +done | ../../src/ltkd/ltkc $ltk_id > /dev/null
   DIR diff --git a/examples/ltkd/testimg.sh b/examples/ltkd/testimg.sh
       t@@ -0,0 +1,14 @@
       +#!/bin/sh
       +
       +export LTKDDIR=".ltkd"
       +export LTKDIR="../../config.example"
       +ltk_id=`../../src/ltkd/ltkd -t "Cool Window"`
       +#if [ $? -ne 0 ]; then
       +#        echo "Unable to start ltkd." >&2
       +#        exit 1
       +#fi
       +
       +{ printf "grid grd1 create 2 1\ngrid grd1 set-row-weight 0 1\ngrid grd1 set-row-weight 1 1\ngrid grd1 set-column-weight 0 1\nset-root-widget grd1\nimage img1 create test.jpg \""; ../../src/ltkd/ltkc_img < ../ltk/test.jpg; printf "\"\ngrid grd1 add img1 1 0 1 1 lrp\n"; } |../../src/ltkd/ltkc $ltk_id | while read cmd
       +do
       +        printf "%s\n" "$cmd" >&2
       +done | ../../src/ltkd/ltkc $ltk_id
   DIR diff --git a/examples/test.c b/examples/test.c
       t@@ -1,87 +0,0 @@
       -#include <stdio.h>
       -
       -#include "ltk.h"
       -#include "label.h"
       -#include "button.h"
       -#include "image.h"
       -#include "image_widget.h"
       -#include "grid.h"
       -#include "entry.h"
       -#include "menu.h"
       -#include "box.h"
       -
       -int
       -quit(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       -        (void)self;
       -        (void)args;
       -        (void)data;
       -        ltk_quit();
       -        return 1;
       -}
       -
       -int
       -printstuff(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       -        (void)self;
       -        (void)args;
       -        printf("%d\n", LTK_CAST_ARG_INT(data));
       -        return 1;
       -}
       -
       -int
       -main(int argc, char *argv[]) {
       -        (void)argc;
       -        (void)argv;
       -        ltk_init();
       -        ltk_window *window = ltk_window_create("Hi", 0, 0, 500, 500);
       -        ltk_grid *grid = ltk_grid_create(window, 5, 2);
       -        ltk_grid_set_column_weight(grid, 0, 1);
       -        ltk_grid_set_column_weight(grid, 1, 1);
       -        ltk_grid_set_row_weight(grid, 4, 1);
       -        ltk_button *button = ltk_button_create(window, "I'm a button!");
       -        ltk_button *button1 = ltk_button_create(window, "I'm also a button!");
       -        ltk_label *label = ltk_label_create(window, "I'm a label!");
       -        ltk_image *img = ltk_image_create_from_path("test.jpg");
       -        if (!img) {
       -                fprintf(stderr, "Unable to load image.\n");
       -                return 1;
       -        }
       -        ltk_image_widget *iw = ltk_image_widget_create(window, img);
       -        ltk_entry *entry = ltk_entry_create(window, "");
       -        ltk_menu *menu = ltk_menu_create(window);
       -        ltk_menuentry *e1 = ltk_menuentry_create(window, "Hi");
       -        ltk_menuentry *e2 = ltk_menuentry_create(window, "I'm a submenu");
       -        ltk_menu_add_entry(menu, e1);
       -        ltk_menu_add_entry(menu, e2);
       -        ltk_menu *submenu = ltk_submenu_create(window);
       -        ltk_menuentry *e3 = ltk_menuentry_create(window, "Menu Entry 1");
       -        ltk_menuentry *e4 = ltk_menuentry_create(window, "Quit");
       -        ltk_menu_add_entry(submenu, e3);
       -        ltk_menu_add_entry(submenu, e4);
       -        ltk_menuentry_attach_submenu(e2, submenu);
       -
       -        ltk_box *box = ltk_box_create(window, LTK_VERTICAL);
       -        ltk_button *btn1 = ltk_button_create(window, "Bla1");
       -        ltk_button *btn2 = ltk_button_create(window, "Bla2");
       -        ltk_button *btn3 = ltk_button_create(window, "Bla3");
       -        ltk_button *btn4 = ltk_button_create(window, "Bla4");
       -        ltk_button *btn5 = ltk_button_create(window, "Bla5");
       -        ltk_box_add(box, LTK_CAST_WIDGET(btn1), LTK_STICKY_LEFT);
       -        ltk_box_add(box, LTK_CAST_WIDGET(btn2), LTK_STICKY_LEFT);
       -        ltk_box_add(box, LTK_CAST_WIDGET(btn3), LTK_STICKY_LEFT);
       -        ltk_box_add(box, LTK_CAST_WIDGET(btn4), LTK_STICKY_LEFT);
       -        ltk_box_add(box, LTK_CAST_WIDGET(btn5), LTK_STICKY_LEFT);
       -
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(menu), 0, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(button), 1, 0, 1, 1, LTK_STICKY_LEFT);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(button1), 1, 1, 1, 1, LTK_STICKY_RIGHT);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(label), 2, 0, 1, 1, LTK_STICKY_RIGHT);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(iw), 2, 1, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_PRESERVE_ASPECT_RATIO);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(entry), 3, 0, 1, 1, LTK_STICKY_LEFT|LTK_STICKY_RIGHT);
       -        ltk_grid_add(grid, LTK_CAST_WIDGET(box), 4, 0, 1, 2, LTK_STICKY_LEFT|LTK_STICKY_RIGHT|LTK_STICKY_TOP|LTK_STICKY_BOTTOM);
       -        ltk_window_set_root_widget(window, LTK_CAST_WIDGET(grid));
       -        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       -        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(e4), LTK_BUTTON_SIGNAL_PRESSED, &quit, LTK_ARG_VOID);
       -        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(button1), LTK_BUTTON_SIGNAL_PRESSED, &printstuff, LTK_MAKE_ARG_INT(5));
       -        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, &quit, LTK_ARG_VOID);
       -        ltk_mainloop();
       -}
   DIR diff --git a/src/box.c b/src/box.c
       t@@ -1,470 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -/* FIXME: implement other sticky options now supported by grid */
       -
       -#include <limits.h>
       -#include <string.h>
       -
       -#include "box.h"
       -#include "event.h"
       -#include "graphics.h"
       -#include "memory.h"
       -#include "rect.h"
       -#include "scrollbar.h"
       -#include "widget.h"
       -
       -static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
       -static void ltk_box_destroy(ltk_widget *self, int shallow);
       -static void ltk_recalculate_box(ltk_widget *self);
       -static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
       -static int ltk_box_remove_child(ltk_widget *self, ltk_widget *widget);
       -/* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */
       -static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data);
       -static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
       -static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
       -static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
       -
       -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,
       -        .hide = NULL,
       -        .draw = &ltk_box_draw,
       -        .destroy = &ltk_box_destroy,
       -        .resize = &ltk_recalculate_box,
       -        .child_size_change = &ltk_box_child_size_change,
       -        .remove_child = &ltk_box_remove_child,
       -        .key_press = NULL,
       -        .key_release = NULL,
       -        .mouse_press = NULL,
       -        .mouse_scroll = &ltk_box_mouse_scroll,
       -        .mouse_release = NULL,
       -        .motion_notify = NULL,
       -        .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,
       -        .invalid_signal = LTK_BOX_SIGNAL_INVALID,
       -};
       -
       -static void
       -ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        ltk_widget *ptr;
       -        /* FIXME: clip out scrollbar */
       -        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, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
       -        }
       -        box->sc->widget.vtable->draw(
       -            LTK_CAST_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)
       -        );
       -}
       -
       -ltk_box *
       -ltk_box_create(ltk_window *window, ltk_orientation orient) {
       -        ltk_box *box = ltk_malloc(sizeof(ltk_box));
       -        ltk_widget *self = LTK_CAST_WIDGET(box);
       -
       -        ltk_fill_widget_defaults(self, window, &vtable, 0, 0);
       -
       -        box->sc = ltk_scrollbar_create(window, orient);
       -        box->sc->widget.parent = self;
       -        ltk_widget_register_signal_handler(
       -                LTK_CAST_WIDGET(box->sc), LTK_SCROLLBAR_SIGNAL_SCROLL,
       -                &ltk_box_scroll_cb, LTK_MAKE_ARG_WIDGET(self)
       -        );
       -        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(self);
       -
       -        return box;
       -}
       -
       -static void
       -ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
       -        ltk_box *box = LTK_CAST_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(LTK_CAST_WIDGET(box->sc), delta, 0);
       -}
       -
       -static void
       -ltk_box_destroy(ltk_widget *self, int shallow) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        ltk_widget *ptr;
       -        for (size_t i = 0; i < box->num_widgets; i++) {
       -                ptr = box->widgets[i];
       -                ptr->parent = NULL;
       -                if (!shallow)
       -                        ltk_widget_destroy(ptr, shallow);
       -        }
       -        ltk_free(box->widgets);
       -        box->sc->widget.parent = NULL;
       -        ltk_widget_destroy(LTK_CAST_WIDGET(box->sc), 0);
       -        ltk_free(box);
       -}
       -
       -/* FIXME: Make this function name more consistent */
       -/* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
       -   virtual_size is set - this can cause problems when a widget changes its size
       -   (in the scrolled direction) when resized. */
       -/* FIXME: avoid complete recalculation when just scrolling (only position updated) */
       -static void
       -ltk_recalculate_box(ltk_widget *self) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        ltk_widget *ptr;
       -        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->lrect.x = cur_pos - box->sc->cur_pos;
       -                        if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
       -                                ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
       -                        if (ptr->sticky & LTK_STICKY_TOP)
       -                                ptr->lrect.y = 0;
       -                        else if (ptr->sticky & LTK_STICKY_BOTTOM)
       -                                ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
       -                        else
       -                                ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
       -                        cur_pos += ptr->lrect.w;
       -                } else {
       -                        ptr->lrect.y = cur_pos - box->sc->cur_pos;
       -                        if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
       -                                ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
       -                        if (ptr->sticky & LTK_STICKY_LEFT)
       -                                ptr->lrect.x = 0;
       -                        else if (ptr->sticky & LTK_STICKY_RIGHT)
       -                                ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
       -                        else
       -                                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);
       -        if (box->orient == LTK_HORIZONTAL) {
       -                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.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_CAST_WIDGET(box->sc));
       -}
       -
       -/* FIXME: This entire resizing thing is a bit weird. For instance, if a label
       -   in a vertical box increases its height because its width has been decreased
       -   and it is forced to wrap, should that just change the rect or also the
       -   ideal size? Ideal size wouldn't really make sense here, but then the box
       -   might be forced to add a scrollbar even though the parent widget would
       -   actually give it more space if it knew that it needed it. */
       -
       -static void
       -ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        short size_changed = 0;
       -        /* This is always reset here - if it needs to be changed,
       -           the resize function called by the last child_size_change
       -           function will fix it */
       -        /* Note: This seems a bit weird, but if each widget set its rect itself,
       -           that would also lead to weird things. For instance, if a butten is
       -           added to after a box after being ungridded, and its rect was changed
       -           by the grid (e.g. because of a column weight), who should reset the
       -           rect if it doesn't have sticky set? Of course, the resize function
       -           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->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;
       -        } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) {
       -                box->widget.ideal_w = widget->ideal_w + sc_w;
       -                size_changed = 1;
       -        }
       -
       -        if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change)
       -                box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
       -        else
       -                ltk_recalculate_box(LTK_CAST_WIDGET(box));
       -        if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
       -                ltk_widget_resize(widget);
       -}
       -
       -int
       -ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) {
       -        if (widget->parent)
       -                return 1;
       -        if (box->num_widgets >= box->num_alloc) {
       -                size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4;
       -                ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *));
       -                box->num_alloc = new_size;
       -                box->widgets = new;
       -        }
       -
       -        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) {
       -                box->widget.ideal_w += widget->ideal_w;
       -                if (widget->ideal_h + sc_h > box->widget.ideal_h)
       -                        box->widget.ideal_h = widget->ideal_h + sc_h;
       -        } else {
       -                box->widget.ideal_h += widget->ideal_h;
       -                if (widget->ideal_w + sc_w > box->widget.ideal_w)
       -                        box->widget.ideal_w = widget->ideal_w + sc_w;
       -        }
       -        widget->parent = LTK_CAST_WIDGET(box);
       -        widget->sticky = sticky;
       -        ltk_box_child_size_change(LTK_CAST_WIDGET(box), widget);
       -        ltk_window_invalidate_widget_rect(box->widget.window, LTK_CAST_WIDGET(box));
       -
       -        return 0;
       -}
       -
       -int
       -ltk_box_remove_index(ltk_box *box, size_t index) {
       -        if (index >= box->num_widgets)
       -                return 1;
       -        ltk_widget *self = LTK_CAST_WIDGET(box);
       -        ltk_widget *widget = box->widgets[index];
       -        int sc_w = box->sc->widget.lrect.w;
       -        int sc_h = box->sc->widget.lrect.h;
       -        if (index < box->num_widgets - 1)
       -                memmove(box->widgets + index, box->widgets + index + 1,
       -                    (box->num_widgets - index - 1) * sizeof(ltk_widget *));
       -        box->num_widgets--;
       -        ltk_window_invalidate_widget_rect(self->window, self);
       -        /* search for new ideal width/height */
       -        /* FIXME: make this all a bit nicer and break the lines better */
       -        /* FIXME: other part of ideal size not updated */
       -        if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) {
       -                self->ideal_h = 0;
       -                for (size_t j = 0; j < box->num_widgets; j++) {
       -                        if (box->widgets[j]->ideal_h + sc_h > self->ideal_h)
       -                                self->ideal_h = box->widgets[j]->ideal_h + sc_h;
       -                }
       -                if (self->parent)
       -                        ltk_widget_resize(self->parent);
       -        } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) {
       -                self->ideal_w = 0;
       -                for (size_t j = 0; j < box->num_widgets; j++) {
       -                        if (box->widgets[j]->ideal_w + sc_w > self->ideal_w)
       -                                self->ideal_w = box->widgets[j]->ideal_w + sc_w;
       -                }
       -                if (self->parent)
       -                        ltk_widget_resize(self->parent);
       -        }
       -        return 0;
       -}
       -
       -int
       -ltk_box_remove(ltk_box *box, ltk_widget *widget) {
       -        if (widget->parent != LTK_CAST_WIDGET(box))
       -                return 1;
       -        widget->parent = NULL;
       -        for (size_t i = 0; i < box->num_widgets; i++) {
       -                if (box->widgets[i] == widget) {
       -                        return ltk_box_remove_index(box, i);
       -                }
       -        }
       -
       -        return 1;
       -}
       -
       -static int
       -ltk_box_remove_child(ltk_widget *self, ltk_widget *widget) {
       -        return ltk_box_remove(LTK_CAST_BOX(self), widget);
       -}
       -
       -/* 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_CAST_BOX(self);
       -        ltk_widget *minw = NULL;
       -        int min_dist = INT_MAX;
       -        for (size_t i = 0; i < box->num_widgets; i++) {
       -                ltk_rect r = box->widgets[i]->lrect;
       -                int dist = ltk_rect_fakedist(rect, r);
       -                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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_BOX(self);
       -        return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
       -}
       -
       -static int
       -ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       -        (void)self;
       -        (void)args;
       -        ltk_widget *boxw = LTK_CAST_ARG_WIDGET(data);
       -        ltk_recalculate_box(boxw);
       -        ltk_window_invalidate_widget_rect(boxw->window, boxw);
       -        return 1;
       -}
       -
       -static ltk_widget *
       -ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        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]->crect, x, y))
       -                        return box->widgets[i];
       -        }
       -        return NULL;
       -}
       -
       -static int
       -ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
       -        ltk_box *box = LTK_CAST_BOX(self);
       -        if (event->dy) {
       -                /* FIXME: horizontal scrolling, etc. */
       -                /* FIXME: configure scrollstep */
       -                int delta = event->dy * -15;
       -                ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
       -                ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
       -                ltk_window_fake_motion_event(self->window, glob.x, glob.y);
       -                return 1;
       -        }
       -        return 0;
       -}
   DIR diff --git a/src/graphics.h b/src/graphics.h
       t@@ -1,76 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#ifndef LTK_GRAPHICS_H
       -#define LTK_GRAPHICS_H
       -
       -typedef struct ltk_renderdata ltk_renderdata;
       -typedef struct ltk_renderwindow ltk_renderwindow;
       -
       -#include <stddef.h>
       -
       -#include "rect.h"
       -#include "color.h"
       -
       -typedef enum {
       -        LTK_BORDER_NONE = 0,
       -        LTK_BORDER_TOP = 1,
       -        LTK_BORDER_RIGHT = 2,
       -        LTK_BORDER_BOTTOM = 4,
       -        LTK_BORDER_LEFT = 8,
       -        LTK_BORDER_ALL = 0xF
       -} ltk_border_sides;
       -
       -typedef struct ltk_surface ltk_surface;
       -
       -/* FIXME: graphics context */
       -ltk_surface *ltk_surface_create(ltk_renderwindow *window, int w, int h);
       -void ltk_surface_destroy(ltk_surface *s);
       -/* returns 0 if successful, 1 if not resizable */
       -int ltk_surface_resize(ltk_surface *s, int w, int h);
       -/* FIXME: kind of hacky */
       -void ltk_surface_update_size(ltk_surface *s, int w, int h);
       -ltk_surface *ltk_surface_from_window(ltk_renderwindow *window, int w, int h);
       -void ltk_surface_get_size(ltk_surface *s, int *w, int *h);
       -void ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y);
       -void ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width);
       -void ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect);
       -/* FIXME: document properly, especially difference to draw_rect with offsets and line_width */
       -void ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides);
       -void ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides);
       -void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints);
       -void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip);
       -
       -/* TODO */
       -/*
       -void ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width);
       -void ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2);
       -void ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width);
       -void ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r);
       -*/
       -
       -void renderer_set_imspot(ltk_renderwindow *window, int x, int y);
       -ltk_renderdata *renderer_create(void);
       -ltk_renderwindow *renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
       -void renderer_destroy_window(ltk_renderwindow *window);
       -void renderer_destroy(ltk_renderdata *data);
       -void renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg);
       -/* FIXME: this is kind of out of place */
       -void renderer_swap_buffers(ltk_renderwindow *window);
       -/* FIXME: this is just for the socket name in ltkd and is a bit weird */
       -unsigned long renderer_get_window_id(ltk_renderwindow *window);
       -
       -#endif /* LTK_GRAPHICS_H */
   DIR diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
       t@@ -1,600 +0,0 @@
       -/*
       - * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#include <stdio.h>
       -#include <string.h>
       -
       -#include <X11/XKBlib.h>
       -#include <X11/Xlib.h>
       -#include <X11/Xutil.h>
       -#include <X11/extensions/XKB.h>
       -#include <X11/extensions/Xdbe.h>
       -#include <X11/extensions/dbe.h>
       -
       -#include "graphics_xlib.h"
       -#include "color.h"
       -#include "memory.h"
       -#include "rect.h"
       -#include "util.h"
       -
       -struct ltk_surface {
       -        int w, h;
       -        ltk_renderwindow *window;
       -        Drawable d;
       -        #if USE_XFT == 1
       -        XftDraw *xftdraw;
       -        #endif
       -        char resizable;
       -};
       -
       -ltk_surface *
       -ltk_surface_create(ltk_renderwindow *window, int w, int h) {
       -        ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
       -        if (w <= 0)
       -                w = 1;
       -        if (h <= 0)
       -                h = 1;
       -        s->w = w;
       -        s->h = h;
       -        s->window = window;
       -        s->d = XCreatePixmap(window->renderdata->dpy, window->xwindow, w, h, window->renderdata->depth);
       -        #if USE_XFT == 1
       -        s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
       -        #endif
       -        s->resizable = 1;
       -        return s;
       -}
       -
       -ltk_surface *
       -ltk_surface_from_window(ltk_renderwindow *window, int w, int h) {
       -        ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
       -        s->w = w;
       -        s->h = h;
       -        s->window = window;
       -        s->d = window->drawable;
       -        #if USE_XFT == 1
       -        s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
       -        #endif
       -        s->resizable = 0;
       -        return s;
       -}
       -
       -void
       -ltk_surface_destroy(ltk_surface *s) {
       -        #if USE_XFT == 1
       -        XftDrawDestroy(s->xftdraw);
       -        #endif
       -        if (s->resizable)
       -                XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
       -        ltk_free(s);
       -}
       -
       -void
       -ltk_surface_update_size(ltk_surface *s, int w, int h) {
       -        /* FIXME: maybe return directly if surface is resizable? */
       -        s->w = w;
       -        s->h = h;
       -        /* FIXME: sort of hacky (this is only used by window surface) */
       -        #if USE_XFT == 1
       -        XftDrawChange(s->xftdraw, s->d);
       -        #endif
       -}
       -
       -int
       -ltk_surface_resize(ltk_surface *s, int w, int h) {
       -        if (!s->resizable)
       -                return 1;
       -        s->w = w;
       -        s->h = h;
       -        XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
       -        s->d = XCreatePixmap(s->window->renderdata->dpy, s->window->xwindow, w, h, s->window->renderdata->depth);
       -        #if USE_XFT == 1
       -        XftDrawChange(s->xftdraw, s->d);
       -        #endif
       -        return 0;
       -}
       -
       -void
       -ltk_surface_get_size(ltk_surface *s, int *w, int *h) {
       -        *w = s->w;
       -        *h = s->h;
       -}
       -
       -void
       -ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width) {
       -        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -        XSetLineAttributes(s->window->renderdata->dpy, s->window->gc, line_width, LineSolid, CapButt, JoinMiter);
       -        XDrawRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
       -}
       -
       -void
       -ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides) {
       -        /* drawn as rectangles to have proper control over line width - I'm not sure how exactly
       -           XDrawLine handles even line widths (i.e. on which side the extra pixel will be) */
       -        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -        if (border_sides & LTK_BORDER_TOP)
       -                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
       -        if (border_sides & LTK_BORDER_BOTTOM)
       -                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y + rect.h - line_width, rect.w, line_width);
       -        if (border_sides & LTK_BORDER_LEFT)
       -                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
       -        if (border_sides & LTK_BORDER_RIGHT)
       -                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
       -}
       -
       -void
       -ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides) {
       -        if (line_width <= 0)
       -                return;
       -        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -        int width;
       -        ltk_rect final_rect = ltk_rect_intersect(rect, clip_rect);
       -        if (border_sides & LTK_BORDER_TOP) {
       -                width = rect.y - final_rect.y;
       -                if (width > -line_width) {
       -                        width = line_width + width;
       -                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, final_rect.w, width);
       -                }
       -        }
       -        if (border_sides & LTK_BORDER_BOTTOM) {
       -                width = (final_rect.y + final_rect.h) - (rect.y + rect.h);
       -                if (width > -line_width) {
       -                        width = line_width + width;
       -                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y + final_rect.h - width, final_rect.w, width);
       -                }
       -        }
       -        if (border_sides & LTK_BORDER_LEFT) {
       -                width = rect.x - final_rect.x;
       -                if (width > -line_width) {
       -                        width = line_width + width;
       -                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, width, final_rect.h);
       -                }
       -        }
       -        if (border_sides & LTK_BORDER_RIGHT) {
       -                width = (final_rect.x + final_rect.w) - (rect.x + rect.w);
       -                if (width > -line_width) {
       -                        width = line_width + width;
       -                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x + final_rect.w - width, final_rect.y, width, final_rect.h);
       -                }
       -        }
       -}
       -
       -void
       -ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
       -        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
       -}
       -
       -void
       -ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
       -        /* FIXME: maybe make this static since this won't be threaded anyways? */
       -        XPoint tmp_points[6]; /* to avoid extra allocations when not necessary */
       -        /* FIXME: this is ugly and inefficient */
       -        XPoint *final_points;
       -        if (npoints <= 6) {
       -                final_points = tmp_points;
       -        } else {
       -                final_points = ltk_reallocarray(NULL, npoints, sizeof(XPoint));
       -        }
       -        /* FIXME: how to deal with ints that don't fit in short? */
       -        for (size_t i = 0; i < npoints; i++) {
       -                final_points[i].x = (short)points[i].x;
       -                final_points[i].y = (short)points[i].y;
       -        }
       -        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -        XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
       -        if (npoints > 6)
       -                ltk_free(final_points);
       -}
       -
       -static inline void
       -swap_ptr(void **ptr1, void **ptr2) {
       -        void *tmp = *ptr1;
       -        *ptr1 = *ptr2;
       -        *ptr2 = tmp;
       -}
       -
       -#define check_size(cond) if (!(cond)) ltk_fatal("Unable to perform polygon clipping. This is a bug, tell lumidify about it.\n")
       -
       -/* FIXME: xlib already includes clipping... */
       -/* FIXME: this can probably be optimized */
       -/* This is basically Sutherland-Hodgman, but only the special case for clipping rectangles. */
       -void
       -ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
       -        /* FIXME: is this even more efficient? */
       -        XPoint tmp_points1[12]; /* to avoid extra allocations when not necessary */
       -        XPoint tmp_points2[12];
       -        XPoint *points1;
       -        XPoint *points2;
       -        /* FIXME: be a bit smarter about this */
       -        if (npoints <= 6) {
       -                points1 = tmp_points1;
       -                points2 = tmp_points2;
       -        } else {
       -                /* FIXME: I'm pretty sure there can never be more points than this
       -                   since we're only clipping against a rectangle, right?
       -                   If I can be sure about that, I can remove all the check_size's below. */
       -                points1 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
       -                points2 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
       -        }
       -
       -        size_t num1 = npoints;
       -        size_t num2 = 0;
       -        for (size_t i = 0; i < npoints; i++) {
       -                points1[i].x = (short)points[i].x;
       -                points1[i].y = (short)points[i].y;
       -        }
       -
       -        for (size_t i = 0; i < num1; i++) {
       -                XPoint p1 = points1[i];
       -                XPoint p2 = points1[(i + 1) % num1];
       -                if (p1.x >= clip.x) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = p1;
       -                        if (p2.x < clip.x) {
       -                                check_size(num2 < npoints * 2);
       -                                points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
       -                        }
       -                } else if (p2.x >= clip.x) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
       -                }
       -        }
       -        num1 = num2;
       -        num2 = 0;
       -        swap_ptr((void**)&points1, (void**)&points2);
       -
       -        for (size_t i = 0; i < num1; i++) {
       -                XPoint p1 = points1[i];
       -                XPoint p2 = points1[(i + 1) % num1];
       -                if (p1.x <= clip.x + clip.w) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = p1;
       -                        if (p2.x > clip.x + clip.w) {
       -                                check_size(num2 < npoints * 2);
       -                                points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
       -                        }
       -                } else if (p2.x <= clip.x + clip.w) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
       -                }
       -        }
       -        num1 = num2;
       -        num2 = 0;
       -        swap_ptr((void**)&points1, (void**)&points2);
       -
       -        for (size_t i = 0; i < num1; i++) {
       -                XPoint p1 = points1[i];
       -                XPoint p2 = points1[(i + 1) % num1];
       -                if (p1.y >= clip.y) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = p1;
       -                        if (p2.y < clip.y) {
       -                                check_size(num2 < npoints * 2);
       -                                points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
       -                        }
       -                } else if (p2.y >= clip.y) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
       -                }
       -        }
       -        num1 = num2;
       -        num2 = 0;
       -        swap_ptr((void**)&points1, (void**)&points2);
       -
       -        for (size_t i = 0; i < num1; i++) {
       -                XPoint p1 = points1[i];
       -                XPoint p2 = points1[(i + 1) % num1];
       -                if (p1.y <= clip.y + clip.h) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = p1;
       -                        if (p2.y > clip.y + clip.h) {
       -                                check_size(num2 < npoints * 2);
       -                                points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
       -                        }
       -                } else if (p2.y <= clip.y + clip.h) {
       -                        check_size(num2 < npoints * 2);
       -                        points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
       -                }
       -        }
       -
       -        if (num2 > 0) {
       -                XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       -                XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, points2, (int)num2, Complex, CoordModeOrigin);
       -        }
       -        if (npoints > 6) {
       -                ltk_free(points1);
       -                ltk_free(points2);
       -        }
       -}
       -
       -void
       -ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y) {
       -        XCopyArea(
       -            src->window->renderdata->dpy, src->d, dst->d, src->window->gc,
       -            src_rect.x, src_rect.y, src_rect.w, src_rect.h, dst_x, dst_y
       -        );
       -}
       -
       -/* TODO */
       -/*
       -void
       -ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width) {
       -}
       -
       -void
       -ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2) {
       -}
       -
       -void
       -ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width) {
       -}
       -
       -void
       -ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r) {
       -}
       -*/
       -
       -#if USE_XFT == 1
       -XftDraw *
       -ltk_surface_get_xft_draw(ltk_surface *s) {
       -        return s->xftdraw;
       -}
       -#endif
       -
       -Drawable
       -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_renderwindow *window);
       -
       -static void
       -ximdestroy(XIM xim, XPointer client, XPointer call) {
       -        (void)xim;
       -        (void)call;
       -        ltk_renderwindow *window = (ltk_renderwindow *)client;
       -        window->xim = NULL;
       -        XRegisterIMInstantiateCallback(
       -            window->renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
       -        );
       -        XFree(window->spotlist);
       -}
       -
       -static int
       -xicdestroy(XIC xim, XPointer client, XPointer call) {
       -        (void)xim;
       -        (void)call;
       -        ltk_renderwindow *window = (ltk_renderwindow *)client;
       -        window->xic = NULL;
       -        return 1;
       -}
       -
       -static int
       -ximopen(ltk_renderwindow *window) {
       -        XIMCallback imdestroy = { .client_data = (XPointer)window, .callback = ximdestroy };
       -        XICCallback icdestroy = { .client_data = (XPointer)window, .callback = xicdestroy };
       -
       -        window->xim = XOpenIM(window->renderdata->dpy, NULL, NULL, NULL);
       -        if (window->xim == NULL)
       -                return 0;
       -
       -        if (XSetIMValues(window->xim, XNDestroyCallback, &imdestroy, NULL))
       -                ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
       -
       -        window->spotlist = XVaCreateNestedList(0, XNSpotLocation, &window->spot, NULL);
       -
       -        if (window->xic == NULL) {
       -                window->xic = XCreateIC(
       -                    window->xim, XNInputStyle,
       -                    XIMPreeditNothing | XIMStatusNothing,
       -                    XNClientWindow, window->xwindow,
       -                    XNDestroyCallback, &icdestroy, NULL
       -                );
       -        }
       -        if (window->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_renderwindow *window = (ltk_renderwindow *)client;
       -        if (ximopen(window)) {
       -                XUnregisterIMInstantiateCallback(
       -                    dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
       -                );
       -        }
       -}
       -
       -void
       -renderer_set_imspot(ltk_renderwindow *window, int x, int y) {
       -        if (window->xic == NULL)
       -                return;
       -        window->spot.x = x;
       -        window->spot.y = y;
       -        XSetICValues(window->xic, XNPreeditAttributes, window->spotlist, NULL);
       -}
       -
       -ltk_renderdata *
       -renderer_create(void) {
       -        /* FIXME: this might not be the best place for this */
       -        XSetLocaleModifiers("");
       -        ltk_renderdata *renderdata = ltk_malloc(sizeof(ltk_renderdata));
       -        renderdata->dpy = XOpenDisplay(NULL);
       -        renderdata->screen = DefaultScreen(renderdata->dpy);
       -        renderdata->db_enabled = 0;
       -        /* based on http://wili.cc/blog/xdbe.html */
       -        int major, minor;
       -        if (XdbeQueryExtension(renderdata->dpy, &major, &minor)) {
       -                int num_screens = 1;
       -                Drawable screens[] = {DefaultRootWindow(renderdata->dpy)};
       -                XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
       -                    renderdata->dpy, screens, &num_screens
       -                );
       -                if (!info || num_screens < 1 || info->count < 1) {
       -                        ltk_fatal("No visuals support Xdbe.");
       -                }
       -                XVisualInfo xvisinfo_templ;
       -                /* we know there's at least one */
       -                xvisinfo_templ.visualid = info->visinfo[0].visual;
       -                /* FIXME: proper screen number? */
       -                xvisinfo_templ.screen = 0;
       -                xvisinfo_templ.depth = info->visinfo[0].depth;
       -                int matches;
       -                XVisualInfo *xvisinfo_match = XGetVisualInfo(
       -                    renderdata->dpy,
       -                    VisualIDMask | VisualScreenMask | VisualDepthMask,
       -                    &xvisinfo_templ, &matches
       -                );
       -                if (!xvisinfo_match || matches < 1) {
       -                        ltk_fatal("Couldn't match a Visual with double buffering.\n");
       -                }
       -                renderdata->vis = xvisinfo_match->visual;
       -                /* FIXME: is it legal to free this while keeping the visual? */
       -                XFree(xvisinfo_match);
       -                XdbeFreeVisualInfo(info);
       -                renderdata->db_enabled = 1;
       -        } else {
       -                renderdata->vis = DefaultVisual(renderdata->dpy, renderdata->screen);
       -                ltk_warn("No Xdbe support.\n");
       -        }
       -        renderdata->cm = DefaultColormap(renderdata->dpy, renderdata->screen);
       -        renderdata->wm_delete_msg = XInternAtom(renderdata->dpy, "WM_DELETE_WINDOW", False);
       -        renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen);
       -        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;
       -}
       -
       -ltk_renderwindow *
       -renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
       -        XSetWindowAttributes attrs;
       -        ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow));
       -        window->renderdata = data;
       -        memset(&attrs, 0, sizeof(attrs));
       -        attrs.background_pixel = BlackPixel(data->dpy, data->screen);
       -        attrs.colormap = data->cm;
       -        attrs.border_pixel = WhitePixel(data->dpy, data->screen);
       -        /* this causes the window contents to be kept
       -         * when it is resized, leading to less flicker */
       -        attrs.bit_gravity = NorthWestGravity;
       -        attrs.event_mask =
       -                ExposureMask | KeyPressMask | KeyReleaseMask |
       -                ButtonPressMask | ButtonReleaseMask |
       -                StructureNotifyMask | PointerMotionMask;
       -        /* FIXME: set border width */
       -        window->xwindow = XCreateWindow(
       -            data->dpy, DefaultRootWindow(data->dpy), x, y,
       -            w, h, 0, data->depth,
       -            InputOutput, data->vis,
       -            CWBackPixel | CWColormap | CWBitGravity | CWEventMask | CWBorderPixel, &attrs
       -        );
       -
       -        if (data->db_enabled) {
       -                window->back_buf = XdbeAllocateBackBufferName(
       -                        data->dpy, window->xwindow, XdbeBackground
       -                );
       -        } else {
       -                window->back_buf = window->xwindow;
       -        }
       -        window->drawable = window->back_buf;
       -        window->gc = XCreateGC(data->dpy, window->xwindow, 0, 0);
       -        XSetStandardProperties(
       -                data->dpy, window->xwindow,
       -                title, NULL, None, NULL, 0, NULL
       -        );
       -        /* FIXME: check return value */
       -        XSetWMProtocols(data->dpy, window->xwindow, &data->wm_delete_msg, 1);
       -
       -        window->xic = NULL;
       -        window->xim = NULL;
       -        if (!ximopen(window)) {
       -                XRegisterIMInstantiateCallback(
       -                    window->renderdata->dpy, NULL, NULL, NULL,
       -                    ximinstantiate, (XPointer)window
       -                );
       -        }
       -
       -        XClearWindow(window->renderdata->dpy, window->xwindow);
       -        XMapRaised(window->renderdata->dpy, window->xwindow);
       -
       -        return window;
       -}
       -
       -void
       -renderer_destroy_window(ltk_renderwindow *window) {
       -        XFreeGC(window->renderdata->dpy, window->gc);
       -        if (window->spotlist)
       -                XFree(window->spotlist);
       -        /* FIXME: destroy xim/xic? */
       -        XDestroyWindow(window->renderdata->dpy, window->xwindow);
       -        ltk_free(window);
       -}
       -
       -void
       -renderer_destroy(ltk_renderdata *renderdata) {
       -        XCloseDisplay(renderdata->dpy);
       -        /* FIXME: destroy visual, wm_delete_msg, etc.? */
       -        ltk_free(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_renderwindow *window, ltk_color *bg) {
       -        XSetWindowBackground(window->renderdata->dpy, window->xwindow, bg->xcolor.pixel);
       -}
       -
       -void
       -renderer_swap_buffers(ltk_renderwindow *window) {
       -        XdbeSwapInfo swap_info;
       -        swap_info.swap_window = window->xwindow;
       -        swap_info.swap_action = XdbeBackground;
       -        if (!XdbeSwapBuffers(window->renderdata->dpy, &swap_info, 1))
       -                ltk_fatal("Unable to swap buffers.\n");
       -        XFlush(window->renderdata->dpy);
       -}
       -
       -unsigned long
       -renderer_get_window_id(ltk_renderwindow *window) {
       -        return (unsigned long)window->xwindow;
       -}
   DIR diff --git a/src/grid.c b/src/grid.c
       t@@ -1,546 +0,0 @@
       -/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
       -
       -/*
       - * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -/* TODO: make ungrid function also adjust static row/column width/height
       -   -> also, how should the grid deal with a widget spanning over multiple
       -      rows/columns with static size - if all are static, it could just
       -      divide the widget size (it would complicate things, though), but
       -      what should happen if some rows/columns under the span do have a
       -      positive weight? */
       -
       -#include <stddef.h>
       -#include <limits.h>
       -
       -#include "memory.h"
       -#include "rect.h"
       -#include "widget.h"
       -#include "util.h"
       -#include "grid.h"
       -#include "graphics.h"
       -
       -void ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight);
       -void ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight);
       -ltk_grid *ltk_grid_create(ltk_window *window, int rows, int columns);
       -int ltk_grid_add(
       -        ltk_grid *grid, ltk_widget *widget,
       -        int row, int column, int row_span, int column_span,
       -        ltk_sticky_mask sticky
       -);
       -/* just a wrapper around ltk_grid_remove to make types match */
       -static int ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget);
       -int ltk_grid_remove(ltk_grid *grid, ltk_widget *widget);
       -
       -static void ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
       -static void ltk_grid_destroy(ltk_widget *self, int shallow);
       -static void ltk_recalculate_grid(ltk_widget *self);
       -static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
       -static int ltk_grid_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,
       -        .resize = &ltk_recalculate_grid,
       -        .hide = NULL,
       -        .change_state = NULL,
       -        .child_size_change = &ltk_grid_child_size_change,
       -        .remove_child = &ltk_grid_remove_child,
       -        .mouse_press = NULL,
       -        .mouse_scroll = NULL,
       -        .mouse_release = NULL,
       -        .motion_notify = NULL,
       -        .get_child_at_pos = &ltk_grid_get_child_at_pos,
       -        .mouse_leave = NULL,
       -        .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,
       -        .invalid_signal = LTK_GRID_SIGNAL_INVALID,
       -};
       -
       -/* FIXME: only set "dirty" bit to avoid constand recalculation when
       -   setting multiple row/column weights? */
       -void
       -ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight) {
       -        ltk_assert(row < grid->rows);
       -        grid->row_weights[row] = weight;
       -        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       -}
       -
       -void
       -ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight) {
       -        ltk_assert(column < grid->columns);
       -        grid->column_weights[column] = weight;
       -        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       -}
       -
       -static void
       -ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
       -        ltk_grid *grid = LTK_CAST_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];
       -                int max_w = grid->column_pos[ptr->column + ptr->column_span] - grid->column_pos[ptr->column];
       -                int max_h = grid->row_pos[ptr->row + ptr->row_span] - grid->row_pos[ptr->row];
       -                ltk_rect r = ltk_rect_intersect(
       -                    (ltk_rect){grid->column_pos[ptr->column], grid->row_pos[ptr->row], max_w, max_h}, real_clip
       -                );
       -                if (ptr->vtable->draw)
       -                        ptr->vtable->draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, r));
       -        }
       -}
       -
       -ltk_grid *
       -ltk_grid_create(ltk_window *window, int rows, int columns) {
       -        ltk_grid *grid = ltk_malloc(sizeof(ltk_grid));
       -
       -        ltk_fill_widget_defaults(LTK_CAST_WIDGET(grid), window, &vtable, 0, 0);
       -
       -        grid->rows = rows;
       -        grid->columns = columns;
       -        grid->widget_grid = ltk_malloc(rows * columns * sizeof(ltk_widget));
       -        grid->row_heights = ltk_malloc(rows * sizeof(int));
       -        grid->column_widths = ltk_malloc(rows * sizeof(int));
       -        grid->row_weights = ltk_malloc(rows * sizeof(int));
       -        grid->column_weights = ltk_malloc(columns * sizeof(int));
       -        /* Positions have one extra for the end */
       -        grid->row_pos = ltk_malloc((rows + 1) * sizeof(int));
       -        grid->column_pos = ltk_malloc((columns + 1) * sizeof(int));
       -        /* FIXME: wow, that's horrible, this should just use memset */
       -        int i;
       -        for (i = 0; i < rows; i++) {
       -                grid->row_heights[i] = 0;
       -                grid->row_weights[i] = 0;
       -                grid->row_pos[i] = 0;
       -        }
       -        grid->row_pos[rows] = 0;
       -        for (i = 0; i < columns; i++) {
       -                grid->column_widths[i] = 0;
       -                grid->column_weights[i] = 0;
       -                grid->column_pos[i] = 0;
       -        }
       -        grid->column_pos[columns] = 0;
       -        for (i = 0; i < rows * columns; i++) {
       -                grid->widget_grid[i] = NULL;
       -        }
       -
       -        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       -        return grid;
       -}
       -
       -static void
       -ltk_grid_destroy(ltk_widget *self, int shallow) {
       -        ltk_grid *grid = LTK_CAST_GRID(self);
       -        ltk_widget *ptr;
       -        for (int i = 0; i < grid->rows * grid->columns; i++) {
       -                if (grid->widget_grid[i]) {
       -                        ptr = grid->widget_grid[i];
       -                        ptr->parent = NULL;
       -                        if (!shallow) {
       -                                /* required to avoid freeing a widget multiple times
       -                                   if row_span or column_span is not 1 */
       -                                for (int r = ptr->row; r < ptr->row + ptr->row_span; r++) {
       -                                        for (int c = ptr->column; c < ptr->column + ptr->column_span; c++) {
       -                                                grid->widget_grid[r * grid->columns + c] = NULL;
       -                                        }
       -                                }
       -                                ltk_widget_destroy(ptr, shallow);
       -                        }
       -                }
       -        }
       -        ltk_free(grid->widget_grid);
       -        ltk_free(grid->row_heights);
       -        ltk_free(grid->column_widths);
       -        ltk_free(grid->row_weights);
       -        ltk_free(grid->column_weights);
       -        ltk_free(grid->row_pos);
       -        ltk_free(grid->column_pos);
       -        ltk_free(grid);
       -}
       -
       -static void
       -ltk_recalculate_grid(ltk_widget *self) {
       -        ltk_grid *grid = LTK_CAST_GRID(self);
       -        unsigned int height_static = 0, width_static = 0;
       -        unsigned int total_row_weight = 0, total_column_weight = 0;
       -        float height_unit = 0, width_unit = 0;
       -        unsigned int currentx = 0, currenty = 0;
       -        int i, j;
       -        for (i = 0; i < grid->rows; i++) {
       -                total_row_weight += grid->row_weights[i];
       -                if (grid->row_weights[i] == 0) {
       -                        height_static += grid->row_heights[i];
       -                }
       -        }
       -        for (i = 0; i < grid->columns; i++) {
       -                total_column_weight += grid->column_weights[i];
       -                if (grid->column_weights[i] == 0) {
       -                        width_static += grid->column_widths[i];
       -                }
       -        }
       -        /* FIXME: what should be done when static height or width is larger than grid? */
       -        if (total_row_weight > 0) {
       -                height_unit = (float) (self->lrect.h - height_static) / (float) total_row_weight;
       -        }
       -        if (total_column_weight > 0) {
       -                width_unit = (float) (self->lrect.w - width_static) / (float) total_column_weight;
       -        }
       -        for (i = 0; i < grid->rows; i++) {
       -                grid->row_pos[i] = currenty;
       -                if (grid->row_weights[i] > 0) {
       -                        grid->row_heights[i] = grid->row_weights[i] * height_unit;
       -                }
       -                currenty += grid->row_heights[i];
       -        }
       -        grid->row_pos[grid->rows] = currenty;
       -        for (i = 0; i < grid->columns; i++) {
       -                grid->column_pos[i] = currentx;
       -                if (grid->column_weights[i] > 0) {
       -                        grid->column_widths[i] = grid->column_weights[i] * width_unit;
       -                }
       -                currentx += grid->column_widths[i];
       -        }
       -        grid->column_pos[grid->columns] = currentx;
       -        /*int orig_width, orig_height;*/
       -        int end_column, end_row;
       -        for (i = 0; i < grid->rows; i++) {
       -                for (j = 0; j < grid->columns; j++) {
       -                        ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
       -                        if (!ptr || ptr->row != i || ptr->column != j)
       -                                continue;
       -                        /*orig_width = ptr->lrect.w;
       -                        orig_height = ptr->lrect.h;*/
       -                        ptr->lrect.w = ptr->ideal_w;
       -                        ptr->lrect.h = ptr->ideal_h;
       -                        end_row = i + ptr->row_span;
       -                        end_column = j + ptr->column_span;
       -                        int max_w = grid->column_pos[end_column] - grid->column_pos[j];
       -                        int max_h = grid->row_pos[end_row] - grid->row_pos[i];
       -                        int stretch_width = (ptr->sticky & LTK_STICKY_LEFT) && (ptr->sticky & LTK_STICKY_RIGHT);
       -                        int shrink_width = (ptr->sticky & LTK_STICKY_SHRINK_WIDTH) && ptr->lrect.w > max_w;
       -                        int stretch_height = (ptr->sticky & LTK_STICKY_TOP) && (ptr->sticky & LTK_STICKY_BOTTOM);
       -                        int shrink_height = (ptr->sticky & LTK_STICKY_SHRINK_HEIGHT) && ptr->lrect.h > max_h;
       -                        if (stretch_width || shrink_width)
       -                                ptr->lrect.w = max_w;
       -                        if (stretch_height || shrink_height)
       -                                ptr->lrect.h = max_h;
       -                        if (ptr->sticky & LTK_STICKY_PRESERVE_ASPECT_RATIO) {
       -                                if (!stretch_width && !shrink_width) {
       -                                        ptr->lrect.w = (int)(((double)ptr->lrect.h / ptr->ideal_h) * ptr->ideal_w);
       -                                } else if (!stretch_height && !shrink_height) {
       -                                        ptr->lrect.h = (int)(((double)ptr->lrect.w / ptr->ideal_w) * ptr->ideal_h);
       -                                } else {
       -                                        double scale_w = (double)ptr->lrect.w / ptr->ideal_w;
       -                                        double scale_h = (double)ptr->lrect.h / ptr->ideal_h;
       -                                        if (scale_w * ptr->ideal_h > ptr->lrect.h)
       -                                                ptr->lrect.w = (int)(scale_h * ptr->ideal_w);
       -                                        else if (scale_h * ptr->ideal_w > ptr->lrect.w)
       -                                                ptr->lrect.h = (int)(scale_w * ptr->ideal_h);
       -                                }
       -                        }
       -                        /* FIXME: Figure out a better system for this - it would be nice to make it more
       -                           efficient by not doing anything if nothing changed, but that doesn't work when
       -                           this function was called because of a child_size_change. In that case, if a
       -                           container widget is nested inside another container widget and another widget
       -                           inside the nested container sends a child_size_change but the toplevel container
       -                           doesn't change the size of the container, the position/size of the widget at the
       -                           bottom of the hierarchy will never be updated. That's why updates are forced
       -                           here even if seemingly nothing changed, but there probably is a better way. */
       -                        /*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
       -                                ltk_widget_resize(ptr);
       -
       -                        /* the "default" case needs to come first because the widget may be stretched
       -                           with aspect ratio preserving, and in that case it should still be centered */
       -                        if (stretch_width || !(ptr->sticky & (LTK_STICKY_RIGHT|LTK_STICKY_LEFT))) {
       -                                ptr->lrect.x = grid->column_pos[j] + (grid->column_pos[end_column] - grid->column_pos[j] - ptr->lrect.w) / 2;
       -                        } else if (ptr->sticky & LTK_STICKY_RIGHT) {
       -                                ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
       -                        } else if (ptr->sticky & LTK_STICKY_LEFT) {
       -                                ptr->lrect.x = grid->column_pos[j];
       -                        }
       -
       -                        if (stretch_height || !(ptr->sticky & (LTK_STICKY_TOP|LTK_STICKY_BOTTOM))) {
       -                                ptr->lrect.y = grid->row_pos[i] + (grid->row_pos[end_row] - grid->row_pos[i] - ptr->lrect.h) / 2;
       -                        } else if (ptr->sticky & LTK_STICKY_BOTTOM) {
       -                                ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
       -                        } else if (ptr->sticky & LTK_STICKY_TOP) {
       -                                ptr->lrect.y = grid->row_pos[i];
       -                        }
       -                        /* intersect both with the grid rect and with the rect of the covered cells since there may be
       -                           weird cases where the layout doesn't work properly and the cells are partially outside the grid */
       -                        ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
       -                        ptr->crect = ltk_rect_intersect((ltk_rect){grid->column_pos[j], grid->row_pos[i], max_w, max_h}, ptr->crect);
       -                }
       -        }
       -}
       -
       -/* FIXME: Maybe add debug stuff to check that grid is actually parent of widget */
       -static void
       -ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
       -        ltk_grid *grid = LTK_CAST_GRID(self);
       -        short size_changed = 0;
       -        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->lrect.w > grid->column_widths[widget->column]) {
       -                self->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->lrect.h > grid->row_heights[widget->row]) {
       -                self->ideal_h += widget->lrect.h - grid->row_heights[widget->row];
       -                grid->row_heights[widget->row] = widget->lrect.h;
       -                size_changed = 1;
       -        }
       -        if (size_changed && self->parent && self->parent->vtable->child_size_change)
       -                self->parent->vtable->child_size_change(self->parent, LTK_CAST_WIDGET(grid));
       -        else
       -                ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       -        if (widget->lrect.w != orig_w || widget->lrect.h != orig_h)
       -                ltk_widget_resize(widget);
       -}
       -
       -/* FIXME: Check if widget already exists at position */
       -int
       -ltk_grid_add(
       -        ltk_grid *grid, ltk_widget *widget,
       -        int row, int column, int row_span, int column_span,
       -        ltk_sticky_mask sticky
       -) {
       -        if (widget->parent)
       -                return 1;
       -        /* FIXME: decide which checks should be asserts and which should be error returns */
       -        /* the client-server version of ltk shouldn't abort on errors like these */
       -        ltk_assert(row >= 0 && row + row_span <= grid->rows);
       -        ltk_assert(column >= 0 && column + column_span <= grid->columns);
       -
       -        widget->sticky = sticky;
       -        widget->row = row;
       -        widget->column = column;
       -        widget->row_span = row_span;
       -        widget->column_span = column_span;
       -        for (int i = row; i < row + row_span; i++) {
       -                for (int j = column; j < column + column_span; j++) {
       -                        grid->widget_grid[i * grid->columns + j] = widget;
       -                }
       -        }
       -        widget->parent = LTK_CAST_WIDGET(grid);
       -        ltk_grid_child_size_change(LTK_CAST_WIDGET(grid), widget);
       -        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
       -
       -        return 0;
       -}
       -
       -int
       -ltk_grid_remove(ltk_grid *grid, ltk_widget *widget) {
       -        if (widget->parent != LTK_CAST_WIDGET(grid))
       -                return 1;
       -        widget->parent = NULL;
       -        for (int i = widget->row; i < widget->row + widget->row_span; i++) {
       -                for (int j = widget->column; j < widget->column + widget->column_span; j++) {
       -                        grid->widget_grid[i * grid->columns + j] = NULL;
       -                }
       -        }
       -        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
       -
       -        return 0;
       -}
       -
       -static int
       -ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget) {
       -        return ltk_grid_remove(LTK_CAST_GRID(self), widget);
       -}
       -
       -static int
       -ltk_grid_find_nearest_column(ltk_grid *grid, int x) {
       -        int i;
       -        for (i = 0; i < grid->columns; i++) {
       -                if (grid->column_pos[i] <= x && grid->column_pos[i + 1] >= x) {
       -                        return i;
       -                }
       -        }
       -        return -1;
       -}
       -
       -static int
       -ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
       -        int i;
       -        for (i = 0; i < grid->rows; i++) {
       -                if (grid->row_pos[i] <= y && grid->row_pos[i + 1] >= y) {
       -                        return i;
       -                }
       -        }
       -        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_CAST_GRID(self);
       -        ltk_widget *minw = NULL;
       -        int min_dist = INT_MAX;
       -        /* 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 */
       -                ltk_rect r = grid->widget_grid[i]->lrect;
       -                int dist = ltk_rect_fakedist(rect, r);
       -                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_CAST_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_CAST_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;
       -}
       -
       -/* FIXME: maybe these should also fall back to widgets in other columns if those
       -   exist but no widgets exist in the same column */
       -static ltk_widget *
       -ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
       -        ltk_grid *grid = LTK_CAST_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_CAST_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_CAST_GRID(self);
       -        int row = ltk_grid_find_nearest_row(grid, y);
       -        int column = ltk_grid_find_nearest_column(grid, x);
       -        if (row == -1 || column == -1)
       -                return 0;
       -        ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
       -        if (ptr && ltk_collide_rect(ptr->crect, x, y))
       -                return ptr;
       -        return NULL;
       -}
       -
       -static ltk_widget *
       -ltk_grid_prev_child(ltk_widget *self, ltk_widget *child) {
       -        ltk_grid *grid = LTK_CAST_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_CAST_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_CAST_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_CAST_GRID(self);
       -        for (unsigned int i = grid->rows * grid->columns; i-- > 0;) {
       -                if (grid->widget_grid[i])
       -                        return grid->widget_grid[i];
       -        }
       -        return NULL;
       -}
   DIR diff --git a/src/ltk.c b/src/ltk.c
       t@@ -1,672 +0,0 @@
       -/*
       - * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#include <locale.h>
       -#include <pwd.h>
       -#include <stdint.h>
       -#include <stdlib.h>
       -#include <string.h>
       -#include <time.h>
       -#include <unistd.h>
       -
       -#include <sys/wait.h>
       -
       -#include "ltk.h"
       -#include "array.h"
       -#include "button.h"
       -#include "config.h"
       -#include "entry.h"
       -#include "event.h"
       -#include "eventdefs.h"
       -#include "graphics.h"
       -#include "image.h"
       -#include "ini.h"
       -#include "label.h"
       -#include "macros.h"
       -#include "memory.h"
       -#include "menu.h"
       -#include "rect.h"
       -#include "scrollbar.h"
       -#include "text.h"
       -#include "util.h"
       -#include "widget.h"
       -
       -#define MAX_WINDOW_FONT_SIZE 200
       -
       -typedef struct {
       -        char *tmpfile;
       -        ltk_widget *caller;
       -        int pid;
       -} ltk_cmdinfo;
       -
       -LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
       -LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
       -LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
       -LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
       -LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo)
       -LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo)
       -
       -static struct {
       -        ltk_renderdata *renderdata;
       -        ltk_text_context *text_context;
       -        ltk_clipboard *clipboard;
       -        ltk_array(window) *windows;
       -        ltk_array(rwindow) *rwindows;
       -        /* PID of external command called e.g. by text widget to edit text.
       -           ON exit, cmd_caller->vtable->cmd_return is called with the text
       -           the external command wrote to a file. */
       -        /*IMPORTANT: this needs to be checked whenever a widget is destroyed!
       -        FIXME: allow option to instead return output of command */
       -        ltk_array(cmd) *cmds;
       -        size_t cur_kbd;
       -} shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0};
       -
       -typedef struct {
       -        void (*callback)(ltk_callback_arg data);
       -        ltk_callback_arg data;
       -        struct timespec repeat;
       -        struct timespec remaining;
       -        int id;
       -} ltk_timer;
       -
       -static ltk_timer *timers = NULL;
       -static size_t timers_num = 0;
       -static size_t timers_alloc = 0;
       -
       -static void ltk_handle_event(ltk_event *event);
       -static void ltk_load_theme(const char *path);
       -static void ltk_uninitialize_theme(void);
       -static int ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value);
       -static int handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b);
       -static int handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b);
       -
       -static short running = 1;
       -
       -typedef struct {
       -        char *name;
       -        int (*ini_handler)(ltk_renderdata *, const char *, const char *);
       -        int (*fill_theme_defaults)(ltk_renderdata *);
       -        void (*uninitialize_theme)(ltk_renderdata *);
       -        int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
       -        int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
       -        void (*cleanup)(void);
       -} ltk_widget_funcs;
       -
       -/* FIXME: use binary search when searching for the widget */
       -ltk_widget_funcs widget_funcs[] = {
       -        {
       -                .name = "box",
       -                .ini_handler = NULL,
       -                .fill_theme_defaults = NULL,
       -                .uninitialize_theme = NULL,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "button",
       -                .ini_handler = &ltk_button_ini_handler,
       -                .fill_theme_defaults = &ltk_button_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_button_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "entry",
       -                .ini_handler = &ltk_entry_ini_handler,
       -                .fill_theme_defaults = &ltk_entry_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_entry_uninitialize_theme,
       -                .register_keypress = &ltk_entry_register_keypress,
       -                .register_keyrelease = &ltk_entry_register_keyrelease,
       -                .cleanup = &ltk_entry_cleanup,
       -        },
       -        {
       -                .name = "grid",
       -                .ini_handler = NULL,
       -                .fill_theme_defaults = NULL,
       -                .uninitialize_theme = NULL,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "label",
       -                .ini_handler = &ltk_label_ini_handler,
       -                .fill_theme_defaults = &ltk_label_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_label_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                /* FIXME: this is actually image_widget */
       -                .name = "image",
       -                .ini_handler = NULL,
       -                .fill_theme_defaults = NULL,
       -                .uninitialize_theme = NULL,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "menu",
       -                .ini_handler = &ltk_menu_ini_handler,
       -                .fill_theme_defaults = &ltk_menu_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_menu_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "menuentry",
       -                .ini_handler = &ltk_menuentry_ini_handler,
       -                .fill_theme_defaults = &ltk_menuentry_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_menuentry_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "submenu",
       -                .ini_handler = &ltk_submenu_ini_handler,
       -                .fill_theme_defaults = &ltk_submenu_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_submenu_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                .name = "submenuentry",
       -                .ini_handler = &ltk_submenuentry_ini_handler,
       -                .fill_theme_defaults = &ltk_submenuentry_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_submenuentry_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -                 /*
       -                 This "widget" is only needed to have separate styles for regular
       -                   menu entries and submenu entries. "submenu" is just an alias for
       -                   "menu" in most cases - it's just needed when creating a menu to
       -                   decide if it's a submenu or not.
       -                   FIXME: is that even necessary? Why can't it just decide if it's
       -                   a submenu based on whether it has a parent or not?
       -                   -> I guess right-click menus are also just submenus, so they
       -                   need to set it explicitly, but wasn't there another reason? 
       -                 */
       -        },
       -        {
       -                .name = "scrollbar",
       -                .ini_handler = &ltk_scrollbar_ini_handler,
       -                .fill_theme_defaults = &ltk_scrollbar_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_scrollbar_uninitialize_theme,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                /* Handler for general widget key bindings. */
       -                .name = "widget",
       -                .ini_handler = NULL,
       -                .fill_theme_defaults = NULL,
       -                .uninitialize_theme = NULL,
       -                .register_keypress = NULL,
       -                .register_keyrelease = NULL,
       -                .cleanup = NULL,
       -        },
       -        {
       -                /* Handler for window theme. */
       -                .name = "window",
       -                .ini_handler = &ltk_window_ini_handler,
       -                .fill_theme_defaults = &ltk_window_fill_theme_defaults,
       -                .uninitialize_theme = &ltk_window_uninitialize_theme,
       -                .register_keypress = &ltk_window_register_keypress,
       -                .register_keyrelease = &ltk_window_register_keyrelease,
       -                .cleanup = &ltk_window_cleanup,
       -        }
       -};
       -
       -/* Get the directory to search for ltk.cfg in.
       -   This first checks the environment variable LTKDIR and,
       -   if that doesn't exist, the home directory with "/.ltk" appended.
       -   Returns NULL on error. */
       -static char *
       -ltk_get_dir(void) {
       -        char *dir, *dir_orig;
       -        struct passwd *pw;
       -        uid_t uid;
       -        int len;
       -
       -        dir_orig = getenv("LTKDIR");
       -        if (dir_orig) {
       -                dir = ltk_strdup(dir_orig);
       -        } else {
       -                uid = getuid();
       -                pw = getpwuid(uid);
       -                if (!pw)
       -                        return NULL;
       -                len = strlen(pw->pw_dir);
       -                dir = ltk_malloc(len + 6);
       -                if (!dir)
       -                        return NULL;
       -                strcpy(dir, pw->pw_dir);
       -                strcpy(dir + len, "/.ltk");
       -        }
       -
       -        return dir;
       -}
       -
       -int
       -ltk_init(void) {
       -        /* FIXME: should ltk set this? probably not */
       -        setlocale(LC_CTYPE, "");
       -        char *ltk_dir = ltk_get_dir();
       -        /* FIXME: return error instead of dying */
       -        if (!ltk_dir)
       -                ltk_fatal_errno("Unable to setup ltk directory.\n");
       -        shared_data.cur_kbd = 0;
       -
       -        /* FIXME: search different directories for config */
       -        char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
       -        char *theme_path;
       -        char *errstr = NULL;
       -        if (ltk_config_parsefile(config_path, &handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
       -                if (errstr) {
       -                        ltk_warn("Unable to load config: %s\n", errstr);
       -                        ltk_free0(errstr);
       -                }
       -                if (ltk_config_load_default(&handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
       -                        /* FIXME: I guess errstr isn't freed here, but whatever */
       -                        /* FIXME: return error instead of dying */
       -                        ltk_fatal("Unable to load default config: %s\n", errstr);
       -                }
       -        }
       -        ltk_free0(config_path);
       -        theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
       -        ltk_free0(ltk_dir);
       -        shared_data.renderdata = renderer_create();
       -        if (!shared_data.renderdata)
       -                return 1; /* FIXME: clean up */
       -        ltk_load_theme(theme_path);
       -        ltk_free0(theme_path);
       -        /* FIXME: maybe "general" theme instead of window theme? */
       -        ltk_window_theme *window_theme = ltk_window_get_theme();
       -        shared_data.text_context = ltk_text_context_create(shared_data.renderdata, window_theme->font);
       -        shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
       -        /* FIXME: configure cache size; check for overflow */
       -        ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
       -        shared_data.windows = ltk_array_create(window, 1);
       -        shared_data.rwindows = ltk_array_create(rwindow, 1);
       -        shared_data.cmds = ltk_array_create(cmd, 1);
       -        return 0; /* FIXME: or maybe 1? */
       -}
       -
       -/* FIXME: need to remove event masks from all widgets when removing client */
       -int
       -ltk_mainloop(void) {
       -        ltk_event event;
       -
       -        /* FIXME: make time management smarter - maybe always figure out how long
       -           it will take until the next timer is due and then sleep if no other events
       -           are happening */
       -        struct timespec now, elapsed, last, lasttimer, sleep_time;
       -        clock_gettime(CLOCK_MONOTONIC, &last);
       -        lasttimer = last;
       -        sleep_time.tv_sec = 0;
       -
       -        /* initialize keyboard mapping */
       -        ltk_generate_keyboard_event(shared_data.renderdata, &event);
       -        ltk_handle_event(&event);
       -
       -        int pid = -1;
       -        int wstatus = 0;
       -        /* FIXME: kill all children on exit? */
       -        while (running) {
       -                if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
       -                        //ltk_error err;
       -                        ltk_cmdinfo *info;
       -                        /* FIXME: should commands be split into read/write and block write commands during external editing? */
       -                        for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
       -                                info = &(ltk_array_get(shared_data.cmds, i));
       -                                if (info->pid == pid) {
       -                                        if (!info->caller) {
       -                                                ltk_warn("Widget disappeared while text was being edited in external program\n");
       -                                        /* FIXME: call overwritten cmd_return! */
       -                                        } else if (info->caller->vtable->cmd_return) {
       -                                                size_t file_len = 0;
       -                                                char *errstr = NULL;
       -                                                char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr);
       -                                                if (!contents) {
       -                                                        ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr);
       -                                                } else {
       -                                                        info->caller->vtable->cmd_return(info->caller, contents, file_len);
       -                                                        ltk_free0(contents);
       -                                                }
       -                                        }
       -                                        ltk_free0(info->tmpfile);
       -                                        ltk_array_delete(cmd, shared_data.cmds, i, 1);
       -                                        break;
       -                                }
       -                        }
       -                }
       -                while (!ltk_next_event(
       -                    shared_data.renderdata,
       -                    ltk_array_get_buf(shared_data.rwindows),
       -                    ltk_array_len(shared_data.rwindows),
       -                    shared_data.clipboard, shared_data.cur_kbd, &event)) {
       -                        ltk_handle_event(&event);
       -                }
       -
       -                clock_gettime(CLOCK_MONOTONIC, &now);
       -                ltk_timespecsub(&now, &lasttimer, &elapsed);
       -                /* Note: it should be safe to give the same pointer as the first and
       -                   last argument, as long as ltk_timespecsub/add isn't changed incompatibly */
       -                size_t i = 0;
       -                while (i < timers_num) {
       -                        ltk_timespecsub(&timers[i].remaining, &elapsed, &timers[i].remaining);
       -                        if (timers[i].remaining.tv_sec < 0 ||
       -                            (timers[i].remaining.tv_sec == 0 && timers[i].remaining.tv_nsec == 0)) {
       -                                timers[i].callback(timers[i].data);
       -                                if (timers[i].repeat.tv_sec == 0 && timers[i].repeat.tv_nsec == 0) {
       -                                        /* remove timer because it has no repeat */
       -                                        memmove(timers + i, timers + i + 1, sizeof(ltk_timer) * (timers_num - i - 1));
       -                                } else {
       -                                        ltk_timespecadd(&timers[i].remaining, &timers[i].repeat, &timers[i].remaining);
       -                                        i++;
       -                                }
       -                        } else {
       -                                i++;
       -                        }
       -                }
       -                lasttimer = now;
       -
       -                for (size_t i = 0; i < shared_data.windows->len; i++) {
       -                        ltk_window *window = shared_data.windows->buf[i];
       -                        if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
       -                                window->widget.vtable->draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0});
       -                        }
       -                }
       -
       -                clock_gettime(CLOCK_MONOTONIC, &now);
       -                ltk_timespecsub(&now, &last, &elapsed);
       -                /* FIXME: configure framerate */
       -                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       -                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       -                        nanosleep(&sleep_time, NULL);
       -                }
       -                last = now;
       -        }
       -
       -        ltk_deinit();
       -
       -        return 0;
       -}
       -
       -void
       -ltk_deinit(void) {
       -        if (running)
       -                return;
       -        if (shared_data.cmds) {
       -                for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
       -                        /* FIXME: maybe kill child processes? */
       -                        ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile);
       -                }
       -                ltk_array_destroy(cmd, shared_data.cmds);
       -        }
       -        shared_data.cmds = NULL;
       -        /* FIXME: also check for overwritten methods everywhere! */
       -        if (shared_data.windows) {
       -                for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
       -                        ltk_window *window = ltk_array_get(shared_data.windows, i);
       -                        ltk_widget_destroy(LTK_CAST_WIDGET(window), 0);
       -                }
       -                ltk_array_destroy(window, shared_data.windows);
       -        }
       -        shared_data.windows = NULL;
       -        if (shared_data.rwindows)
       -                ltk_array_destroy(rwindow, shared_data.rwindows);
       -        shared_data.rwindows = NULL;
       -        ltk_config_cleanup();
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (widget_funcs[i].cleanup)
       -                        widget_funcs[i].cleanup();
       -        }
       -        if (shared_data.text_context)
       -                ltk_text_context_destroy(shared_data.text_context);
       -        shared_data.text_context = NULL;
       -        if (shared_data.clipboard)
       -                ltk_clipboard_destroy(shared_data.clipboard);
       -        shared_data.clipboard = NULL;
       -        ltk_events_cleanup();
       -        if (shared_data.renderdata) {
       -                ltk_uninitialize_theme();
       -                renderer_destroy(shared_data.renderdata);
       -        }
       -        shared_data.renderdata = NULL;
       -}
       -
       -void
       -ltk_quit(void) {
       -        /* FIXME: maybe prevent other events from running? */
       -        running = 0;
       -}
       -
       -/* FIXME: check everywhere if initialized already */
       -ltk_window *
       -ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) {
       -        /* FIXME: more asserts, or maybe global "initialized" flag */
       -        ltk_assert(shared_data.renderdata != NULL);
       -        ltk_assert(shared_data.windows != NULL);
       -        ltk_assert(shared_data.rwindows != NULL);
       -        ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h);
       -        ltk_array_append(window, shared_data.windows, window);
       -        ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow);
       -        return window;
       -}
       -
       -void
       -ltk_window_destroy(ltk_widget *self, int shallow) {
       -        /* FIXME: would it make sense to do something with 'shallow' here? */
       -        (void)shallow;
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
       -                if (ltk_array_get(shared_data.windows, i) == window) {
       -                        ltk_array_delete(window, shared_data.windows, i, 1);
       -                        ltk_array_delete(rwindow, shared_data.rwindows, i, 1);
       -                        break;
       -                }
       -        }
       -        ltk_window_destroy_intern(window);
       -}
       -
       -ltk_clipboard *
       -ltk_get_clipboard(void) {
       -        /* FIXME: what to do when not initialized? */
       -        return shared_data.clipboard;
       -}
       -
       -/* FIXME: optimize timer handling - maybe also a sort of priority queue */
       -/* FIXME: JUST USE A GENERIC DYNAMIC ARRAY ALREADY!!!!! */
       -void
       -ltk_unregister_timer(int timer_id) {
       -        for (size_t i = 0; i < timers_num; i++) {
       -                if (timers[i].id == timer_id) {
       -                        memmove(
       -                            timers + i,
       -                            timers + i + 1,
       -                            sizeof(ltk_timer) * (timers_num - i - 1)
       -                        );
       -                        timers_num--;
       -                        size_t sz = ideal_array_size(timers_alloc, timers_num);
       -                        if (sz != timers_alloc) {
       -                                timers_alloc = sz;
       -                                timers = ltk_reallocarray(
       -                                    timers, sz, sizeof(ltk_timer)
       -                                );
       -                        }
       -                        return;
       -                }
       -        }
       -}
       -
       -/* repeat <= 0 means no repeat, first <= 0 means run as soon as possible */
       -int
       -ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data) {
       -        if (first < 0)
       -                first = 0;
       -        if (repeat < 0)
       -                repeat = 0;
       -        if (timers_num == timers_alloc) {
       -                timers_alloc = ideal_array_size(timers_alloc, timers_num + 1);
       -                timers = ltk_reallocarray(
       -                    timers, timers_alloc, sizeof(ltk_timer)
       -                );
       -        }
       -        /* FIXME: better finding of id */
       -        /* FIXME: maybe store sorted by id */
       -        int id = 0;
       -        for (size_t i = 0; i < timers_num; i++) {
       -                if (timers[i].id >= id)
       -                        id = timers[i].id + 1;
       -        }
       -        ltk_timer *t = &timers[timers_num++];
       -        t->callback = callback;
       -        t->data = data;
       -        t->repeat.tv_sec = repeat / 1000;
       -        t->repeat.tv_nsec = (repeat % 1000) * 1000;
       -        t->remaining.tv_sec = first / 1000;
       -        t->remaining.tv_nsec = (first % 1000) * 1000;
       -        t->id = id;
       -        return id;
       -}
       -
       -/* FIXME: standardize return codes - usually, 0 is returned on success, but ini.h
       -   uses 1 on success, so this is all a bit confusing */
       -/* FIXME: switch away from ini.h */
       -static int
       -ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value) {
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
       -                        widget_funcs[i].ini_handler(renderdata, prop, value);
       -                        return 1;
       -                }
       -        }
       -        return 0;
       -}
       -
       -/* FIXME: don't call ltk_fatal, instead return error from ltk_init */
       -static void
       -ltk_load_theme(const char *path) {
       -        /* FIXME: give line number in error message */
       -        if (ini_parse(path, ltk_ini_handler, shared_data.renderdata) != 0) {
       -                ltk_warn("Unable to load theme.\n");
       -        }
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (widget_funcs[i].fill_theme_defaults) {
       -                        if (widget_funcs[i].fill_theme_defaults(shared_data.renderdata)) {
       -                                ltk_uninitialize_theme();
       -                                ltk_fatal("Unable to load theme defaults.\n");
       -                        }
       -                }
       -        }
       -}
       -
       -static void
       -ltk_uninitialize_theme(void) {
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (widget_funcs[i].uninitialize_theme)
       -                        widget_funcs[i].uninitialize_theme(shared_data.renderdata);
       -        }
       -}
       -
       -static int
       -handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b) {
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
       -                        if (!widget_funcs[i].register_keypress)
       -                                return 1;
       -                        return widget_funcs[i].register_keypress(name, nlen, b);
       -                }
       -        }
       -        return 1;
       -}
       -
       -static int
       -handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b) {
       -        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       -                if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
       -                        if (!widget_funcs[i].register_keyrelease)
       -                                return 1;
       -                        return widget_funcs[i].register_keyrelease(name, nlen, b);
       -                }
       -        }
       -        return 1;
       -}
       -
       -int
       -ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
       -        /* FIXME: support environment variable $TMPDIR */
       -        ltk_cmdinfo info = {NULL, NULL, -1};
       -        info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
       -        int fd = mkstemp(info.tmpfile);
       -        if (fd == -1) {
       -                ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       -        }
       -        close(fd);
       -        /* FIXME: give file descriptor directly to modified version of ltk_write_file */
       -        char *errstr = NULL;
       -        if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) {
       -                ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr);
       -                unlink(info.tmpfile);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       -        }
       -        int pid = -1;
       -        if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) {
       -                /* FIXME: errno */
       -                ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
       -                unlink(info.tmpfile);
       -                ltk_free0(info.tmpfile);
       -                return 1;
       -        }
       -        info.pid = pid;
       -        info.caller = caller;
       -        ltk_array_append(cmd, shared_data.cmds, info);
       -        return 0;
       -}
       -
       -static void
       -ltk_handle_event(ltk_event *event) {
       -        size_t kbd_idx;
       -        if (event->type == 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
       -                        shared_data.cur_kbd = kbd_idx;
       -        } else {
       -                if (event->any.window_id < ltk_array_len(shared_data.windows)) {
       -                        ltk_window_handle_event(ltk_array_get(shared_data.windows, event->any.window_id), event);
       -                }
       -        }
       -}
       -
       -ltk_text_line *
       -ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width) {
       -        return ltk_text_line_create(shared_data.text_context, font_size, text, take_over_text, width);
       -}
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -1,51 +0,0 @@
       -/*
       - * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#ifndef LTK_H
       -#define LTK_H
       -
       -#include <stddef.h>
       -#include <stdint.h>
       -
       -#include "clipboard.h"
       -#include "widget.h"
       -#include "window.h"
       -#include "text.h"
       -
       -int ltk_init(void);
       -void ltk_deinit(void);
       -void ltk_quit(void);
       -int ltk_mainloop(void);
       -
       -void ltk_unregister_timer(int timer_id);
       -int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data);
       -
       -/* These are here so they can be added to the global array in ltk.c */
       -ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h);
       -void ltk_window_destroy(ltk_widget *self, int shallow);
       -
       -/* FIXME: allow piping text instead of writing to temporary file */
       -/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those 
       --> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
       --> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
       -int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
       -
       -/* convenience function to use the default text context */
       -ltk_text_line *ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width);
       -
       -ltk_clipboard *ltk_get_clipboard(void);
       -
       -#endif /* LTK_H */
   DIR diff --git a/src/.gitignore b/src/ltk/.gitignore
   DIR diff --git a/src/array.h b/src/ltk/array.h
   DIR diff --git a/src/ltk/box.c b/src/ltk/box.c
       t@@ -0,0 +1,470 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +/* FIXME: implement other sticky options now supported by grid */
       +
       +#include <limits.h>
       +#include <string.h>
       +
       +#include "box.h"
       +#include "event.h"
       +#include "graphics.h"
       +#include "memory.h"
       +#include "rect.h"
       +#include "scrollbar.h"
       +#include "widget.h"
       +
       +static void ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
       +static void ltk_box_destroy(ltk_widget *self, int shallow);
       +static void ltk_recalculate_box(ltk_widget *self);
       +static void ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget);
       +static int ltk_box_remove_child(ltk_widget *self, ltk_widget *widget);
       +/* static int ltk_box_clear(ltk_window *window, ltk_box *box, int shallow); */
       +static int ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data);
       +static int ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
       +static ltk_widget *ltk_box_get_child_at_pos(ltk_widget *self, int x, int y);
       +static void ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r);
       +
       +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,
       +        .hide = NULL,
       +        .draw = &ltk_box_draw,
       +        .destroy = &ltk_box_destroy,
       +        .resize = &ltk_recalculate_box,
       +        .child_size_change = &ltk_box_child_size_change,
       +        .remove_child = &ltk_box_remove_child,
       +        .key_press = NULL,
       +        .key_release = NULL,
       +        .mouse_press = NULL,
       +        .mouse_scroll = &ltk_box_mouse_scroll,
       +        .mouse_release = NULL,
       +        .motion_notify = NULL,
       +        .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,
       +        .invalid_signal = LTK_BOX_SIGNAL_INVALID,
       +};
       +
       +static void
       +ltk_box_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        ltk_widget *ptr;
       +        /* FIXME: clip out scrollbar */
       +        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 */
       +                ltk_widget_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, real_clip));
       +        }
       +        ltk_widget_draw(
       +            LTK_CAST_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)
       +        );
       +}
       +
       +ltk_box *
       +ltk_box_create(ltk_window *window, ltk_orientation orient) {
       +        ltk_box *box = ltk_malloc(sizeof(ltk_box));
       +        ltk_widget *self = LTK_CAST_WIDGET(box);
       +
       +        ltk_fill_widget_defaults(self, window, &vtable, 0, 0);
       +
       +        box->sc = ltk_scrollbar_create(window, orient);
       +        box->sc->widget.parent = self;
       +        ltk_widget_register_signal_handler(
       +                LTK_CAST_WIDGET(box->sc), LTK_SCROLLBAR_SIGNAL_SCROLL,
       +                &ltk_box_scroll_cb, LTK_MAKE_ARG_WIDGET(self)
       +        );
       +        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(self);
       +
       +        return box;
       +}
       +
       +static void
       +ltk_box_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
       +        ltk_box *box = LTK_CAST_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(LTK_CAST_WIDGET(box->sc), delta, 0);
       +}
       +
       +static void
       +ltk_box_destroy(ltk_widget *self, int shallow) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        ltk_widget *ptr;
       +        for (size_t i = 0; i < box->num_widgets; i++) {
       +                ptr = box->widgets[i];
       +                ptr->parent = NULL;
       +                if (!shallow)
       +                        ltk_widget_destroy(ptr, shallow);
       +        }
       +        ltk_free(box->widgets);
       +        box->sc->widget.parent = NULL;
       +        ltk_widget_destroy(LTK_CAST_WIDGET(box->sc), 0);
       +        ltk_free(box);
       +}
       +
       +/* FIXME: Make this function name more consistent */
       +/* FIXME: The widget positions are set with the old scrollbar->cur_pos, before the
       +   virtual_size is set - this can cause problems when a widget changes its size
       +   (in the scrolled direction) when resized. */
       +/* FIXME: avoid complete recalculation when just scrolling (only position updated) */
       +static void
       +ltk_recalculate_box(ltk_widget *self) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        ltk_widget *ptr;
       +        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->lrect.x = cur_pos - box->sc->cur_pos;
       +                        if (ptr->sticky & LTK_STICKY_TOP && ptr->sticky & LTK_STICKY_BOTTOM)
       +                                ptr->lrect.h = box->widget.lrect.h - sc_rect->h;
       +                        if (ptr->sticky & LTK_STICKY_TOP)
       +                                ptr->lrect.y = 0;
       +                        else if (ptr->sticky & LTK_STICKY_BOTTOM)
       +                                ptr->lrect.y = box->widget.lrect.h - ptr->lrect.h - sc_rect->h;
       +                        else
       +                                ptr->lrect.y = (box->widget.lrect.h - ptr->lrect.h) / 2;
       +                        cur_pos += ptr->lrect.w;
       +                } else {
       +                        ptr->lrect.y = cur_pos - box->sc->cur_pos;
       +                        if (ptr->sticky & LTK_STICKY_LEFT && ptr->sticky & LTK_STICKY_RIGHT)
       +                                ptr->lrect.w = box->widget.lrect.w - sc_rect->w;
       +                        if (ptr->sticky & LTK_STICKY_LEFT)
       +                                ptr->lrect.x = 0;
       +                        else if (ptr->sticky & LTK_STICKY_RIGHT)
       +                                ptr->lrect.x = box->widget.lrect.w - ptr->lrect.w - sc_rect->w;
       +                        else
       +                                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);
       +        if (box->orient == LTK_HORIZONTAL) {
       +                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.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_CAST_WIDGET(box->sc));
       +}
       +
       +/* FIXME: This entire resizing thing is a bit weird. For instance, if a label
       +   in a vertical box increases its height because its width has been decreased
       +   and it is forced to wrap, should that just change the rect or also the
       +   ideal size? Ideal size wouldn't really make sense here, but then the box
       +   might be forced to add a scrollbar even though the parent widget would
       +   actually give it more space if it knew that it needed it. */
       +
       +static void
       +ltk_box_child_size_change(ltk_widget *self, ltk_widget *widget) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        short size_changed = 0;
       +        /* This is always reset here - if it needs to be changed,
       +           the resize function called by the last child_size_change
       +           function will fix it */
       +        /* Note: This seems a bit weird, but if each widget set its rect itself,
       +           that would also lead to weird things. For instance, if a butten is
       +           added to after a box after being ungridded, and its rect was changed
       +           by the grid (e.g. because of a column weight), who should reset the
       +           rect if it doesn't have sticky set? Of course, the resize function
       +           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->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;
       +        } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w > box->widget.ideal_h) {
       +                box->widget.ideal_w = widget->ideal_w + sc_w;
       +                size_changed = 1;
       +        }
       +
       +        if (size_changed && box->widget.parent && box->widget.parent->vtable->child_size_change)
       +                box->widget.parent->vtable->child_size_change(box->widget.parent, (ltk_widget *)box);
       +        else
       +                ltk_recalculate_box(LTK_CAST_WIDGET(box));
       +        if (orig_w != widget->lrect.w || orig_h != widget->lrect.h)
       +                ltk_widget_resize(widget);
       +}
       +
       +int
       +ltk_box_add(ltk_box *box, ltk_widget *widget, ltk_sticky_mask sticky) {
       +        if (widget->parent)
       +                return 1;
       +        if (box->num_widgets >= box->num_alloc) {
       +                size_t new_size = box->num_alloc > 0 ? box->num_alloc * 2 : 4;
       +                ltk_widget **new = ltk_realloc(box->widgets, new_size * sizeof(ltk_widget *));
       +                box->num_alloc = new_size;
       +                box->widgets = new;
       +        }
       +
       +        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) {
       +                box->widget.ideal_w += widget->ideal_w;
       +                if (widget->ideal_h + sc_h > box->widget.ideal_h)
       +                        box->widget.ideal_h = widget->ideal_h + sc_h;
       +        } else {
       +                box->widget.ideal_h += widget->ideal_h;
       +                if (widget->ideal_w + sc_w > box->widget.ideal_w)
       +                        box->widget.ideal_w = widget->ideal_w + sc_w;
       +        }
       +        widget->parent = LTK_CAST_WIDGET(box);
       +        widget->sticky = sticky;
       +        ltk_box_child_size_change(LTK_CAST_WIDGET(box), widget);
       +        ltk_window_invalidate_widget_rect(box->widget.window, LTK_CAST_WIDGET(box));
       +
       +        return 0;
       +}
       +
       +int
       +ltk_box_remove_index(ltk_box *box, size_t index) {
       +        if (index >= box->num_widgets)
       +                return 1;
       +        ltk_widget *self = LTK_CAST_WIDGET(box);
       +        ltk_widget *widget = box->widgets[index];
       +        int sc_w = box->sc->widget.lrect.w;
       +        int sc_h = box->sc->widget.lrect.h;
       +        if (index < box->num_widgets - 1)
       +                memmove(box->widgets + index, box->widgets + index + 1,
       +                    (box->num_widgets - index - 1) * sizeof(ltk_widget *));
       +        box->num_widgets--;
       +        ltk_window_invalidate_widget_rect(self->window, self);
       +        /* search for new ideal width/height */
       +        /* FIXME: make this all a bit nicer and break the lines better */
       +        /* FIXME: other part of ideal size not updated */
       +        if (box->orient == LTK_HORIZONTAL && widget->ideal_h + sc_h == self->ideal_h) {
       +                self->ideal_h = 0;
       +                for (size_t j = 0; j < box->num_widgets; j++) {
       +                        if (box->widgets[j]->ideal_h + sc_h > self->ideal_h)
       +                                self->ideal_h = box->widgets[j]->ideal_h + sc_h;
       +                }
       +                if (self->parent)
       +                        ltk_widget_resize(self->parent);
       +        } else if (box->orient == LTK_VERTICAL && widget->ideal_w + sc_w == self->ideal_w) {
       +                self->ideal_w = 0;
       +                for (size_t j = 0; j < box->num_widgets; j++) {
       +                        if (box->widgets[j]->ideal_w + sc_w > self->ideal_w)
       +                                self->ideal_w = box->widgets[j]->ideal_w + sc_w;
       +                }
       +                if (self->parent)
       +                        ltk_widget_resize(self->parent);
       +        }
       +        return 0;
       +}
       +
       +int
       +ltk_box_remove(ltk_box *box, ltk_widget *widget) {
       +        if (widget->parent != LTK_CAST_WIDGET(box))
       +                return 1;
       +        widget->parent = NULL;
       +        for (size_t i = 0; i < box->num_widgets; i++) {
       +                if (box->widgets[i] == widget) {
       +                        return ltk_box_remove_index(box, i);
       +                }
       +        }
       +
       +        return 1;
       +}
       +
       +static int
       +ltk_box_remove_child(ltk_widget *self, ltk_widget *widget) {
       +        return ltk_box_remove(LTK_CAST_BOX(self), widget);
       +}
       +
       +/* 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_CAST_BOX(self);
       +        ltk_widget *minw = NULL;
       +        int min_dist = INT_MAX;
       +        for (size_t i = 0; i < box->num_widgets; i++) {
       +                ltk_rect r = box->widgets[i]->lrect;
       +                int dist = ltk_rect_fakedist(rect, r);
       +                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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_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_CAST_BOX(self);
       +        return box->num_widgets > 0 ? box->widgets[box->num_widgets-1] : NULL;
       +}
       +
       +static int
       +ltk_box_scroll_cb(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)self;
       +        (void)args;
       +        ltk_widget *boxw = LTK_CAST_ARG_WIDGET(data);
       +        ltk_recalculate_box(boxw);
       +        ltk_window_invalidate_widget_rect(boxw->window, boxw);
       +        return 1;
       +}
       +
       +static ltk_widget *
       +ltk_box_get_child_at_pos(ltk_widget *self, int x, int y) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        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]->crect, x, y))
       +                        return box->widgets[i];
       +        }
       +        return NULL;
       +}
       +
       +static int
       +ltk_box_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
       +        ltk_box *box = LTK_CAST_BOX(self);
       +        if (event->dy) {
       +                /* FIXME: horizontal scrolling, etc. */
       +                /* FIXME: configure scrollstep */
       +                int delta = event->dy * -15;
       +                ltk_scrollbar_scroll(LTK_CAST_WIDGET(box->sc), delta, 0);
       +                ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
       +                ltk_window_fake_motion_event(self->window, glob.x, glob.y);
       +                return 1;
       +        }
       +        return 0;
       +}
   DIR diff --git a/src/box.h b/src/ltk/box.h
   DIR diff --git a/src/button.c b/src/ltk/button.c
   DIR diff --git a/src/button.h b/src/ltk/button.h
   DIR diff --git a/src/clipboard.h b/src/ltk/clipboard.h
   DIR diff --git a/src/clipboard_xlib.c b/src/ltk/clipboard_xlib.c
   DIR diff --git a/src/clipboard_xlib.h b/src/ltk/clipboard_xlib.h
   DIR diff --git a/src/color.h b/src/ltk/color.h
   DIR diff --git a/src/color_xlib.c b/src/ltk/color_xlib.c
   DIR diff --git a/src/config.c b/src/ltk/config.c
   DIR diff --git a/src/config.h b/src/ltk/config.h
   DIR diff --git a/src/ctrlsel.c b/src/ltk/ctrlsel.c
   DIR diff --git a/src/ctrlsel.h b/src/ltk/ctrlsel.h
   DIR diff --git a/src/entry.c b/src/ltk/entry.c
   DIR diff --git a/src/entry.h b/src/ltk/entry.h
   DIR diff --git a/src/event.h b/src/ltk/event.h
   DIR diff --git a/src/event_xlib.c b/src/ltk/event_xlib.c
   DIR diff --git a/src/eventdefs.h b/src/ltk/eventdefs.h
   DIR diff --git a/src/ltk/graphics.h b/src/ltk/graphics.h
       t@@ -0,0 +1,77 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTK_GRAPHICS_H
       +#define LTK_GRAPHICS_H
       +
       +typedef struct ltk_renderdata ltk_renderdata;
       +typedef struct ltk_renderwindow ltk_renderwindow;
       +
       +#include <stddef.h>
       +
       +#include "rect.h"
       +#include "color.h"
       +
       +typedef enum {
       +        LTK_BORDER_NONE = 0,
       +        LTK_BORDER_TOP = 1,
       +        LTK_BORDER_RIGHT = 2,
       +        LTK_BORDER_BOTTOM = 4,
       +        LTK_BORDER_LEFT = 8,
       +        LTK_BORDER_ALL = 0xF
       +} ltk_border_sides;
       +
       +typedef struct ltk_surface ltk_surface;
       +
       +/* FIXME: graphics context */
       +ltk_surface *ltk_surface_create(ltk_renderwindow *window, int w, int h);
       +void ltk_surface_destroy(ltk_surface *s);
       +/* returns 0 if successful, 1 if not resizable */
       +int ltk_surface_resize(ltk_surface *s, int w, int h);
       +/* FIXME: kind of hacky */
       +void ltk_surface_update_size(ltk_surface *s, int w, int h);
       +ltk_surface *ltk_surface_from_window(ltk_renderwindow *window, int w, int h);
       +void ltk_surface_get_size(ltk_surface *s, int *w, int *h);
       +void ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y);
       +void ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width);
       +void ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect);
       +/* FIXME: document properly, especially difference to draw_rect with offsets and line_width */
       +void ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides);
       +void ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides);
       +void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints);
       +void ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip);
       +
       +/* TODO */
       +/*
       +void ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width);
       +void ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2);
       +void ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width);
       +void ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r);
       +*/
       +
       +/* FIXME: rename some of these functions */
       +void ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y);
       +ltk_renderdata *ltk_renderer_create(void);
       +ltk_renderwindow *ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h);
       +void ltk_renderer_destroy_window(ltk_renderwindow *window);
       +void ltk_renderer_destroy(ltk_renderdata *data);
       +void ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg);
       +/* FIXME: this is kind of out of place */
       +void ltk_renderer_swap_buffers(ltk_renderwindow *window);
       +/* FIXME: this is just for the socket name in ltkd and is a bit weird */
       +unsigned long ltk_renderer_get_window_id(ltk_renderwindow *window);
       +
       +#endif /* LTK_GRAPHICS_H */
   DIR diff --git a/src/ltk/graphics_xlib.c b/src/ltk/graphics_xlib.c
       t@@ -0,0 +1,600 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stdio.h>
       +#include <string.h>
       +
       +#include <X11/XKBlib.h>
       +#include <X11/Xlib.h>
       +#include <X11/Xutil.h>
       +#include <X11/extensions/XKB.h>
       +#include <X11/extensions/Xdbe.h>
       +#include <X11/extensions/dbe.h>
       +
       +#include "graphics_xlib.h"
       +#include "color.h"
       +#include "memory.h"
       +#include "rect.h"
       +#include "util.h"
       +
       +struct ltk_surface {
       +        int w, h;
       +        ltk_renderwindow *window;
       +        Drawable d;
       +        #if USE_XFT == 1
       +        XftDraw *xftdraw;
       +        #endif
       +        char resizable;
       +};
       +
       +ltk_surface *
       +ltk_surface_create(ltk_renderwindow *window, int w, int h) {
       +        ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
       +        if (w <= 0)
       +                w = 1;
       +        if (h <= 0)
       +                h = 1;
       +        s->w = w;
       +        s->h = h;
       +        s->window = window;
       +        s->d = XCreatePixmap(window->renderdata->dpy, window->xwindow, w, h, window->renderdata->depth);
       +        #if USE_XFT == 1
       +        s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
       +        #endif
       +        s->resizable = 1;
       +        return s;
       +}
       +
       +ltk_surface *
       +ltk_surface_from_window(ltk_renderwindow *window, int w, int h) {
       +        ltk_surface *s = ltk_malloc(sizeof(ltk_surface));
       +        s->w = w;
       +        s->h = h;
       +        s->window = window;
       +        s->d = window->drawable;
       +        #if USE_XFT == 1
       +        s->xftdraw = XftDrawCreate(window->renderdata->dpy, s->d, window->renderdata->vis, window->renderdata->cm);
       +        #endif
       +        s->resizable = 0;
       +        return s;
       +}
       +
       +void
       +ltk_surface_destroy(ltk_surface *s) {
       +        #if USE_XFT == 1
       +        XftDrawDestroy(s->xftdraw);
       +        #endif
       +        if (s->resizable)
       +                XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
       +        ltk_free(s);
       +}
       +
       +void
       +ltk_surface_update_size(ltk_surface *s, int w, int h) {
       +        /* FIXME: maybe return directly if surface is resizable? */
       +        s->w = w;
       +        s->h = h;
       +        /* FIXME: sort of hacky (this is only used by window surface) */
       +        #if USE_XFT == 1
       +        XftDrawChange(s->xftdraw, s->d);
       +        #endif
       +}
       +
       +int
       +ltk_surface_resize(ltk_surface *s, int w, int h) {
       +        if (!s->resizable)
       +                return 1;
       +        s->w = w;
       +        s->h = h;
       +        XFreePixmap(s->window->renderdata->dpy, (Pixmap)s->d);
       +        s->d = XCreatePixmap(s->window->renderdata->dpy, s->window->xwindow, w, h, s->window->renderdata->depth);
       +        #if USE_XFT == 1
       +        XftDrawChange(s->xftdraw, s->d);
       +        #endif
       +        return 0;
       +}
       +
       +void
       +ltk_surface_get_size(ltk_surface *s, int *w, int *h) {
       +        *w = s->w;
       +        *h = s->h;
       +}
       +
       +void
       +ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width) {
       +        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +        XSetLineAttributes(s->window->renderdata->dpy, s->window->gc, line_width, LineSolid, CapButt, JoinMiter);
       +        XDrawRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
       +}
       +
       +void
       +ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides) {
       +        /* drawn as rectangles to have proper control over line width - I'm not sure how exactly
       +           XDrawLine handles even line widths (i.e. on which side the extra pixel will be) */
       +        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +        if (border_sides & LTK_BORDER_TOP)
       +                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
       +        if (border_sides & LTK_BORDER_BOTTOM)
       +                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y + rect.h - line_width, rect.w, line_width);
       +        if (border_sides & LTK_BORDER_LEFT)
       +                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
       +        if (border_sides & LTK_BORDER_RIGHT)
       +                XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
       +}
       +
       +void
       +ltk_surface_draw_border_clipped(ltk_surface *s, ltk_color *c, ltk_rect rect, ltk_rect clip_rect, int line_width, ltk_border_sides border_sides) {
       +        if (line_width <= 0)
       +                return;
       +        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +        int width;
       +        ltk_rect final_rect = ltk_rect_intersect(rect, clip_rect);
       +        if (border_sides & LTK_BORDER_TOP) {
       +                width = rect.y - final_rect.y;
       +                if (width > -line_width) {
       +                        width = line_width + width;
       +                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, final_rect.w, width);
       +                }
       +        }
       +        if (border_sides & LTK_BORDER_BOTTOM) {
       +                width = (final_rect.y + final_rect.h) - (rect.y + rect.h);
       +                if (width > -line_width) {
       +                        width = line_width + width;
       +                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y + final_rect.h - width, final_rect.w, width);
       +                }
       +        }
       +        if (border_sides & LTK_BORDER_LEFT) {
       +                width = rect.x - final_rect.x;
       +                if (width > -line_width) {
       +                        width = line_width + width;
       +                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x, final_rect.y, width, final_rect.h);
       +                }
       +        }
       +        if (border_sides & LTK_BORDER_RIGHT) {
       +                width = (final_rect.x + final_rect.w) - (rect.x + rect.w);
       +                if (width > -line_width) {
       +                        width = line_width + width;
       +                        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, final_rect.x + final_rect.w - width, final_rect.y, width, final_rect.h);
       +                }
       +        }
       +}
       +
       +void
       +ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
       +        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +        XFillRectangle(s->window->renderdata->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
       +}
       +
       +void
       +ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
       +        /* FIXME: maybe make this static since this won't be threaded anyways? */
       +        XPoint tmp_points[6]; /* to avoid extra allocations when not necessary */
       +        /* FIXME: this is ugly and inefficient */
       +        XPoint *final_points;
       +        if (npoints <= 6) {
       +                final_points = tmp_points;
       +        } else {
       +                final_points = ltk_reallocarray(NULL, npoints, sizeof(XPoint));
       +        }
       +        /* FIXME: how to deal with ints that don't fit in short? */
       +        for (size_t i = 0; i < npoints; i++) {
       +                final_points[i].x = (short)points[i].x;
       +                final_points[i].y = (short)points[i].y;
       +        }
       +        XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +        XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
       +        if (npoints > 6)
       +                ltk_free(final_points);
       +}
       +
       +static inline void
       +swap_ptr(void **ptr1, void **ptr2) {
       +        void *tmp = *ptr1;
       +        *ptr1 = *ptr2;
       +        *ptr2 = tmp;
       +}
       +
       +#define check_size(cond) if (!(cond)) ltk_fatal("Unable to perform polygon clipping. This is a bug, tell lumidify about it.\n")
       +
       +/* FIXME: xlib already includes clipping... */
       +/* FIXME: this can probably be optimized */
       +/* This is basically Sutherland-Hodgman, but only the special case for clipping rectangles. */
       +void
       +ltk_surface_fill_polygon_clipped(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints, ltk_rect clip) {
       +        /* FIXME: is this even more efficient? */
       +        XPoint tmp_points1[12]; /* to avoid extra allocations when not necessary */
       +        XPoint tmp_points2[12];
       +        XPoint *points1;
       +        XPoint *points2;
       +        /* FIXME: be a bit smarter about this */
       +        if (npoints <= 6) {
       +                points1 = tmp_points1;
       +                points2 = tmp_points2;
       +        } else {
       +                /* FIXME: I'm pretty sure there can never be more points than this
       +                   since we're only clipping against a rectangle, right?
       +                   If I can be sure about that, I can remove all the check_size's below. */
       +                points1 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
       +                points2 = ltk_reallocarray(NULL, npoints, sizeof(XPoint) * 2);
       +        }
       +
       +        size_t num1 = npoints;
       +        size_t num2 = 0;
       +        for (size_t i = 0; i < npoints; i++) {
       +                points1[i].x = (short)points[i].x;
       +                points1[i].y = (short)points[i].y;
       +        }
       +
       +        for (size_t i = 0; i < num1; i++) {
       +                XPoint p1 = points1[i];
       +                XPoint p2 = points1[(i + 1) % num1];
       +                if (p1.x >= clip.x) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = p1;
       +                        if (p2.x < clip.x) {
       +                                check_size(num2 < npoints * 2);
       +                                points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
       +                        }
       +                } else if (p2.x >= clip.x) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = (XPoint){.x = (short)clip.x, .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x - p1.x) / (p2.x - p1.x))};
       +                }
       +        }
       +        num1 = num2;
       +        num2 = 0;
       +        swap_ptr((void**)&points1, (void**)&points2);
       +
       +        for (size_t i = 0; i < num1; i++) {
       +                XPoint p1 = points1[i];
       +                XPoint p2 = points1[(i + 1) % num1];
       +                if (p1.x <= clip.x + clip.w) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = p1;
       +                        if (p2.x > clip.x + clip.w) {
       +                                check_size(num2 < npoints * 2);
       +                                points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
       +                        }
       +                } else if (p2.x <= clip.x + clip.w) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = (XPoint){.x = (short)(clip.x + clip.w), .y = (short)(p1.y + (p2.y - p1.y) * (float)(clip.x + clip.w - p1.x) / (p2.x - p1.x))};
       +                }
       +        }
       +        num1 = num2;
       +        num2 = 0;
       +        swap_ptr((void**)&points1, (void**)&points2);
       +
       +        for (size_t i = 0; i < num1; i++) {
       +                XPoint p1 = points1[i];
       +                XPoint p2 = points1[(i + 1) % num1];
       +                if (p1.y >= clip.y) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = p1;
       +                        if (p2.y < clip.y) {
       +                                check_size(num2 < npoints * 2);
       +                                points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
       +                        }
       +                } else if (p2.y >= clip.y) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = (XPoint){.y = (short)clip.y, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y - p1.y) / (p2.y - p1.y))};
       +                }
       +        }
       +        num1 = num2;
       +        num2 = 0;
       +        swap_ptr((void**)&points1, (void**)&points2);
       +
       +        for (size_t i = 0; i < num1; i++) {
       +                XPoint p1 = points1[i];
       +                XPoint p2 = points1[(i + 1) % num1];
       +                if (p1.y <= clip.y + clip.h) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = p1;
       +                        if (p2.y > clip.y + clip.h) {
       +                                check_size(num2 < npoints * 2);
       +                                points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
       +                        }
       +                } else if (p2.y <= clip.y + clip.h) {
       +                        check_size(num2 < npoints * 2);
       +                        points2[num2++] = (XPoint){.y = (short)clip.y + clip.h, .x = (short)(p1.x + (p2.x - p1.x) * (float)(clip.y + clip.h - p1.y) / (p2.y - p1.y))};
       +                }
       +        }
       +
       +        if (num2 > 0) {
       +                XSetForeground(s->window->renderdata->dpy, s->window->gc, c->xcolor.pixel);
       +                XFillPolygon(s->window->renderdata->dpy, s->d, s->window->gc, points2, (int)num2, Complex, CoordModeOrigin);
       +        }
       +        if (npoints > 6) {
       +                ltk_free(points1);
       +                ltk_free(points2);
       +        }
       +}
       +
       +void
       +ltk_surface_copy(ltk_surface *src, ltk_surface *dst, ltk_rect src_rect, int dst_x, int dst_y) {
       +        XCopyArea(
       +            src->window->renderdata->dpy, src->d, dst->d, src->window->gc,
       +            src_rect.x, src_rect.y, src_rect.w, src_rect.h, dst_x, dst_y
       +        );
       +}
       +
       +/* TODO */
       +/*
       +void
       +ltk_surface_draw_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2, int line_width) {
       +}
       +
       +void
       +ltk_surface_fill_arc(ltk_surface *s, ltk_color *c, int x, int y, int w, int h, int angle1, int angle2) {
       +}
       +
       +void
       +ltk_surface_draw_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r, int line_width) {
       +}
       +
       +void
       +ltk_surface_fill_circle(ltk_surface *s, ltk_color *c, int xc, int yc, int r) {
       +}
       +*/
       +
       +#if USE_XFT == 1
       +XftDraw *
       +ltk_surface_get_xft_draw(ltk_surface *s) {
       +        return s->xftdraw;
       +}
       +#endif
       +
       +Drawable
       +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_renderwindow *window);
       +
       +static void
       +ximdestroy(XIM xim, XPointer client, XPointer call) {
       +        (void)xim;
       +        (void)call;
       +        ltk_renderwindow *window = (ltk_renderwindow *)client;
       +        window->xim = NULL;
       +        XRegisterIMInstantiateCallback(
       +            window->renderdata->dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
       +        );
       +        XFree(window->spotlist);
       +}
       +
       +static int
       +xicdestroy(XIC xim, XPointer client, XPointer call) {
       +        (void)xim;
       +        (void)call;
       +        ltk_renderwindow *window = (ltk_renderwindow *)client;
       +        window->xic = NULL;
       +        return 1;
       +}
       +
       +static int
       +ximopen(ltk_renderwindow *window) {
       +        XIMCallback imdestroy = { .client_data = (XPointer)window, .callback = ximdestroy };
       +        XICCallback icdestroy = { .client_data = (XPointer)window, .callback = xicdestroy };
       +
       +        window->xim = XOpenIM(window->renderdata->dpy, NULL, NULL, NULL);
       +        if (window->xim == NULL)
       +                return 0;
       +
       +        if (XSetIMValues(window->xim, XNDestroyCallback, &imdestroy, NULL))
       +                ltk_warn("XSetIMValues: Could not set XNDestroyCallback.\n");
       +
       +        window->spotlist = XVaCreateNestedList(0, XNSpotLocation, &window->spot, NULL);
       +
       +        if (window->xic == NULL) {
       +                window->xic = XCreateIC(
       +                    window->xim, XNInputStyle,
       +                    XIMPreeditNothing | XIMStatusNothing,
       +                    XNClientWindow, window->xwindow,
       +                    XNDestroyCallback, &icdestroy, NULL
       +                );
       +        }
       +        if (window->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_renderwindow *window = (ltk_renderwindow *)client;
       +        if (ximopen(window)) {
       +                XUnregisterIMInstantiateCallback(
       +                    dpy, NULL, NULL, NULL, ximinstantiate, (XPointer)window
       +                );
       +        }
       +}
       +
       +void
       +ltk_renderer_set_imspot(ltk_renderwindow *window, int x, int y) {
       +        if (window->xic == NULL)
       +                return;
       +        window->spot.x = x;
       +        window->spot.y = y;
       +        XSetICValues(window->xic, XNPreeditAttributes, window->spotlist, NULL);
       +}
       +
       +ltk_renderdata *
       +ltk_renderer_create(void) {
       +        /* FIXME: this might not be the best place for this */
       +        XSetLocaleModifiers("");
       +        ltk_renderdata *renderdata = ltk_malloc(sizeof(ltk_renderdata));
       +        renderdata->dpy = XOpenDisplay(NULL);
       +        renderdata->screen = DefaultScreen(renderdata->dpy);
       +        renderdata->db_enabled = 0;
       +        /* based on http://wili.cc/blog/xdbe.html */
       +        int major, minor;
       +        if (XdbeQueryExtension(renderdata->dpy, &major, &minor)) {
       +                int num_screens = 1;
       +                Drawable screens[] = {DefaultRootWindow(renderdata->dpy)};
       +                XdbeScreenVisualInfo *info = XdbeGetVisualInfo(
       +                    renderdata->dpy, screens, &num_screens
       +                );
       +                if (!info || num_screens < 1 || info->count < 1) {
       +                        ltk_fatal("No visuals support Xdbe.");
       +                }
       +                XVisualInfo xvisinfo_templ;
       +                /* we know there's at least one */
       +                xvisinfo_templ.visualid = info->visinfo[0].visual;
       +                /* FIXME: proper screen number? */
       +                xvisinfo_templ.screen = 0;
       +                xvisinfo_templ.depth = info->visinfo[0].depth;
       +                int matches;
       +                XVisualInfo *xvisinfo_match = XGetVisualInfo(
       +                    renderdata->dpy,
       +                    VisualIDMask | VisualScreenMask | VisualDepthMask,
       +                    &xvisinfo_templ, &matches
       +                );
       +                if (!xvisinfo_match || matches < 1) {
       +                        ltk_fatal("Couldn't match a Visual with double buffering.\n");
       +                }
       +                renderdata->vis = xvisinfo_match->visual;
       +                /* FIXME: is it legal to free this while keeping the visual? */
       +                XFree(xvisinfo_match);
       +                XdbeFreeVisualInfo(info);
       +                renderdata->db_enabled = 1;
       +        } else {
       +                renderdata->vis = DefaultVisual(renderdata->dpy, renderdata->screen);
       +                ltk_warn("No Xdbe support.\n");
       +        }
       +        renderdata->cm = DefaultColormap(renderdata->dpy, renderdata->screen);
       +        renderdata->wm_delete_msg = XInternAtom(renderdata->dpy, "WM_DELETE_WINDOW", False);
       +        renderdata->depth = DefaultDepth(renderdata->dpy, renderdata->screen);
       +        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;
       +}
       +
       +ltk_renderwindow *
       +ltk_renderer_create_window(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
       +        XSetWindowAttributes attrs;
       +        ltk_renderwindow *window = ltk_malloc(sizeof(ltk_renderwindow));
       +        window->renderdata = data;
       +        memset(&attrs, 0, sizeof(attrs));
       +        attrs.background_pixel = BlackPixel(data->dpy, data->screen);
       +        attrs.colormap = data->cm;
       +        attrs.border_pixel = WhitePixel(data->dpy, data->screen);
       +        /* this causes the window contents to be kept
       +         * when it is resized, leading to less flicker */
       +        attrs.bit_gravity = NorthWestGravity;
       +        attrs.event_mask =
       +                ExposureMask | KeyPressMask | KeyReleaseMask |
       +                ButtonPressMask | ButtonReleaseMask |
       +                StructureNotifyMask | PointerMotionMask;
       +        /* FIXME: set border width */
       +        window->xwindow = XCreateWindow(
       +            data->dpy, DefaultRootWindow(data->dpy), x, y,
       +            w, h, 0, data->depth,
       +            InputOutput, data->vis,
       +            CWBackPixel | CWColormap | CWBitGravity | CWEventMask | CWBorderPixel, &attrs
       +        );
       +
       +        if (data->db_enabled) {
       +                window->back_buf = XdbeAllocateBackBufferName(
       +                        data->dpy, window->xwindow, XdbeBackground
       +                );
       +        } else {
       +                window->back_buf = window->xwindow;
       +        }
       +        window->drawable = window->back_buf;
       +        window->gc = XCreateGC(data->dpy, window->xwindow, 0, 0);
       +        XSetStandardProperties(
       +                data->dpy, window->xwindow,
       +                title, NULL, None, NULL, 0, NULL
       +        );
       +        /* FIXME: check return value */
       +        XSetWMProtocols(data->dpy, window->xwindow, &data->wm_delete_msg, 1);
       +
       +        window->xic = NULL;
       +        window->xim = NULL;
       +        if (!ximopen(window)) {
       +                XRegisterIMInstantiateCallback(
       +                    window->renderdata->dpy, NULL, NULL, NULL,
       +                    ximinstantiate, (XPointer)window
       +                );
       +        }
       +
       +        XClearWindow(window->renderdata->dpy, window->xwindow);
       +        XMapRaised(window->renderdata->dpy, window->xwindow);
       +
       +        return window;
       +}
       +
       +void
       +ltk_renderer_destroy_window(ltk_renderwindow *window) {
       +        XFreeGC(window->renderdata->dpy, window->gc);
       +        if (window->spotlist)
       +                XFree(window->spotlist);
       +        /* FIXME: destroy xim/xic? */
       +        XDestroyWindow(window->renderdata->dpy, window->xwindow);
       +        ltk_free(window);
       +}
       +
       +void
       +ltk_renderer_destroy(ltk_renderdata *renderdata) {
       +        XCloseDisplay(renderdata->dpy);
       +        /* FIXME: destroy visual, wm_delete_msg, etc.? */
       +        ltk_free(renderdata);
       +}
       +
       +/* FIXME: this is a completely random collection of properties and should be
       +   changed to a more sensible list */
       +void
       +ltk_renderer_set_window_properties(ltk_renderwindow *window, ltk_color *bg) {
       +        XSetWindowBackground(window->renderdata->dpy, window->xwindow, bg->xcolor.pixel);
       +}
       +
       +void
       +ltk_renderer_swap_buffers(ltk_renderwindow *window) {
       +        XdbeSwapInfo swap_info;
       +        swap_info.swap_window = window->xwindow;
       +        swap_info.swap_action = XdbeBackground;
       +        if (!XdbeSwapBuffers(window->renderdata->dpy, &swap_info, 1))
       +                ltk_fatal("Unable to swap buffers.\n");
       +        XFlush(window->renderdata->dpy);
       +}
       +
       +unsigned long
       +ltk_renderer_get_window_id(ltk_renderwindow *window) {
       +        return (unsigned long)window->xwindow;
       +}
   DIR diff --git a/src/graphics_xlib.h b/src/ltk/graphics_xlib.h
   DIR diff --git a/src/ltk/grid.c b/src/ltk/grid.c
       t@@ -0,0 +1,546 @@
       +/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
       +
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +/* TODO: make ungrid function also adjust static row/column width/height
       +   -> also, how should the grid deal with a widget spanning over multiple
       +      rows/columns with static size - if all are static, it could just
       +      divide the widget size (it would complicate things, though), but
       +      what should happen if some rows/columns under the span do have a
       +      positive weight? */
       +
       +#include <stddef.h>
       +#include <limits.h>
       +
       +#include "memory.h"
       +#include "rect.h"
       +#include "widget.h"
       +#include "util.h"
       +#include "grid.h"
       +#include "graphics.h"
       +
       +void ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight);
       +void ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight);
       +ltk_grid *ltk_grid_create(ltk_window *window, int rows, int columns);
       +int ltk_grid_add(
       +        ltk_grid *grid, ltk_widget *widget,
       +        int row, int column, int row_span, int column_span,
       +        ltk_sticky_mask sticky
       +);
       +/* just a wrapper around ltk_grid_remove to make types match */
       +static int ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget);
       +int ltk_grid_remove(ltk_grid *grid, ltk_widget *widget);
       +
       +static void ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
       +static void ltk_grid_destroy(ltk_widget *self, int shallow);
       +static void ltk_recalculate_grid(ltk_widget *self);
       +static void ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget);
       +static int ltk_grid_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,
       +        .resize = &ltk_recalculate_grid,
       +        .hide = NULL,
       +        .change_state = NULL,
       +        .child_size_change = &ltk_grid_child_size_change,
       +        .remove_child = &ltk_grid_remove_child,
       +        .mouse_press = NULL,
       +        .mouse_scroll = NULL,
       +        .mouse_release = NULL,
       +        .motion_notify = NULL,
       +        .get_child_at_pos = &ltk_grid_get_child_at_pos,
       +        .mouse_leave = NULL,
       +        .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,
       +        .invalid_signal = LTK_GRID_SIGNAL_INVALID,
       +};
       +
       +/* FIXME: only set "dirty" bit to avoid constand recalculation when
       +   setting multiple row/column weights? */
       +void
       +ltk_grid_set_row_weight(ltk_grid *grid, int row, int weight) {
       +        ltk_assert(row < grid->rows);
       +        grid->row_weights[row] = weight;
       +        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       +}
       +
       +void
       +ltk_grid_set_column_weight(ltk_grid *grid, int column, int weight) {
       +        ltk_assert(column < grid->columns);
       +        grid->column_weights[column] = weight;
       +        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       +}
       +
       +static void
       +ltk_grid_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
       +        ltk_grid *grid = LTK_CAST_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];
       +                int max_w = grid->column_pos[ptr->column + ptr->column_span] - grid->column_pos[ptr->column];
       +                int max_h = grid->row_pos[ptr->row + ptr->row_span] - grid->row_pos[ptr->row];
       +                ltk_rect r = ltk_rect_intersect(
       +                    (ltk_rect){grid->column_pos[ptr->column], grid->row_pos[ptr->row], max_w, max_h}, real_clip
       +                );
       +                ltk_widget_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, r));
       +        }
       +}
       +
       +ltk_grid *
       +ltk_grid_create(ltk_window *window, int rows, int columns) {
       +        ltk_grid *grid = ltk_malloc(sizeof(ltk_grid));
       +
       +        ltk_fill_widget_defaults(LTK_CAST_WIDGET(grid), window, &vtable, 0, 0);
       +
       +        grid->rows = rows;
       +        grid->columns = columns;
       +        grid->widget_grid = ltk_malloc(rows * columns * sizeof(ltk_widget));
       +        grid->row_heights = ltk_malloc(rows * sizeof(int));
       +        grid->column_widths = ltk_malloc(rows * sizeof(int));
       +        grid->row_weights = ltk_malloc(rows * sizeof(int));
       +        grid->column_weights = ltk_malloc(columns * sizeof(int));
       +        /* Positions have one extra for the end */
       +        grid->row_pos = ltk_malloc((rows + 1) * sizeof(int));
       +        grid->column_pos = ltk_malloc((columns + 1) * sizeof(int));
       +        /* FIXME: wow, that's horrible, this should just use memset */
       +        int i;
       +        for (i = 0; i < rows; i++) {
       +                grid->row_heights[i] = 0;
       +                grid->row_weights[i] = 0;
       +                grid->row_pos[i] = 0;
       +        }
       +        grid->row_pos[rows] = 0;
       +        for (i = 0; i < columns; i++) {
       +                grid->column_widths[i] = 0;
       +                grid->column_weights[i] = 0;
       +                grid->column_pos[i] = 0;
       +        }
       +        grid->column_pos[columns] = 0;
       +        for (i = 0; i < rows * columns; i++) {
       +                grid->widget_grid[i] = NULL;
       +        }
       +
       +        ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       +        return grid;
       +}
       +
       +static void
       +ltk_grid_destroy(ltk_widget *self, int shallow) {
       +        ltk_grid *grid = LTK_CAST_GRID(self);
       +        ltk_widget *ptr;
       +        for (int i = 0; i < grid->rows * grid->columns; i++) {
       +                if (grid->widget_grid[i]) {
       +                        ptr = grid->widget_grid[i];
       +                        ptr->parent = NULL;
       +                        if (!shallow) {
       +                                /* required to avoid freeing a widget multiple times
       +                                   if row_span or column_span is not 1 */
       +                                for (int r = ptr->row; r < ptr->row + ptr->row_span; r++) {
       +                                        for (int c = ptr->column; c < ptr->column + ptr->column_span; c++) {
       +                                                grid->widget_grid[r * grid->columns + c] = NULL;
       +                                        }
       +                                }
       +                                ltk_widget_destroy(ptr, shallow);
       +                        }
       +                }
       +        }
       +        ltk_free(grid->widget_grid);
       +        ltk_free(grid->row_heights);
       +        ltk_free(grid->column_widths);
       +        ltk_free(grid->row_weights);
       +        ltk_free(grid->column_weights);
       +        ltk_free(grid->row_pos);
       +        ltk_free(grid->column_pos);
       +        ltk_free(grid);
       +}
       +
       +static void
       +ltk_recalculate_grid(ltk_widget *self) {
       +        ltk_grid *grid = LTK_CAST_GRID(self);
       +        unsigned int height_static = 0, width_static = 0;
       +        unsigned int total_row_weight = 0, total_column_weight = 0;
       +        float height_unit = 0, width_unit = 0;
       +        unsigned int currentx = 0, currenty = 0;
       +        int i, j;
       +        for (i = 0; i < grid->rows; i++) {
       +                total_row_weight += grid->row_weights[i];
       +                if (grid->row_weights[i] == 0) {
       +                        height_static += grid->row_heights[i];
       +                }
       +        }
       +        for (i = 0; i < grid->columns; i++) {
       +                total_column_weight += grid->column_weights[i];
       +                if (grid->column_weights[i] == 0) {
       +                        width_static += grid->column_widths[i];
       +                }
       +        }
       +        /* FIXME: what should be done when static height or width is larger than grid? */
       +        if (total_row_weight > 0) {
       +                height_unit = (float) (self->lrect.h - height_static) / (float) total_row_weight;
       +        }
       +        if (total_column_weight > 0) {
       +                width_unit = (float) (self->lrect.w - width_static) / (float) total_column_weight;
       +        }
       +        for (i = 0; i < grid->rows; i++) {
       +                grid->row_pos[i] = currenty;
       +                if (grid->row_weights[i] > 0) {
       +                        grid->row_heights[i] = grid->row_weights[i] * height_unit;
       +                }
       +                currenty += grid->row_heights[i];
       +        }
       +        grid->row_pos[grid->rows] = currenty;
       +        for (i = 0; i < grid->columns; i++) {
       +                grid->column_pos[i] = currentx;
       +                if (grid->column_weights[i] > 0) {
       +                        grid->column_widths[i] = grid->column_weights[i] * width_unit;
       +                }
       +                currentx += grid->column_widths[i];
       +        }
       +        grid->column_pos[grid->columns] = currentx;
       +        /*int orig_width, orig_height;*/
       +        int end_column, end_row;
       +        for (i = 0; i < grid->rows; i++) {
       +                for (j = 0; j < grid->columns; j++) {
       +                        ltk_widget *ptr = grid->widget_grid[i * grid->columns + j];
       +                        if (!ptr || ptr->row != i || ptr->column != j)
       +                                continue;
       +                        /*orig_width = ptr->lrect.w;
       +                        orig_height = ptr->lrect.h;*/
       +                        ptr->lrect.w = ptr->ideal_w;
       +                        ptr->lrect.h = ptr->ideal_h;
       +                        end_row = i + ptr->row_span;
       +                        end_column = j + ptr->column_span;
       +                        int max_w = grid->column_pos[end_column] - grid->column_pos[j];
       +                        int max_h = grid->row_pos[end_row] - grid->row_pos[i];
       +                        int stretch_width = (ptr->sticky & LTK_STICKY_LEFT) && (ptr->sticky & LTK_STICKY_RIGHT);
       +                        int shrink_width = (ptr->sticky & LTK_STICKY_SHRINK_WIDTH) && ptr->lrect.w > max_w;
       +                        int stretch_height = (ptr->sticky & LTK_STICKY_TOP) && (ptr->sticky & LTK_STICKY_BOTTOM);
       +                        int shrink_height = (ptr->sticky & LTK_STICKY_SHRINK_HEIGHT) && ptr->lrect.h > max_h;
       +                        if (stretch_width || shrink_width)
       +                                ptr->lrect.w = max_w;
       +                        if (stretch_height || shrink_height)
       +                                ptr->lrect.h = max_h;
       +                        if (ptr->sticky & LTK_STICKY_PRESERVE_ASPECT_RATIO) {
       +                                if (!stretch_width && !shrink_width) {
       +                                        ptr->lrect.w = (int)(((double)ptr->lrect.h / ptr->ideal_h) * ptr->ideal_w);
       +                                } else if (!stretch_height && !shrink_height) {
       +                                        ptr->lrect.h = (int)(((double)ptr->lrect.w / ptr->ideal_w) * ptr->ideal_h);
       +                                } else {
       +                                        double scale_w = (double)ptr->lrect.w / ptr->ideal_w;
       +                                        double scale_h = (double)ptr->lrect.h / ptr->ideal_h;
       +                                        if (scale_w * ptr->ideal_h > ptr->lrect.h)
       +                                                ptr->lrect.w = (int)(scale_h * ptr->ideal_w);
       +                                        else if (scale_h * ptr->ideal_w > ptr->lrect.w)
       +                                                ptr->lrect.h = (int)(scale_w * ptr->ideal_h);
       +                                }
       +                        }
       +
       +                        /* the "default" case needs to come first because the widget may be stretched
       +                           with aspect ratio preserving, and in that case it should still be centered */
       +                        if (stretch_width || !(ptr->sticky & (LTK_STICKY_RIGHT|LTK_STICKY_LEFT))) {
       +                                ptr->lrect.x = grid->column_pos[j] + (grid->column_pos[end_column] - grid->column_pos[j] - ptr->lrect.w) / 2;
       +                        } else if (ptr->sticky & LTK_STICKY_RIGHT) {
       +                                ptr->lrect.x = grid->column_pos[end_column] - ptr->lrect.w;
       +                        } else if (ptr->sticky & LTK_STICKY_LEFT) {
       +                                ptr->lrect.x = grid->column_pos[j];
       +                        }
       +
       +                        if (stretch_height || !(ptr->sticky & (LTK_STICKY_TOP|LTK_STICKY_BOTTOM))) {
       +                                ptr->lrect.y = grid->row_pos[i] + (grid->row_pos[end_row] - grid->row_pos[i] - ptr->lrect.h) / 2;
       +                        } else if (ptr->sticky & LTK_STICKY_BOTTOM) {
       +                                ptr->lrect.y = grid->row_pos[end_row] - ptr->lrect.h;
       +                        } else if (ptr->sticky & LTK_STICKY_TOP) {
       +                                ptr->lrect.y = grid->row_pos[i];
       +                        }
       +                        /* intersect both with the grid rect and with the rect of the covered cells since there may be
       +                           weird cases where the layout doesn't work properly and the cells are partially outside the grid */
       +                        ptr->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, ptr->lrect);
       +                        ptr->crect = ltk_rect_intersect((ltk_rect){grid->column_pos[j], grid->row_pos[i], max_w, max_h}, ptr->crect);
       +
       +                        /* FIXME: Figure out a better system for this - it would be nice to make it more
       +                           efficient by not doing anything if nothing changed, but that doesn't work when
       +                           this function was called because of a child_size_change. In that case, if a
       +                           container widget is nested inside another container widget and another widget
       +                           inside the nested container sends a child_size_change but the toplevel container
       +                           doesn't change the size of the container, the position/size of the widget at the
       +                           bottom of the hierarchy will never be updated. That's why updates are forced
       +                           here even if seemingly nothing changed, but there probably is a better way. */
       +                        /*if (orig_width != ptr->lrect.w || orig_height != ptr->lrect.h)*/
       +                                ltk_widget_resize(ptr);
       +                }
       +        }
       +}
       +
       +/* FIXME: Maybe add debug stuff to check that grid is actually parent of widget */
       +static void
       +ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
       +        ltk_grid *grid = LTK_CAST_GRID(self);
       +        short size_changed = 0;
       +        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->lrect.w > grid->column_widths[widget->column]) {
       +                self->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->lrect.h > grid->row_heights[widget->row]) {
       +                self->ideal_h += widget->lrect.h - grid->row_heights[widget->row];
       +                grid->row_heights[widget->row] = widget->lrect.h;
       +                size_changed = 1;
       +        }
       +        if (size_changed && self->parent && self->parent->vtable->child_size_change)
       +                self->parent->vtable->child_size_change(self->parent, LTK_CAST_WIDGET(grid));
       +        else
       +                ltk_recalculate_grid(LTK_CAST_WIDGET(grid));
       +        if (widget->lrect.w != orig_w || widget->lrect.h != orig_h)
       +                ltk_widget_resize(widget);
       +}
       +
       +/* FIXME: Check if widget already exists at position */
       +int
       +ltk_grid_add(
       +        ltk_grid *grid, ltk_widget *widget,
       +        int row, int column, int row_span, int column_span,
       +        ltk_sticky_mask sticky
       +) {
       +        if (widget->parent)
       +                return 1;
       +        /* FIXME: decide which checks should be asserts and which should be error returns */
       +        /* the client-server version of ltk shouldn't abort on errors like these */
       +        ltk_assert(row >= 0 && row + row_span <= grid->rows);
       +        ltk_assert(column >= 0 && column + column_span <= grid->columns);
       +
       +        widget->sticky = sticky;
       +        widget->row = row;
       +        widget->column = column;
       +        widget->row_span = row_span;
       +        widget->column_span = column_span;
       +        for (int i = row; i < row + row_span; i++) {
       +                for (int j = column; j < column + column_span; j++) {
       +                        grid->widget_grid[i * grid->columns + j] = widget;
       +                }
       +        }
       +        widget->parent = LTK_CAST_WIDGET(grid);
       +        ltk_grid_child_size_change(LTK_CAST_WIDGET(grid), widget);
       +        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
       +
       +        return 0;
       +}
       +
       +int
       +ltk_grid_remove(ltk_grid *grid, ltk_widget *widget) {
       +        if (widget->parent != LTK_CAST_WIDGET(grid))
       +                return 1;
       +        widget->parent = NULL;
       +        for (int i = widget->row; i < widget->row + widget->row_span; i++) {
       +                for (int j = widget->column; j < widget->column + widget->column_span; j++) {
       +                        grid->widget_grid[i * grid->columns + j] = NULL;
       +                }
       +        }
       +        ltk_window_invalidate_widget_rect(LTK_CAST_WIDGET(grid)->window, LTK_CAST_WIDGET(grid));
       +
       +        return 0;
       +}
       +
       +static int
       +ltk_grid_remove_child(ltk_widget *self, ltk_widget *widget) {
       +        return ltk_grid_remove(LTK_CAST_GRID(self), widget);
       +}
       +
       +static int
       +ltk_grid_find_nearest_column(ltk_grid *grid, int x) {
       +        int i;
       +        for (i = 0; i < grid->columns; i++) {
       +                if (grid->column_pos[i] <= x && grid->column_pos[i + 1] >= x) {
       +                        return i;
       +                }
       +        }
       +        return -1;
       +}
       +
       +static int
       +ltk_grid_find_nearest_row(ltk_grid *grid, int y) {
       +        int i;
       +        for (i = 0; i < grid->rows; i++) {
       +                if (grid->row_pos[i] <= y && grid->row_pos[i + 1] >= y) {
       +                        return i;
       +                }
       +        }
       +        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_CAST_GRID(self);
       +        ltk_widget *minw = NULL;
       +        int min_dist = INT_MAX;
       +        /* 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 */
       +                ltk_rect r = grid->widget_grid[i]->lrect;
       +                int dist = ltk_rect_fakedist(rect, r);
       +                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_CAST_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_CAST_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;
       +}
       +
       +/* FIXME: maybe these should also fall back to widgets in other columns if those
       +   exist but no widgets exist in the same column */
       +static ltk_widget *
       +ltk_grid_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
       +        ltk_grid *grid = LTK_CAST_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_CAST_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_CAST_GRID(self);
       +        int row = ltk_grid_find_nearest_row(grid, y);
       +        int column = ltk_grid_find_nearest_column(grid, x);
       +        if (row == -1 || column == -1)
       +                return 0;
       +        ltk_widget *ptr = grid->widget_grid[row * grid->columns + column];
       +        if (ptr && ltk_collide_rect(ptr->crect, x, y))
       +                return ptr;
       +        return NULL;
       +}
       +
       +static ltk_widget *
       +ltk_grid_prev_child(ltk_widget *self, ltk_widget *child) {
       +        ltk_grid *grid = LTK_CAST_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_CAST_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_CAST_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_CAST_GRID(self);
       +        for (unsigned int i = grid->rows * grid->columns; i-- > 0;) {
       +                if (grid->widget_grid[i])
       +                        return grid->widget_grid[i];
       +        }
       +        return NULL;
       +}
   DIR diff --git a/src/grid.h b/src/ltk/grid.h
   DIR diff --git a/src/image.h b/src/ltk/image.h
   DIR diff --git a/src/image_imlib.c b/src/ltk/image_imlib.c
   DIR diff --git a/src/image_widget.c b/src/ltk/image_widget.c
   DIR diff --git a/src/image_widget.h b/src/ltk/image_widget.h
   DIR diff --git a/src/ini.c b/src/ltk/ini.c
   DIR diff --git a/src/ini.h b/src/ltk/ini.h
   DIR diff --git a/src/keys.h b/src/ltk/keys.h
   DIR diff --git a/src/label.c b/src/ltk/label.c
   DIR diff --git a/src/label.h b/src/ltk/label.h
   DIR diff --git a/src/ltk/ltk.c b/src/ltk/ltk.c
       t@@ -0,0 +1,667 @@
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <locale.h>
       +#include <pwd.h>
       +#include <stdint.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <time.h>
       +#include <unistd.h>
       +
       +#include <sys/wait.h>
       +
       +#include "ltk.h"
       +#include "array.h"
       +#include "button.h"
       +#include "config.h"
       +#include "entry.h"
       +#include "event.h"
       +#include "eventdefs.h"
       +#include "graphics.h"
       +#include "image.h"
       +#include "ini.h"
       +#include "label.h"
       +#include "macros.h"
       +#include "memory.h"
       +#include "menu.h"
       +#include "rect.h"
       +#include "scrollbar.h"
       +#include "text.h"
       +#include "util.h"
       +#include "widget.h"
       +
       +#define MAX_WINDOW_FONT_SIZE 200
       +
       +typedef struct {
       +        char *tmpfile;
       +        ltk_widget *caller;
       +        int pid;
       +} ltk_cmdinfo;
       +
       +LTK_ARRAY_INIT_DECL_STATIC(window, ltk_window *)
       +LTK_ARRAY_INIT_IMPL_STATIC(window, ltk_window *)
       +LTK_ARRAY_INIT_DECL_STATIC(rwindow, ltk_renderwindow *)
       +LTK_ARRAY_INIT_IMPL_STATIC(rwindow, ltk_renderwindow *)
       +LTK_ARRAY_INIT_DECL_STATIC(cmd, ltk_cmdinfo)
       +LTK_ARRAY_INIT_IMPL_STATIC(cmd, ltk_cmdinfo)
       +
       +static struct {
       +        ltk_renderdata *renderdata;
       +        ltk_text_context *text_context;
       +        ltk_clipboard *clipboard;
       +        ltk_array(window) *windows;
       +        ltk_array(rwindow) *rwindows;
       +        /* PID of external command called e.g. by text widget to edit text.
       +           ON exit, cmd_caller->vtable->cmd_return is called with the text
       +           the external command wrote to a file. */
       +        /*IMPORTANT: this needs to be checked whenever a widget is destroyed!
       +        FIXME: allow option to instead return output of command */
       +        ltk_array(cmd) *cmds;
       +        size_t cur_kbd;
       +} shared_data = {NULL, NULL, NULL, NULL, NULL, NULL, 0};
       +
       +typedef struct {
       +        void (*callback)(ltk_callback_arg data);
       +        ltk_callback_arg data;
       +        struct timespec repeat;
       +        struct timespec remaining;
       +        int id;
       +} ltk_timer;
       +
       +static ltk_timer *timers = NULL;
       +static size_t timers_num = 0;
       +static size_t timers_alloc = 0;
       +
       +static void ltk_handle_event(ltk_event *event);
       +static void ltk_load_theme(const char *path);
       +static void ltk_uninitialize_theme(void);
       +static int ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value);
       +static int handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b);
       +static int handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b);
       +
       +static short running = 1;
       +
       +typedef struct {
       +        char *name;
       +        int (*ini_handler)(ltk_renderdata *, const char *, const char *);
       +        int (*fill_theme_defaults)(ltk_renderdata *);
       +        void (*uninitialize_theme)(ltk_renderdata *);
       +        int (*register_keypress)(const char *, size_t, ltk_keypress_binding);
       +        int (*register_keyrelease)(const char *, size_t, ltk_keyrelease_binding);
       +        void (*cleanup)(void);
       +} ltk_widget_funcs;
       +
       +/* FIXME: use binary search when searching for the widget */
       +static ltk_widget_funcs widget_funcs[] = {
       +        {
       +                .name = "box",
       +                .ini_handler = NULL,
       +                .fill_theme_defaults = NULL,
       +                .uninitialize_theme = NULL,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "button",
       +                .ini_handler = &ltk_button_ini_handler,
       +                .fill_theme_defaults = &ltk_button_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_button_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "entry",
       +                .ini_handler = &ltk_entry_ini_handler,
       +                .fill_theme_defaults = &ltk_entry_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_entry_uninitialize_theme,
       +                .register_keypress = &ltk_entry_register_keypress,
       +                .register_keyrelease = &ltk_entry_register_keyrelease,
       +                .cleanup = &ltk_entry_cleanup,
       +        },
       +        {
       +                .name = "grid",
       +                .ini_handler = NULL,
       +                .fill_theme_defaults = NULL,
       +                .uninitialize_theme = NULL,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "label",
       +                .ini_handler = &ltk_label_ini_handler,
       +                .fill_theme_defaults = &ltk_label_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_label_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                /* FIXME: this is actually image_widget */
       +                .name = "image",
       +                .ini_handler = NULL,
       +                .fill_theme_defaults = NULL,
       +                .uninitialize_theme = NULL,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "menu",
       +                .ini_handler = &ltk_menu_ini_handler,
       +                .fill_theme_defaults = &ltk_menu_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_menu_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "menuentry",
       +                .ini_handler = &ltk_menuentry_ini_handler,
       +                .fill_theme_defaults = &ltk_menuentry_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_menuentry_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "submenu",
       +                .ini_handler = &ltk_submenu_ini_handler,
       +                .fill_theme_defaults = &ltk_submenu_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_submenu_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                .name = "submenuentry",
       +                .ini_handler = &ltk_submenuentry_ini_handler,
       +                .fill_theme_defaults = &ltk_submenuentry_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_submenuentry_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +                 /*
       +                 This "widget" is only needed to have separate styles for regular
       +                   menu entries and submenu entries. "submenu" is just an alias for
       +                   "menu" in most cases - it's just needed when creating a menu to
       +                   decide if it's a submenu or not.
       +                   FIXME: is that even necessary? Why can't it just decide if it's
       +                   a submenu based on whether it has a parent or not?
       +                   -> I guess right-click menus are also just submenus, so they
       +                   need to set it explicitly, but wasn't there another reason? 
       +                 */
       +        },
       +        {
       +                .name = "scrollbar",
       +                .ini_handler = &ltk_scrollbar_ini_handler,
       +                .fill_theme_defaults = &ltk_scrollbar_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_scrollbar_uninitialize_theme,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                /* Handler for general widget key bindings. */
       +                .name = "widget",
       +                .ini_handler = NULL,
       +                .fill_theme_defaults = NULL,
       +                .uninitialize_theme = NULL,
       +                .register_keypress = NULL,
       +                .register_keyrelease = NULL,
       +                .cleanup = NULL,
       +        },
       +        {
       +                /* Handler for window theme. */
       +                .name = "window",
       +                .ini_handler = &ltk_window_ini_handler,
       +                .fill_theme_defaults = &ltk_window_fill_theme_defaults,
       +                .uninitialize_theme = &ltk_window_uninitialize_theme,
       +                .register_keypress = &ltk_window_register_keypress,
       +                .register_keyrelease = &ltk_window_register_keyrelease,
       +                .cleanup = &ltk_window_cleanup,
       +        }
       +};
       +
       +ltk_renderdata *
       +ltk_get_renderer(void) {
       +        /* FIXME: check if initialized? */
       +        return shared_data.renderdata;
       +}
       +
       +int
       +ltk_init(void) {
       +        /* FIXME: should ltk set this? probably not */
       +        setlocale(LC_CTYPE, "");
       +        char *ltk_dir = ltk_setup_directory("LTKDIR", ".ltk", 0);
       +        if (!ltk_dir)
       +                ltk_fatal_errno("Unable to setup ltk directory.\n");
       +        shared_data.cur_kbd = 0;
       +
       +        /* FIXME: search different directories for config */
       +        /* FIXME: don't print error if config or theme file doesn't exist */
       +        char *config_path = ltk_strcat_useful(ltk_dir, "/ltk.cfg");
       +        char *theme_path;
       +        char *errstr = NULL;
       +        if (ltk_config_parsefile(config_path, &handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
       +                if (errstr) {
       +                        ltk_warn("Unable to load config: %s\n", errstr);
       +                        ltk_free0(errstr);
       +                }
       +                if (ltk_config_load_default(&handle_keypress_binding, &handle_keyrelease_binding, &errstr)) {
       +                        /* FIXME: I guess errstr isn't freed here, but whatever */
       +                        /* FIXME: return error instead of dying */
       +                        ltk_fatal("Unable to load default config: %s\n", errstr);
       +                }
       +        }
       +        ltk_free0(config_path);
       +        theme_path = ltk_strcat_useful(ltk_dir, "/theme.ini");
       +        ltk_free0(ltk_dir);
       +        shared_data.renderdata = ltk_renderer_create();
       +        if (!shared_data.renderdata)
       +                return 1; /* FIXME: clean up */
       +        ltk_load_theme(theme_path);
       +        ltk_free0(theme_path);
       +        /* FIXME: maybe "general" theme instead of window theme? */
       +        ltk_window_theme *window_theme = ltk_window_get_theme();
       +        shared_data.text_context = ltk_text_context_create(shared_data.renderdata, window_theme->font);
       +        shared_data.clipboard = ltk_clipboard_create(shared_data.renderdata);
       +        /* FIXME: configure cache size; check for overflow */
       +        ltk_image_init(shared_data.renderdata, 1024 * 1024 * 4);
       +        shared_data.windows = ltk_array_create(window, 1);
       +        shared_data.rwindows = ltk_array_create(rwindow, 1);
       +        shared_data.cmds = ltk_array_create(cmd, 1);
       +        return 0; /* FIXME: or maybe 1? */
       +}
       +
       +static struct {
       +        struct timespec last;
       +        struct timespec lasttimer;
       +} mainloop_data;
       +
       +void
       +ltk_mainloop_init(void) {
       +        clock_gettime(CLOCK_MONOTONIC, &mainloop_data.last);
       +        mainloop_data.lasttimer = mainloop_data.last;
       +
       +        /* initialize keyboard mapping */
       +        ltk_event event;
       +        ltk_generate_keyboard_event(shared_data.renderdata, &event);
       +        ltk_handle_event(&event);
       +}
       +
       +/* FIXME: maybe split this up into multiple stages */
       +void
       +ltk_mainloop_step(int limit_framerate) {
       +        ltk_event event;
       +
       +        /* FIXME: make time management smarter - maybe always figure out how long
       +           it will take until the next timer is due and then sleep if no other events
       +           are happening (would need separate parameter to turn that off when a
       +           different mainloop is used) */
       +        struct timespec now, elapsed, sleep_time;
       +        sleep_time.tv_sec = 0;
       +
       +        int pid = -1;
       +        int wstatus = 0;
       +        /* FIXME: kill all children on exit? */
       +        if ((pid = waitpid(-1, &wstatus, WNOHANG)) > 0) {
       +                ltk_cmdinfo *info;
       +                /* FIXME: should commands be split into read/write and block write commands during external editing? */
       +                for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
       +                        info = &(ltk_array_get(shared_data.cmds, i));
       +                        if (info->pid == pid) {
       +                                if (!info->caller) {
       +                                        ltk_warn("Widget disappeared while text was being edited in external program\n");
       +                                /* FIXME: call overwritten cmd_return! */
       +                                } else if (info->caller->vtable->cmd_return) {
       +                                        size_t file_len = 0;
       +                                        char *errstr = NULL;
       +                                        char *contents = ltk_read_file(info->tmpfile, &file_len, &errstr);
       +                                        if (!contents) {
       +                                                ltk_warn("Unable to read file '%s' written by external command: %s\n", info->tmpfile, errstr);
       +                                        } else {
       +                                                info->caller->vtable->cmd_return(info->caller, contents, file_len);
       +                                                ltk_free0(contents);
       +                                        }
       +                                }
       +                                ltk_free0(info->tmpfile);
       +                                ltk_array_delete(cmd, shared_data.cmds, i, 1);
       +                                break;
       +                        }
       +                }
       +        }
       +        while (!ltk_next_event(
       +            shared_data.renderdata,
       +            ltk_array_get_buf(shared_data.rwindows),
       +            ltk_array_len(shared_data.rwindows),
       +            shared_data.clipboard, shared_data.cur_kbd, &event)) {
       +                ltk_handle_event(&event);
       +        }
       +
       +        clock_gettime(CLOCK_MONOTONIC, &now);
       +        ltk_timespecsub(&now, &mainloop_data.lasttimer, &elapsed);
       +        /* Note: it should be safe to give the same pointer as the first and
       +           last argument, as long as ltk_timespecsub/add isn't changed incompatibly */
       +        size_t i = 0;
       +        while (i < timers_num) {
       +                ltk_timespecsub(&timers[i].remaining, &elapsed, &timers[i].remaining);
       +                if (timers[i].remaining.tv_sec < 0 ||
       +                    (timers[i].remaining.tv_sec == 0 && timers[i].remaining.tv_nsec == 0)) {
       +                        timers[i].callback(timers[i].data);
       +                        if (timers[i].repeat.tv_sec == 0 && timers[i].repeat.tv_nsec == 0) {
       +                                /* remove timer because it has no repeat */
       +                                memmove(timers + i, timers + i + 1, sizeof(ltk_timer) * (timers_num - i - 1));
       +                        } else {
       +                                ltk_timespecadd(&timers[i].remaining, &timers[i].repeat, &timers[i].remaining);
       +                                i++;
       +                        }
       +                } else {
       +                        i++;
       +                }
       +        }
       +        mainloop_data.lasttimer = now;
       +
       +        for (size_t i = 0; i < shared_data.windows->len; i++) {
       +                ltk_window *window = shared_data.windows->buf[i];
       +                if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
       +                        ltk_widget_draw(LTK_CAST_WIDGET(window), NULL, 0, 0, (ltk_rect){0, 0, 0, 0});
       +                }
       +        }
       +
       +        if (limit_framerate) {
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &mainloop_data.last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                mainloop_data.last = now;
       +        }
       +}
       +
       +void
       +ltk_mainloop_quit(void) {
       +        /* FIXME: maybe prevent other events from running? */
       +        running = 0;
       +}
       +
       +void
       +ltk_mainloop_restartable(void) {
       +        ltk_mainloop_init();
       +        while (running) {
       +                ltk_mainloop_step(1);
       +        }
       +}
       +
       +void
       +ltk_mainloop(void) {
       +        ltk_mainloop_restartable();
       +        ltk_deinit();
       +}
       +
       +void
       +ltk_deinit(void) {
       +        if (running)
       +                return;
       +        if (shared_data.cmds) {
       +                for (size_t i = 0; i < ltk_array_len(shared_data.cmds); i++) {
       +                        /* FIXME: maybe kill child processes? */
       +                        ltk_free((ltk_array_get(shared_data.cmds, i)).tmpfile);
       +                }
       +                ltk_array_destroy(cmd, shared_data.cmds);
       +        }
       +        shared_data.cmds = NULL;
       +        if (shared_data.windows) {
       +                for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
       +                        ltk_window *window = ltk_array_get(shared_data.windows, i);
       +                        ltk_widget_destroy(LTK_CAST_WIDGET(window), 0);
       +                }
       +                ltk_array_destroy(window, shared_data.windows);
       +        }
       +        shared_data.windows = NULL;
       +        if (shared_data.rwindows)
       +                ltk_array_destroy(rwindow, shared_data.rwindows);
       +        shared_data.rwindows = NULL;
       +        ltk_config_cleanup();
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (widget_funcs[i].cleanup)
       +                        widget_funcs[i].cleanup();
       +        }
       +        if (shared_data.text_context)
       +                ltk_text_context_destroy(shared_data.text_context);
       +        shared_data.text_context = NULL;
       +        if (shared_data.clipboard)
       +                ltk_clipboard_destroy(shared_data.clipboard);
       +        shared_data.clipboard = NULL;
       +        ltk_events_cleanup();
       +        if (shared_data.renderdata) {
       +                ltk_uninitialize_theme();
       +                ltk_renderer_destroy(shared_data.renderdata);
       +        }
       +        shared_data.renderdata = NULL;
       +}
       +
       +/* FIXME: check everywhere if initialized already */
       +ltk_window *
       +ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h) {
       +        /* FIXME: more asserts, or maybe global "initialized" flag */
       +        ltk_assert(shared_data.renderdata != NULL);
       +        ltk_assert(shared_data.windows != NULL);
       +        ltk_assert(shared_data.rwindows != NULL);
       +        ltk_window *window = ltk_window_create_intern(shared_data.renderdata, title, x, y, w, h);
       +        ltk_array_append(window, shared_data.windows, window);
       +        ltk_array_append(rwindow, shared_data.rwindows, window->renderwindow);
       +        return window;
       +}
       +
       +void
       +ltk_window_destroy(ltk_widget *self, int shallow) {
       +        /* FIXME: would it make sense to do something with 'shallow' here? */
       +        (void)shallow;
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        for (size_t i = 0; i < ltk_array_len(shared_data.windows); i++) {
       +                if (ltk_array_get(shared_data.windows, i) == window) {
       +                        ltk_array_delete(window, shared_data.windows, i, 1);
       +                        ltk_array_delete(rwindow, shared_data.rwindows, i, 1);
       +                        break;
       +                }
       +        }
       +        ltk_window_destroy_intern(window);
       +}
       +
       +ltk_clipboard *
       +ltk_get_clipboard(void) {
       +        /* FIXME: what to do when not initialized? */
       +        return shared_data.clipboard;
       +}
       +
       +/* FIXME: optimize timer handling - maybe also a sort of priority queue */
       +/* FIXME: JUST USE A GENERIC DYNAMIC ARRAY ALREADY!!!!! */
       +void
       +ltk_unregister_timer(int timer_id) {
       +        for (size_t i = 0; i < timers_num; i++) {
       +                if (timers[i].id == timer_id) {
       +                        memmove(
       +                            timers + i,
       +                            timers + i + 1,
       +                            sizeof(ltk_timer) * (timers_num - i - 1)
       +                        );
       +                        timers_num--;
       +                        size_t sz = ideal_array_size(timers_alloc, timers_num);
       +                        if (sz != timers_alloc) {
       +                                timers_alloc = sz;
       +                                timers = ltk_reallocarray(
       +                                    timers, sz, sizeof(ltk_timer)
       +                                );
       +                        }
       +                        return;
       +                }
       +        }
       +}
       +
       +/* repeat <= 0 means no repeat, first <= 0 means run as soon as possible */
       +int
       +ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data) {
       +        if (first < 0)
       +                first = 0;
       +        if (repeat < 0)
       +                repeat = 0;
       +        if (timers_num == timers_alloc) {
       +                timers_alloc = ideal_array_size(timers_alloc, timers_num + 1);
       +                timers = ltk_reallocarray(
       +                    timers, timers_alloc, sizeof(ltk_timer)
       +                );
       +        }
       +        /* FIXME: better finding of id */
       +        /* FIXME: maybe store sorted by id */
       +        int id = 0;
       +        for (size_t i = 0; i < timers_num; i++) {
       +                if (timers[i].id >= id)
       +                        id = timers[i].id + 1;
       +        }
       +        ltk_timer *t = &timers[timers_num++];
       +        t->callback = callback;
       +        t->data = data;
       +        t->repeat.tv_sec = repeat / 1000;
       +        t->repeat.tv_nsec = (repeat % 1000) * 1000;
       +        t->remaining.tv_sec = first / 1000;
       +        t->remaining.tv_nsec = (first % 1000) * 1000;
       +        t->id = id;
       +        return id;
       +}
       +
       +/* FIXME: standardize return codes - usually, 0 is returned on success, but ini.h
       +   uses 1 on success, so this is all a bit confusing */
       +/* FIXME: switch away from ini.h */
       +static int
       +ltk_ini_handler(void *renderdata, const char *widget, const char *prop, const char *value) {
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (widget_funcs[i].ini_handler && !strcmp(widget, widget_funcs[i].name)) {
       +                        widget_funcs[i].ini_handler(renderdata, prop, value);
       +                        return 1;
       +                }
       +        }
       +        return 0;
       +}
       +
       +/* FIXME: don't call ltk_fatal, instead return error from ltk_init */
       +static void
       +ltk_load_theme(const char *path) {
       +        /* FIXME: give line number in error message */
       +        if (ini_parse(path, ltk_ini_handler, shared_data.renderdata) != 0) {
       +                ltk_warn("Unable to load theme.\n");
       +        }
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (widget_funcs[i].fill_theme_defaults) {
       +                        if (widget_funcs[i].fill_theme_defaults(shared_data.renderdata)) {
       +                                ltk_uninitialize_theme();
       +                                ltk_fatal("Unable to load theme defaults.\n");
       +                        }
       +                }
       +        }
       +}
       +
       +static void
       +ltk_uninitialize_theme(void) {
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (widget_funcs[i].uninitialize_theme)
       +                        widget_funcs[i].uninitialize_theme(shared_data.renderdata);
       +        }
       +}
       +
       +static int
       +handle_keypress_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keypress_binding b) {
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
       +                        if (!widget_funcs[i].register_keypress)
       +                                return 1;
       +                        return widget_funcs[i].register_keypress(name, nlen, b);
       +                }
       +        }
       +        return 1;
       +}
       +
       +static int
       +handle_keyrelease_binding(const char *widget_name, size_t wlen, const char *name, size_t nlen, ltk_keyrelease_binding b) {
       +        for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                if (str_array_equal(widget_funcs[i].name, widget_name, wlen)) {
       +                        if (!widget_funcs[i].register_keyrelease)
       +                                return 1;
       +                        return widget_funcs[i].register_keyrelease(name, nlen, b);
       +                }
       +        }
       +        return 1;
       +}
       +
       +int
       +ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen) {
       +        /* FIXME: support environment variable $TMPDIR */
       +        ltk_cmdinfo info = {NULL, NULL, -1};
       +        info.tmpfile = ltk_strdup("/tmp/ltk.XXXXXX");
       +        int fd = mkstemp(info.tmpfile);
       +        if (fd == -1) {
       +                ltk_warn_errno("Unable to create temporary file while trying to run command '%.*s'\n", (int)cmdlen, cmd);
       +                ltk_free0(info.tmpfile);
       +                return 1;
       +        }
       +        close(fd);
       +        /* FIXME: give file descriptor directly to modified version of ltk_write_file */
       +        char *errstr = NULL;
       +        if (ltk_write_file(info.tmpfile, text, textlen, &errstr)) {
       +                ltk_warn("Unable to write to file '%s' while trying to run command '%.*s': %s\n", info.tmpfile, (int)cmdlen, cmd, errstr);
       +                unlink(info.tmpfile);
       +                ltk_free0(info.tmpfile);
       +                return 1;
       +        }
       +        int pid = -1;
       +        if ((pid = ltk_parse_run_cmd(cmd, cmdlen, info.tmpfile)) <= 0) {
       +                /* FIXME: errno */
       +                ltk_warn("Unable to run command '%.*s'\n", (int)cmdlen, cmd);
       +                unlink(info.tmpfile);
       +                ltk_free0(info.tmpfile);
       +                return 1;
       +        }
       +        info.pid = pid;
       +        info.caller = caller;
       +        ltk_array_append(cmd, shared_data.cmds, info);
       +        return 0;
       +}
       +
       +static void
       +ltk_handle_event(ltk_event *event) {
       +        size_t kbd_idx;
       +        if (event->type == 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
       +                        shared_data.cur_kbd = kbd_idx;
       +        } else {
       +                if (event->any.window_id < ltk_array_len(shared_data.windows)) {
       +                        ltk_window_handle_event(ltk_array_get(shared_data.windows, event->any.window_id), event);
       +                }
       +        }
       +}
       +
       +ltk_text_line *
       +ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width) {
       +        return ltk_text_line_create(shared_data.text_context, font_size, text, take_over_text, width);
       +}
   DIR diff --git a/src/ltk/ltk.h b/src/ltk/ltk.h
       t@@ -0,0 +1,58 @@
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTK_H
       +#define LTK_H
       +
       +#include <stddef.h>
       +#include <stdint.h>
       +
       +#include "clipboard.h"
       +#include "widget.h"
       +#include "window.h"
       +#include "text.h"
       +
       +int ltk_init(void);
       +void ltk_deinit(void);
       +
       +void ltk_mainloop_init(void);
       +void ltk_mainloop_step(int limit_framerate);
       +
       +void ltk_mainloop(void);
       +/* FIXME: maybe better name */
       +void ltk_mainloop_restartable(void);
       +void ltk_mainloop_quit(void);
       +
       +void ltk_unregister_timer(int timer_id);
       +int ltk_register_timer(long first, long repeat, void (*callback)(ltk_callback_arg data), ltk_callback_arg data);
       +
       +/* These are here so they can be added to the global array in ltk.c */
       +ltk_window *ltk_window_create(const char *title, int x, int y, unsigned int w, unsigned int h);
       +void ltk_window_destroy(ltk_widget *self, int shallow);
       +
       +/* FIXME: allow piping text instead of writing to temporary file */
       +/* FIXME: how to avoid bad things happening while external program open? maybe store cmd widget somewhere (but could be multiple!) and check if widget to destroy is one of those 
       +-> alternative: store all widgets in array and only give out IDs, then when returning from cmd, widget is already destroyed and can be ignored
       +-> first option maybe just set callback, etc. of current cmd to NULL so widget can still be destroyed */
       +int ltk_call_cmd(ltk_widget *caller, const char *cmd, size_t cmdlen, const char *text, size_t textlen);
       +
       +/* convenience function to use the default text context */
       +ltk_text_line *ltk_text_line_create_default(uint16_t font_size, char *text, int take_over_text, int width);
       +
       +ltk_clipboard *ltk_get_clipboard(void);
       +ltk_renderdata *ltk_get_renderer(void);
       +
       +#endif /* LTK_H */
   DIR diff --git a/src/macros.h b/src/ltk/macros.h
   DIR diff --git a/src/memory.c b/src/ltk/memory.c
   DIR diff --git a/src/memory.h b/src/ltk/memory.h
   DIR diff --git a/src/menu.c b/src/ltk/menu.c
   DIR diff --git a/src/menu.h b/src/ltk/menu.h
   DIR diff --git a/src/rect.c b/src/ltk/rect.c
   DIR diff --git a/src/rect.h b/src/ltk/rect.h
   DIR diff --git a/src/scrollbar.c b/src/ltk/scrollbar.c
   DIR diff --git a/src/scrollbar.h b/src/ltk/scrollbar.h
   DIR diff --git a/src/stb_truetype.c b/src/ltk/stb_truetype.c
   DIR diff --git a/src/stb_truetype.h b/src/ltk/stb_truetype.h
   DIR diff --git a/src/strtonum.c b/src/ltk/strtonum.c
   DIR diff --git a/src/surface_cache.c b/src/ltk/surface_cache.c
   DIR diff --git a/src/surface_cache.h b/src/ltk/surface_cache.h
   DIR diff --git a/src/text.h b/src/ltk/text.h
   DIR diff --git a/src/text_pango.c b/src/ltk/text_pango.c
   DIR diff --git a/src/text_stb.c b/src/ltk/text_stb.c
   DIR diff --git a/src/theme.c b/src/ltk/theme.c
   DIR diff --git a/src/theme.h b/src/ltk/theme.h
   DIR diff --git a/src/txtbuf.c b/src/ltk/txtbuf.c
   DIR diff --git a/src/txtbuf.h b/src/ltk/txtbuf.h
   DIR diff --git a/src/ltk/util.c b/src/ltk/util.c
       t@@ -0,0 +1,394 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <pwd.h>
       +#include <time.h>
       +#include <ctype.h>
       +#include <errno.h>
       +#include <stdio.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <stdarg.h>
       +#include <unistd.h>
       +#include <sys/stat.h>
       +
       +#include "ltk.h"
       +#include "util.h"
       +#include "array.h"
       +#include "memory.h"
       +#include "txtbuf.h"
       +
       +/* FIXME: Should these functions really fail on memory error? */
       +
       +char *
       +ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret) {
       +        long len;
       +        char *file_contents;
       +        FILE *file;
       +
       +        /* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
       +        file = fopen(filename, "r");
       +        if (!file) goto error;
       +        if (fseek(file, 0, SEEK_END)) goto errorclose;
       +        len = ftell(file);
       +        if (len < 0) goto errorclose;
       +        if (fseek(file, 0, SEEK_SET)) goto errorclose;
       +        file_contents = ltk_malloc((size_t)len + 1);
       +        clearerr(file);
       +        fread(file_contents, 1, (size_t)len, file);
       +        if (ferror(file)) goto errorclose;
       +        file_contents[len] = '\0';
       +        if (fclose(file)) goto error;
       +        *len_ret = (size_t)len;
       +        return file_contents;
       +error:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        return NULL;
       +errorclose:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        fclose(file);
       +        return NULL;
       +}
       +
       +/* FIXME: not sure if errno actually is set usefully after all these functions */
       +int
       +ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret) {
       +        FILE *file = fopen(path, "w");
       +        if (!file) goto error;
       +        clearerr(file);
       +        if (fwrite(data, 1, len, file) < len) goto errorclose;
       +        if (fclose(file)) goto error;
       +        return 0;
       +error:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        return 1;
       +errorclose:
       +        if (errstr_ret)
       +                *errstr_ret = strerror(errno);
       +        fclose(file);
       +        return 1;
       +}
       +
       +/* FIXME: maybe have a few standard array types defined somewhere else */
       +LTK_ARRAY_INIT_DECL_STATIC(cmd, char *)
       +LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *)
       +
       +static void
       +free_helper(char *ptr) {
       +        ltk_free(ptr);
       +}
       +
       +/* FIXME: this is really ugly */
       +/* FIXME: parse command only once in beginning instead of each time it is run? */
       +/* FIXME: this handles double-quote, but the config parser already uses that, so
       +   it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
       +int
       +ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) {
       +        int bs = 0;
       +        int in_sqstr = 0;
       +        int in_dqstr = 0;
       +        int in_ws = 1;
       +        char c;
       +        size_t cur_start = 0;
       +        int offset = 0;
       +        txtbuf *cur_arg = txtbuf_new();
       +        ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
       +        char *cmdcopy = ltk_strndup(cmdtext, len);
       +        for (size_t i = 0; i < len; i++) {
       +                c = cmdcopy[i];
       +                if (c == '\\') {
       +                        if (bs) {
       +                                offset++;
       +                                bs = 0;
       +                        } else {
       +                                bs = 1;
       +                        }
       +                } else if (isspace(c)) {
       +                        if (!in_sqstr && !in_dqstr) {
       +                                if (bs) {
       +                                        if (in_ws) {
       +                                                in_ws = 0;
       +                                                cur_start = i;
       +                                                offset = 0;
       +                                        } else {
       +                                                offset++;
       +                                        }
       +                                        bs = 0;
       +                                } else if (!in_ws) {
       +                                        /* FIXME: shouldn't this be < instead of <=? */
       +                                        if (cur_start <= i - offset)
       +                                                txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       +                                        /* FIXME: cmd is named horribly */
       +                                        ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       +                                        txtbuf_clear(cur_arg);
       +                                        in_ws = 1;
       +                                        offset = 0;
       +                                }
       +                        /* FIXME: parsing weird here - bs just ignored */
       +                        } else if (bs) {
       +                                bs = 0;
       +                        }
       +                } else if (c == '%') {
       +                        if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') {
       +                                if (!in_ws && cur_start < i - offset)
       +                                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       +                                txtbuf_append(cur_arg, filename);
       +                                i++;
       +                                cur_start = i + 1;
       +                                offset = 0;
       +                        } else if (in_ws) {
       +                                cur_start = i;
       +                                offset = 0;
       +                        }
       +                        in_ws = 0;
       +                } else if (c == '"') {
       +                        if (in_sqstr) {
       +                                bs = 0;
       +                        } else if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (in_dqstr) {
       +                                offset++;
       +                                in_dqstr = 0;
       +                                continue;
       +                        } else {
       +                                in_dqstr = 1;
       +                                if (in_ws) {
       +                                        cur_start = i + 1;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                        continue;
       +                                }
       +                        }
       +                        in_ws = 0;
       +                } else if (c == '\'') {
       +                        if (in_dqstr) {
       +                                bs = 0;
       +                        } else if (bs) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                                bs = 0;
       +                        } else if (in_sqstr) {
       +                                offset++;
       +                                in_sqstr = 0;
       +                                continue;
       +                        } else {
       +                                in_sqstr = 1;
       +                                if (in_ws) {
       +                                        cur_start = i + 1;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                        continue;
       +                                }
       +                        }
       +                        in_ws = 0;
       +                } else if (bs) {
       +                        if (!in_sqstr && !in_dqstr) {
       +                                if (in_ws) {
       +                                        cur_start = i;
       +                                        offset = 0;
       +                                } else {
       +                                        offset++;
       +                                }
       +                        }
       +                        bs = 0;
       +                        in_ws = 0;
       +                } else {
       +                        if (in_ws) {
       +                                cur_start = i;
       +                                offset = 0;
       +                        }
       +                        in_ws = 0;
       +                }
       +                cmdcopy[i - offset] = cmdcopy[i];
       +        }
       +        if (in_sqstr || in_dqstr) {
       +                ltk_warn("Unterminated string in command\n");
       +                goto error;
       +        }
       +        if (!in_ws) {
       +                if (cur_start <= len - offset)
       +                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset);
       +                ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       +        }
       +        if (cmd->len == 0) {
       +                ltk_warn("Empty command\n");
       +                goto error;
       +        }
       +        ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */
       +        int fret = -1;
       +        if ((fret = fork()) < 0) {
       +                ltk_warn("Unable to fork\n");
       +                goto error;
       +        } else if (fret == 0) {
       +                if (execvp(cmd->buf[0], cmd->buf) == -1) {
       +                        /* FIXME: what to do on error here? */
       +                        exit(1);
       +                }
       +        } else {
       +                ltk_free(cmdcopy);
       +                txtbuf_destroy(cur_arg);
       +                ltk_array_destroy_deep(cmd, cmd, &free_helper);
       +                return fret;
       +        }
       +error:
       +        ltk_free(cmdcopy);
       +        txtbuf_destroy(cur_arg);
       +        ltk_array_destroy_deep(cmd, cmd, &free_helper);
       +        return -1;
       +}
       +
       +/* If `needed` is larger than `*alloc_size`, resize `*str` to
       +   `max(needed, *alloc_size * 2)`. Aborts program on error. */
       +void
       +ltk_grow_string(char **str, int *alloc_size, int needed) {
       +        if (needed <= *alloc_size) return;
       +        int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2);
       +        char *new = ltk_realloc(*str, new_size);
       +        *str = new;
       +        *alloc_size = new_size;
       +}
       +
       +/* Get the directory to store ltk files in and optionally create it if it
       +   doesn't exist yet and `create` is set.
       +   This first checks the environment variable `env` and, if that doesn't
       +   exist, the home directory with "/" and `default` appended.
       +   Returns NULL on error. */
       +char *
       +ltk_setup_directory(const char *envname, const char *defaultname, int create) {
       +        char *dir, *dir_orig;
       +        struct passwd *pw;
       +        uid_t uid;
       +
       +        dir_orig = getenv(envname);
       +        if (dir_orig) {
       +                dir = ltk_strdup(dir_orig);
       +        } else {
       +                uid = getuid();
       +                pw = getpwuid(uid);
       +                if (!pw)
       +                        return NULL;
       +                size_t len = strlen(pw->pw_dir);
       +                size_t dlen = strlen(defaultname);
       +                dir = ltk_malloc(len + dlen + 2);
       +                strcpy(dir, pw->pw_dir);
       +                dir[len] = '/';
       +                strcpy(dir + len + 1, defaultname);
       +        }
       +
       +        if (create && mkdir(dir, 0700) < 0) {
       +                if (errno != EEXIST)
       +                        return NULL;
       +        }
       +
       +        return dir;
       +}
       +
       +/* Concatenate the two given strings and return the result.
       +   This allocates new memory for the result string, unlike
       +   the actual strcat. Aborts program on error */
       +char *
       +ltk_strcat_useful(const char *str1, const char *str2) {
       +        int len1, len2;
       +        char *ret;
       +
       +        len1 = strlen(str1);
       +        len2 = strlen(str2);
       +        ret = ltk_malloc(len1 + len2 + 1);
       +        strcpy(ret, str1);
       +        strcpy(ret + len1, str2);
       +
       +        return ret;
       +}
       +
       +static void
       +ltk_log_msg(const char *mode, const char *format, va_list args) {
       +        char logtime[25]; /* FIXME: This should always be big enough, right? */
       +        time_t clock;
       +        struct tm *timeptr;
       +
       +        time(&clock);
       +        timeptr = localtime(&clock);
       +        strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
       +
       +        fprintf(stderr, "%s ltk %s: ", logtime, mode);
       +        vfprintf(stderr, format, args);
       +}
       +
       +LTK_GEN_LOG_FUNCS(ltk, ltk_log_msg, ltk_deinit)
       +
       +int
       +str_array_equal(const char *terminated, const 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;
       +}
       +
       +size_t
       +prev_utf8(char *text, size_t index) {
       +        if (index == 0)
       +                return 0;
       +        size_t i = index - 1;
       +        /* find valid utf8 char - this probably needs to be improved */
       +        while (i > 0 && ((text[i] & 0xC0) == 0x80))
       +                i--;
       +        return i;
       +}
       +
       +size_t
       +next_utf8(char *text, size_t len, size_t index) {
       +        if (index >= len)
       +                return len;
       +        size_t i = index + 1;
       +        while (i < len && ((text[i] & 0xC0) == 0x80))
       +                i++;
       +        return i;
       +}
       +
       +void
       +ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr)
       +{
       +        (void)fprintf(stderr,
       +            "assertion \"%s\" failed: file \"%s\", line %d, function \"%s\"\n",
       +            failedexpr, file, line, func);
       +        abort();
       +        /* NOTREACHED */
       +}
   DIR diff --git a/src/ltk/util.h b/src/ltk/util.h
       t@@ -0,0 +1,100 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTK_UTIL_H
       +#define LTK_UTIL_H
       +
       +#include <stdarg.h>
       +#include <stddef.h>
       +
       +long long ltk_strtonum(
       +    const char *numstr, long long minval,
       +    long long maxval, const char **errstrp
       +);
       +
       +char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret);
       +int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret);
       +int ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename);
       +void ltk_grow_string(char **str, int *alloc_size, int needed);
       +char *ltk_setup_directory(const char *envname, const char *defaultname, int create);
       +char *ltk_strcat_useful(const char *str1, const char *str2);
       +
       +/*
       + * 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(const char *terminated, const char *array, size_t len);
       +
       +size_t prev_utf8(char *text, size_t index);
       +size_t next_utf8(char *text, size_t len, size_t index);
       +
       +/* based on the assert found in OpenBSD */
       +void ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr);
       +#define ltk_assert(e) ((e) ? (void)0 : ltk_assert_impl(__FILE__, __LINE__, __func__, #e))
       +
       +#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
       +
       +#define LTK_GEN_LOG_FUNC_PROTO(prefix)                        \
       +void prefix##_warn_errno(const char *format, ...);        \
       +void prefix##_fatal_errno(const char *format, ...);        \
       +void prefix##_fatal(const char *format, ...);                \
       +void prefix##_warn(const char *format, ...);
       +
       +#define LTK_GEN_LOG_FUNCS(prefix, log_func, cleanup_func)        \
       +void                                                                \
       +prefix##_warn(const char *format, ...) {                        \
       +        va_list args;                                                \
       +        va_start(args, format);                                        \
       +        log_func("Warning", format, args);                        \
       +        va_end(args);                                                \
       +}                                                                \
       +                                                                \
       +void                                                                \
       +prefix##_fatal(const char *format, ...) {                        \
       +        va_list args;                                                \
       +        va_start(args, format);                                        \
       +        log_func("Fatal", format, args);                        \
       +        va_end(args);                                                \
       +        cleanup_func();                                                \
       +                                                                \
       +        exit(1);                                                \
       +}                                                                \
       +                                                                \
       +void                                                                \
       +prefix##_warn_errno(const char *format, ...) {                        \
       +        va_list args;                                                \
       +        char *errstr = strerror(errno);                                \
       +        va_start(args, format);                                        \
       +        log_func("Warning", format, args);                        \
       +        va_end(args);                                                \
       +        prefix##_warn("system error: %s\n", errstr);                \
       +}                                                                \
       +                                                                \
       +void                                                                \
       +prefix##_fatal_errno(const char *format, ...) {                        \
       +        va_list args;                                                \
       +        char *errstr = strerror(errno);                                \
       +        va_start(args, format);                                        \
       +        log_func("Fatal", format, args);                        \
       +        va_end(args);                                                \
       +        prefix##_fatal("system error: %s\n", errstr);                \
       +}
       +
       +LTK_GEN_LOG_FUNC_PROTO(ltk)
       +
       +#endif /* LTK_UTIL_H */
   DIR diff --git a/src/ltk/widget.c b/src/ltk/widget.c
       t@@ -0,0 +1,295 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <string.h>
       +
       +#include "rect.h"
       +#include "widget.h"
       +#include "window.h"
       +#include "memory.h"
       +#include "array.h"
       +
       +LTK_ARRAY_INIT_FUNC_DECL_STATIC(signal, ltk_signal_callback_info)
       +LTK_ARRAY_INIT_IMPL_STATIC(signal, ltk_signal_callback_info)
       +
       +void
       +ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window,
       +    struct ltk_widget_vtable *vtable, int w, int h) {
       +        widget->window = window;
       +        widget->parent = NULL;
       +
       +        /* FIXME: possibly check that draw and destroy aren't NULL */
       +        widget->vtable = vtable;
       +
       +        widget->state = LTK_NORMAL;
       +        widget->row = 0;
       +        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->row = 0;
       +        widget->column = 0;
       +        widget->row_span = 0;
       +        widget->column_span = 0;
       +        widget->sticky = 0;
       +        widget->dirty = 1;
       +        widget->hidden = 0;
       +        widget->vtable_copied = 0;
       +        widget->signal_cbs = NULL;
       +        /* FIXME: null other members! */
       +}
       +
       +void
       +ltk_widget_hide(ltk_widget *widget) {
       +        /* FIXME: it may not make sense to call this here */
       +        if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_HIDE, LTK_EMPTY_ARGLIST))
       +                return;
       +        if (widget->vtable->hide)
       +                widget->vtable->hide(widget);
       +        widget->hidden = 1;
       +        /* remove hover state */
       +        /* FIXME: this needs to call change_state but that might cause issues */
       +        ltk_widget *hover = widget->window->hover_widget;
       +        while (hover) {
       +                if (hover == widget) {
       +                        widget->window->hover_widget->state &= ~LTK_HOVER;
       +                        widget->window->hover_widget = NULL;
       +                        break;
       +                }
       +                hover = hover->parent;
       +        }
       +        ltk_widget *pressed = widget->window->pressed_widget;
       +        while (pressed) {
       +                if (pressed == widget) {
       +                        widget->window->pressed_widget->state &= ~LTK_PRESSED;
       +                        widget->window->pressed_widget = NULL;
       +                        break;
       +                }
       +                pressed = pressed->parent;
       +        }
       +        ltk_widget *active = widget->window->active_widget;
       +        /* if current active widget is child, set active widget to widget above in hierarchy */
       +        int set_next = 0;
       +        while (active) {
       +                if (active == widget) {
       +                        set_next = 1;
       +                /* 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;
       +                }
       +                active = active->parent;
       +        }
       +        if (set_next && !active)
       +                ltk_window_set_active_widget(active->window, NULL);
       +}
       +
       +/* FIXME: Maybe pass the new width as arg here?
       +   That would make a bit more sense */
       +/* FIXME: maybe give global and local position in event */
       +void
       +ltk_widget_resize(ltk_widget *widget) {
       +        if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_RESIZE, LTK_EMPTY_ARGLIST))
       +                return;
       +        if (widget->vtable->resize)
       +                widget->vtable->resize(widget);
       +        widget->dirty = 1;
       +}
       +
       +void
       +ltk_widget_draw(ltk_widget *widget, ltk_surface *draw_surf, int x, int y, ltk_rect clip_rect) {
       +        ltk_callback_arg args[] = {
       +                LTK_MAKE_ARG_SURFACE(draw_surf),
       +                LTK_MAKE_ARG_INT(x),
       +                LTK_MAKE_ARG_INT(y),
       +                LTK_MAKE_ARG_RECT(clip_rect)
       +        };
       +        if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_DRAW, (ltk_callback_arglist){args, LENGTH(args)}))
       +                return;
       +        if (widget->vtable->draw)
       +                widget->vtable->draw(widget, draw_surf, x, y, clip_rect);
       +}
       +
       +void
       +ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
       +        if (old_state == widget->state)
       +                return;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_INT(old_state)};
       +        if (ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_CHANGE_STATE, (ltk_callback_arglist){args, LENGTH(args)}))
       +                return;
       +        if (widget->vtable->change_state)
       +                widget->vtable->change_state(widget, old_state);
       +        if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
       +                widget->dirty = 1;
       +                ltk_window_invalidate_widget_rect(widget->window, widget);
       +        }
       +}
       +
       +/* FIXME: document that it's really dangerous to overwrite remove_child or destroy */
       +int
       +ltk_widget_destroy(ltk_widget *widget, int shallow) {
       +        ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_DESTROY, LTK_EMPTY_ARGLIST);
       +        /* widget->parent->remove_child should never be NULL because of the fact that
       +           the widget is set as parent, but let's just check anyways... */
       +        int invalid = 0;
       +        if (widget->parent) {
       +                if (widget->parent->vtable->remove_child)
       +                        invalid = widget->parent->vtable->remove_child(widget->parent, widget);
       +        }
       +        if (widget->vtable_copied) {
       +                ltk_free(widget->vtable);
       +                widget->vtable = NULL;
       +        }
       +        if (widget->signal_cbs) {
       +                ltk_array_destroy(signal, widget->signal_cbs);
       +                widget->signal_cbs = NULL;
       +        }
       +        widget->vtable->destroy(widget, shallow);
       +
       +        return invalid;
       +}
       +
       +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};
       +}
       +
       +int
       +ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data) {
       +        if ((type >= LTK_WIDGET_SIGNAL_INVALID) || type <= widget->vtable->invalid_signal)
       +                return 1;
       +        if (!widget->signal_cbs) {
       +                widget->signal_cbs = ltk_array_create(signal, 1);
       +        }
       +        ltk_array_append_signal(widget->signal_cbs, (ltk_signal_callback_info){callback, data, type});
       +        return 0;
       +}
       +
       +int
       +ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args) {
       +        if (!widget->signal_cbs)
       +                return 0;
       +        int handled = 0;
       +        for (size_t i = 0; i < ltk_array_len(widget->signal_cbs); i++) {
       +                if (ltk_array_get(widget->signal_cbs, i).type == type) {
       +                        handled |= ltk_array_get(widget->signal_cbs, i).callback(widget, args, ltk_array_get(widget->signal_cbs, i).data);
       +                }
       +        }
       +        return handled;
       +}
       +
       +static int
       +filter_by_type(ltk_signal_callback_info *info, void *data) {
       +        return info->type == *(int *)data;
       +}
       +
       +size_t
       +ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type) {
       +        if (!widget->signal_cbs)
       +                return 0;
       +        return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_type, &type);
       +}
       +
       +struct func_wrapper {
       +        ltk_signal_callback callback;
       +};
       +
       +static int
       +filter_by_callback(ltk_signal_callback_info *info, void *data) {
       +        return info->callback == ((struct func_wrapper *)data)->callback;
       +}
       +
       +size_t
       +ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback) {
       +        if (!widget->signal_cbs)
       +                return 0;
       +        /* callback can't be passed directly because ISO C forbids
       +           conversion of object pointer to function pointer */
       +        struct func_wrapper data = {callback};
       +        return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_callback, &data);
       +}
       +
       +struct delete_wrapper {
       +        int (*filter_func)(ltk_signal_callback_info *, ltk_signal_callback_info *);
       +        ltk_signal_callback_info *info;
       +};
       +
       +static int
       +filter_by_info(ltk_signal_callback_info *info, void *data) {
       +        struct delete_wrapper *w = data;
       +        return w->filter_func(info, w->info);
       +}
       +
       +size_t
       +ltk_widget_remove_signal_handler_by_info(
       +        ltk_widget *widget,
       +        int (*filter_func)(ltk_signal_callback_info *to_check, ltk_signal_callback_info *info),
       +        ltk_signal_callback_info *info) {
       +
       +        if (!widget->signal_cbs)
       +                return 0;
       +        struct delete_wrapper data = {filter_func, info};
       +        return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_info, &data);
       +}
       +
       +void
       +ltk_widget_remove_all_signal_handlers(ltk_widget *widget) {
       +        if (!widget->signal_cbs)
       +                return;
       +        ltk_array_destroy(signal, widget->signal_cbs);
       +        widget->signal_cbs = NULL;
       +}
       +
       +int ltk_widget_register_type(void); /* FIXME */
       +
       +ltk_widget_vtable *
       +ltk_widget_get_editable_vtable(ltk_widget *widget) {
       +        if (!widget->vtable_copied) {
       +                ltk_widget_vtable *vtable = ltk_malloc(sizeof(ltk_widget_vtable));
       +                memcpy(vtable, widget->vtable, sizeof(ltk_widget_vtable));
       +                widget->vtable_copied = 1;
       +        }
       +        return widget->vtable;
       +}
   DIR diff --git a/src/ltk/widget.h b/src/ltk/widget.h
       t@@ -0,0 +1,346 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +/* need to check what happens when registering signal for destroy, but then calling ltk_destroy_widget from
       +that handler - maybe loop if container widget deletes children but they call parent->delete_child again */
       +#ifndef LTK_WIDGET_H
       +#define LTK_WIDGET_H
       +
       +/* FIXME: destroy signal for widgets (also window) */
       +
       +#include <stddef.h>
       +#include "array.h"
       +#include "event.h"
       +#include "graphics.h"
       +#include "rect.h"
       +#include "util.h"
       +
       +struct ltk_widget;
       +struct ltk_window;
       +typedef struct ltk_widget ltk_widget;
       +
       +typedef enum {
       +        LTK_WIDGET_UNKNOWN = 0,
       +        LTK_WIDGET_ANY,
       +        LTK_WIDGET_GRID,
       +        LTK_WIDGET_BUTTON,
       +        LTK_WIDGET_LABEL,
       +        LTK_WIDGET_BOX,
       +        LTK_WIDGET_MENU,
       +        LTK_WIDGET_MENUENTRY,
       +        LTK_WIDGET_ENTRY,
       +        LTK_WIDGET_IMAGE,
       +        LTK_WIDGET_WINDOW,
       +        LTK_WIDGET_SCROLLBAR,
       +        LTK_NUM_WIDGETS,
       +} ltk_widget_type;
       +
       +typedef enum {
       +        LTK_ACTIVATABLE_NORMAL = 1,
       +        /* only activatable when "all-activatable"
       +           is set to true in the config */
       +        LTK_ACTIVATABLE_SPECIAL = 2,
       +        LTK_ACTIVATABLE_ALWAYS = 1|2,
       +        /* FIXME: redundant or needs better name - is implied by entries in vtable
       +           - if there are widgets that have keyboard functions in the vtable but
       +             shouldn't have this set, then it's a bad name */
       +        LTK_NEEDS_KEYBOARD = 4,
       +        LTK_NEEDS_REDRAW = 8,
       +        LTK_HOVER_IS_ACTIVE = 16,
       +} ltk_widget_flags;
       +
       +/* FIXME: "sticky" is maybe not the correct name anymore */
       +typedef enum {
       +        LTK_STICKY_NONE = 0,
       +        LTK_STICKY_LEFT = 1 << 0,
       +        LTK_STICKY_RIGHT = 1 << 1,
       +        LTK_STICKY_TOP = 1 << 2,
       +        LTK_STICKY_BOTTOM = 1 << 3,
       +        LTK_STICKY_SHRINK_WIDTH = 1 << 4,
       +        LTK_STICKY_SHRINK_HEIGHT = 1 << 5,
       +        LTK_STICKY_PRESERVE_ASPECT_RATIO = 1 << 6,
       +} ltk_sticky_mask;
       +
       +typedef enum {
       +        LTK_VERTICAL,
       +        LTK_HORIZONTAL
       +} ltk_orientation;
       +
       +typedef enum {
       +        LTK_NORMAL = 0,
       +        LTK_HOVER = 1,
       +        LTK_PRESSED = 2,
       +        LTK_ACTIVE = 4,
       +        LTK_HOVERACTIVE = 1 | 4,
       +        LTK_FOCUSED = 8,
       +        LTK_DISABLED = 16,
       +} ltk_widget_state;
       +
       +/* FIXME: need "ltk_register_type" just to get unique integer for type checking */
       +
       +typedef struct {
       +        union {
       +                int i;
       +                size_t sz;
       +                char c;
       +                ltk_widget *widget;
       +                char *str;
       +                const char *cstr;
       +                ltk_key_event *key_event;
       +                ltk_button_event *button_event;
       +                ltk_scroll_event *scroll_event;
       +                ltk_motion_event *motion_event;
       +                ltk_surface *surface;
       +                void *v;
       +                /* FIXME: maybe rewrite the functions to take
       +                   pointers instead so this doesn't increase
       +                   the size of the union (thereby increasing
       +                   the size of every arg in the arglist) */
       +                ltk_rect rect;
       +        } arg;
       +        enum {
       +                LTK_TYPE_INT,
       +                LTK_TYPE_SIZE_T,
       +                LTK_TYPE_CHAR,
       +                LTK_TYPE_WIDGET,
       +                LTK_TYPE_STRING,
       +                LTK_TYPE_CONST_STRING,
       +                LTK_TYPE_KEY_EVENT,
       +                LTK_TYPE_BUTTON_EVENT,
       +                LTK_TYPE_SCROLL_EVENT,
       +                LTK_TYPE_MOTION_EVENT,
       +                LTK_TYPE_SURFACE,
       +                LTK_TYPE_RECT,
       +                LTK_TYPE_VOID,
       +                LTK_TYPE_VOIDP,
       +        } type;
       +} ltk_callback_arg;
       +
       +/* FIXME: STRING should be CHARP for consistency */
       +#define LTK_MAKE_ARG_INT(data) ((ltk_callback_arg){.type = LTK_TYPE_INT, .arg = {.i = (data)}})
       +#define LTK_MAKE_ARG_SIZE_T(data) ((ltk_callback_arg){.type = LTK_TYPE_SIZE_T, .arg = {.sz = (data)}})
       +#define LTK_MAKE_ARG_CHAR(data) ((ltk_callback_arg){.type = LTK_TYPE_CHAR, .arg = {.c = (data)}})
       +#define LTK_MAKE_ARG_WIDGET(data) ((ltk_callback_arg){.type = LTK_TYPE_WIDGET, .arg = {.widget = (data)}})
       +#define LTK_MAKE_ARG_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_STRING, .arg = {.str = (data)}})
       +#define LTK_MAKE_ARG_CONST_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_CONST_STRING, .arg = {.cstr = (data)}})
       +#define LTK_MAKE_ARG_KEY_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_KEY_EVENT, .arg = {.key_event = (data)}})
       +#define LTK_MAKE_ARG_BUTTON_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_BUTTON_EVENT, .arg = {.button_event = (data)}})
       +#define LTK_MAKE_ARG_SCROLL_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_SCROLL_EVENT, .arg = {.scroll_event = (data)}})
       +#define LTK_MAKE_ARG_MOTION_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_MOTION_EVENT, .arg = {.motion_event = (data)}})
       +#define LTK_MAKE_ARG_SURFACE(data) ((ltk_callback_arg){.type = LTK_TYPE_SURFACE, .arg = {.surface = (data)}})
       +#define LTK_MAKE_ARG_RECT(data) ((ltk_callback_arg){.type = LTK_TYPE_RECT, .arg = {.rect = (data)}})
       +#define LTK_MAKE_ARG_VOIDP(data) ((ltk_callback_arg){.type = LTK_TYPE_VOIDP, .arg = {.v = (data)}})
       +
       +#define LTK_ARG_VOID ((ltk_callback_arg){.type = LTK_TYPE_VOID})
       +
       +#define LTK_CAST_ARG_INT(carg) (ltk_assert(carg.type == LTK_TYPE_INT), carg.arg.i)
       +#define LTK_CAST_ARG_SIZE_T(carg) (ltk_assert(carg.type == LTK_TYPE_SIZE_T), carg.arg.sz)
       +#define LTK_CAST_ARG_CHAR(carg) (ltk_assert(carg.type == LTK_TYPE_CHAR), carg.arg.c)
       +#define LTK_CAST_ARG_WIDGET(carg) (ltk_assert(carg.type == LTK_TYPE_WIDGET), carg.arg.widget)
       +#define LTK_CAST_ARG_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_STRING), carg.arg.str)
       +#define LTK_CAST_ARG_CONST_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_CONST_STRING), carg.arg.cstr)
       +#define LTK_CAST_ARG_KEY_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_KEY_EVENT), carg.arg.key_event)
       +#define LTK_CAST_ARG_BUTTON_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_BUTTON_EVENT), carg.arg.button_event)
       +#define LTK_CAST_ARG_SCROLL_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_SCROLL_EVENT), carg.arg.scroll_event)
       +#define LTK_CAST_ARG_MOTION_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_MOTION_EVENT), carg.arg.motion_event)
       +#define LTK_CAST_ARG_SURFACE(carg) (ltk_assert(carg.type == LTK_TYPE_SURFACE), carg.arg.surface)
       +#define LTK_CAST_ARG_RECT(carg) (ltk_assert(carg.type == LTK_TYPE_RECT), carg.arg.rect)
       +#define LTK_CAST_ARG_VOIDP(carg) (ltk_assert(carg.type == LTK_TYPE_VOIDP), carg.arg.v)
       +
       +#define LTK_GET_ARG_INT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_INT(cargs.args[i]))
       +#define LTK_GET_ARG_SIZE_T(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SIZE_T(cargs.args[i]))
       +#define LTK_GET_ARG_CHAR(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CHAR(cargs.args[i]))
       +#define LTK_GET_ARG_WIDGET(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_WIDGET(cargs.args[i]))
       +#define LTK_GET_ARG_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_STRING(cargs.args[i]))
       +#define LTK_GET_ARG_CONST_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CONST_STRING(cargs.args[i]))
       +#define LTK_GET_ARG_KEY_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_KEY_EVENT(cargs.args[i]))
       +#define LTK_GET_ARG_BUTTON_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_BUTTON_EVENT(cargs.args[i]))
       +#define LTK_GET_ARG_SCROLL_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SCROLL_EVENT(cargs.args[i]))
       +#define LTK_GET_ARG_MOTION_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_MOTION_EVENT(cargs.args[i]))
       +#define LTK_GET_ARG_SURFACE(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SURFACE(cargs.args[i]))
       +#define LTK_GET_ARG_RECT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_RECT(cargs.args[i]))
       +
       +#define LTK_CAST_WIDGET(w) (&(w)->widget)
       +#define LTK_CAST_WINDOW(w) (ltk_assert(w->vtable->type == LTK_WIDGET_WINDOW), (ltk_window *)(w))
       +#define LTK_CAST_LABEL(w) (ltk_assert(w->vtable->type == LTK_WIDGET_LABEL), (ltk_label *)(w))
       +#define LTK_CAST_BUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BUTTON), (ltk_button *)(w))
       +#define LTK_CAST_GRID(w) (ltk_assert(w->vtable->type == LTK_WIDGET_GRID), (ltk_grid *)(w))
       +#define LTK_CAST_IMAGE_WIDGET(w) (ltk_assert(w->vtable->type == LTK_WIDGET_IMAGE), (ltk_image_widget *)(w))
       +#define LTK_CAST_ENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_ENTRY), (ltk_entry *)(w))
       +#define LTK_CAST_MENU(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENU), (ltk_menu *)(w))
       +#define LTK_CAST_MENUENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENUENTRY), (ltk_menuentry *)(w))
       +#define LTK_CAST_SCROLLBAR(w) (ltk_assert(w->vtable->type == LTK_WIDGET_SCROLLBAR), (ltk_scrollbar *)(w))
       +#define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
       +
       +/* FIXME: a bit weird because window never gets some of these signals */
       +#define LTK_WIDGET_SIGNAL_KEY_PRESS          1
       +#define LTK_WIDGET_SIGNAL_KEY_RELEASE        2
       +#define LTK_WIDGET_SIGNAL_MOUSE_PRESS        3
       +#define LTK_WIDGET_SIGNAL_MOUSE_RELEASE      4
       +#define LTK_WIDGET_SIGNAL_MOUSE_SCROLL       5
       +#define LTK_WIDGET_SIGNAL_MOTION_NOTIFY      6
       +#define LTK_WIDGET_SIGNAL_MOUSE_ENTER        7
       +#define LTK_WIDGET_SIGNAL_MOUSE_LEAVE        8
       +#define LTK_WIDGET_SIGNAL_PRESS              9
       +#define LTK_WIDGET_SIGNAL_RELEASE            10
       +#define LTK_WIDGET_SIGNAL_RESIZE             11
       +#define LTK_WIDGET_SIGNAL_HIDE               12
       +#define LTK_WIDGET_SIGNAL_DRAW               13
       +#define LTK_WIDGET_SIGNAL_CHANGE_STATE       14
       +/* The return value for this is ignored, i.e.
       +   the widget destroy function is always called.
       +   Also, it doesn't receive the 'shallow' argument
       +   that the widget method receives. */
       +#define LTK_WIDGET_SIGNAL_DESTROY            15
       +#define LTK_WIDGET_SIGNAL_INVALID            16
       +
       +typedef struct {
       +        ltk_callback_arg *args;
       +        size_t num;
       +} ltk_callback_arglist;
       +
       +#define LTK_EMPTY_ARGLIST ((ltk_callback_arglist){NULL, 0})
       +
       +typedef int (*ltk_signal_callback)(ltk_widget *widget, ltk_callback_arglist args, ltk_callback_arg data);
       +typedef struct {
       +        ltk_signal_callback callback;
       +        ltk_callback_arg data;
       +        int type;
       +} ltk_signal_callback_info;
       +
       +int ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data);
       +int ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args);
       +size_t ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type);
       +size_t ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback);
       +size_t ltk_widget_remove_signal_handler_by_info(
       +        ltk_widget *widget,
       +        int (*filter_func)(ltk_signal_callback_info *to_check, ltk_signal_callback_info *info),
       +        ltk_signal_callback_info *info
       +);
       +void ltk_widget_remove_all_signal_handlers(ltk_widget *widget);
       +int ltk_widget_register_type(void);
       +
       +LTK_ARRAY_INIT_STRUCT_DECL(signal, ltk_signal_callback_info)
       +
       +struct ltk_widget {
       +        struct ltk_window *window;
       +        struct ltk_widget *parent;
       +
       +        struct ltk_widget_vtable *vtable;
       +
       +        /* 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;
       +
       +        /* maybe mask to determine quickly which callbacks are included?
       +        default signals only allowed to have one callback? */
       +        /* could maybe have just uint32_t mask so at least the lower signals
       +           can be easily checked */
       +        ltk_array(signal) *signal_cbs;
       +
       +        ltk_widget_state state;
       +        /* FIXME: store this in grid/box - for row_span, column_span the other cells could be marked with "not top left cell of widget" so they can be skipped */
       +        ltk_sticky_mask sticky;
       +        unsigned short row;
       +        unsigned short column;
       +        unsigned short row_span;
       +        unsigned short column_span;
       +        /* ALSO NEED SIGNALS LIKE ADD-TEXT (called *before* text is inserted to check validity) - these would need argument
       +        FIGURE OUT HOW TO DO KEY MAPPINGS - should reuse parts of builtin mapping handling
       +        -> maybe something like tk
       +        -> or maybe just say everyone needs to override event handler? but makes simple stuff more difficult
       +        -> also need "global" mappings/key event handler for global shortcuts */
       +        /* needed to properly handle handle local coordinates since
       +           popups are positioned globally instead of locally */
       +        char popup;
       +        char dirty;
       +        char hidden;
       +        char vtable_copied;
       +};
       +
       +typedef struct ltk_widget_vtable {
       +        int (*key_press)(struct ltk_widget *, ltk_key_event *);
       +        int (*key_release)(struct ltk_widget *, ltk_key_event *);
       +        /* press/release also receive double/triple-click/release */
       +        int (*mouse_press)(struct ltk_widget *, ltk_button_event *);
       +        int (*mouse_release)(struct ltk_widget *, ltk_button_event *);
       +        int (*mouse_scroll)(struct ltk_widget *, ltk_scroll_event *);
       +        int (*motion_notify)(struct ltk_widget *, ltk_motion_event *);
       +        int (*mouse_leave)(struct ltk_widget *, ltk_motion_event *);
       +        int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
       +        int (*press)(struct ltk_widget *);
       +        int (*release)(struct ltk_widget *);
       +        void (*cmd_return)(struct ltk_widget *self, char *text, size_t len);
       +
       +        void (*resize)(struct ltk_widget *);
       +        void (*hide)(struct ltk_widget *);
       +        /* 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_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 *);
       +        int (*remove_child)(struct ltk_widget *, struct ltk_widget *);
       +        /* 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;
       +        int invalid_signal;
       +} ltk_widget_vtable;
       +
       +void ltk_widget_hide(ltk_widget *widget);
       +int ltk_widget_destroy(ltk_widget *widget, int shallow);
       +void ltk_fill_widget_defaults(
       +    ltk_widget *widget, struct ltk_window *window,
       +    struct ltk_widget_vtable *vtable, int w, int h
       +);
       +void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
       +void ltk_widget_resize(ltk_widget *widget);
       +void ltk_widget_draw(ltk_widget *widget, ltk_surface *draw_surf, int x, int y, ltk_rect clip_rect);
       +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);
       +
       +ltk_widget_vtable *ltk_widget_get_editable_vtable(ltk_widget *widget);
       +
       +#endif /* LTK_WIDGET_H */
   DIR diff --git a/src/ltk/window.c b/src/ltk/window.c
       t@@ -0,0 +1,1319 @@
       +/* FIXME: signal handling is really ugly and inconsistent at the moment */
       +/*
       + * Copyright (c) 2020-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stdlib.h>
       +#include <string.h>
       +
       +#include "ltk.h"
       +#include "util.h"
       +#include "keys.h"
       +#include "array.h"
       +#include "theme.h"
       +#include "widget.h"
       +#include "window.h"
       +#include "memory.h"
       +#include "eventdefs.h"
       +
       +#define MAX_WINDOW_FONT_SIZE 200
       +
       +static void gen_widget_stack(ltk_widget *bottom);
       +static ltk_widget *get_hover_popup(ltk_window *window, int x, int y);
       +static int is_parent(ltk_widget *parent, ltk_widget *child);
       +static ltk_widget *get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret);
       +
       +static int ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event);
       +static int ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event);
       +static int ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event);
       +static int ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event);
       +static int ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event);
       +static int ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event);
       +static void ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
       +
       +/* FIXME: actually use this properly */
       +static struct ltk_widget_vtable vtable = {
       +        .key_press = &ltk_window_key_press_event,
       +        .key_release = &ltk_window_key_release_event,
       +        .mouse_press = &ltk_window_mouse_press_event,
       +        .mouse_release = &ltk_window_mouse_release_event,
       +        .release = NULL,
       +        .motion_notify = &ltk_window_motion_notify_event,
       +        .mouse_leave = NULL,
       +        .mouse_enter = NULL,
       +        .change_state = NULL,
       +        .get_child_at_pos = NULL,
       +        .resize = NULL,
       +        .hide = NULL,
       +        .draw = &ltk_window_redraw,
       +        .destroy = &ltk_window_destroy,
       +        .child_size_change = NULL,
       +        .remove_child = NULL,
       +        .type = LTK_WIDGET_WINDOW,
       +        .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
       +        .invalid_signal = LTK_WINDOW_SIGNAL_INVALID,
       +};
       +
       +static int cb_focus_active(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_prev(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_next(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_left(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_right(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_up(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_move_down(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled);
       +static int cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled);
       +
       +struct key_cb {
       +        char *func_name;
       +        int (*callback)(ltk_window *, ltk_key_event *, int handled);
       +};
       +
       +static struct key_cb cb_map[] = {
       +        {"focus-active", &cb_focus_active},
       +        {"move-down", &cb_move_down},
       +        {"move-left", &cb_move_left},
       +        {"move-next", &cb_move_next},
       +        {"move-prev", &cb_move_prev},
       +        {"move-right", &cb_move_right},
       +        {"move-up", &cb_move_up},
       +        {"remove-popups", &cb_remove_popups},
       +        {"set-pressed", &cb_set_pressed},
       +        {"unfocus-active", &cb_unfocus_active},
       +        {"unset-pressed", &cb_unset_pressed},
       +};
       +
       +struct keypress_cfg {
       +        ltk_keypress_binding b;
       +        struct key_cb cb;
       +};
       +
       +struct keyrelease_cfg {
       +        ltk_keyrelease_binding b;
       +        struct key_cb cb;
       +};
       +
       +LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
       +LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
       +LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
       +LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
       +
       +static ltk_array(keypress) *keypresses = NULL;
       +static ltk_array(keyrelease) *keyreleases = NULL;
       +
       +GEN_CB_MAP_HELPERS(cb_map, struct key_cb, func_name)
       +
       +/* 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 ltk_window_theme theme;
       +static ltk_theme_parseinfo theme_parseinfo[] = {
       +        {"bg", THEME_COLOR, {.color = &theme.bg}, {.color = "#000000"}, 0, 0, 0},
       +        {"fg", THEME_COLOR, {.color = &theme.fg}, {.color = "#FFFFFF"}, 0, 0, 0},
       +        {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
       +        {"font-size", THEME_INT, {.i = &theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0},
       +};
       +static int theme_parseinfo_sorted = 0;
       +
       +int
       +ltk_window_fill_theme_defaults(ltk_renderdata *data) {
       +        return ltk_theme_fill_defaults(data, "window", theme_parseinfo, LENGTH(theme_parseinfo));
       +}
       +
       +int
       +ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
       +        return ltk_theme_handle_value(data, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
       +}
       +
       +void
       +ltk_window_uninitialize_theme(ltk_renderdata *data) {
       +        ltk_theme_uninitialize(data, theme_parseinfo, LENGTH(theme_parseinfo));
       +}
       +
       +/* FIXME: maybe ltk_fatal if ltk not initialized? */
       +ltk_window_theme *
       +ltk_window_get_theme(void) {
       +        return &theme;
       +}
       +
       +/* FIXME: most of this is duplicated code */
       +
       +int
       +ltk_window_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
       +        if (!keypresses)
       +                keypresses = ltk_array_create(keypress, 1);
       +        struct key_cb *cb = cb_map_get_entry(func_name, func_len);
       +        if (!cb)
       +                return 1;
       +        struct keypress_cfg cfg = {b, *cb};
       +        ltk_array_append(keypress, keypresses, cfg);
       +        return 0;
       +}
       +
       +int
       +ltk_window_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
       +        if (!keyreleases)
       +                keyreleases = ltk_array_create(keyrelease, 1);
       +        struct key_cb *cb = cb_map_get_entry(func_name, func_len);
       +        if (!cb)
       +                return 1;
       +        struct keyrelease_cfg cfg = {b, *cb};
       +        ltk_array_append(keyrelease, keyreleases, cfg);
       +        return 0;
       +}
       +
       +static void
       +destroy_keypress_cfg(struct keypress_cfg cfg) {
       +        ltk_keypress_binding_destroy(cfg.b);
       +}
       +
       +void
       +ltk_window_cleanup(void) {
       +        ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
       +        ltk_array_destroy(keyrelease, keyreleases);
       +        free(widget_stack);
       +        keypresses = NULL;
       +        keyreleases = NULL;
       +        widget_stack = NULL;
       +}
       +
       +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: 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 */
       +static int
       +ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        int handled = 0;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_KEY_EVENT(event)};
       +        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 (ltk_widget_emit_signal(widget_stack[i], LTK_WIDGET_SIGNAL_KEY_PRESS, (ltk_callback_arglist){args, LENGTH(args)}) ||
       +                            (widget_stack[i]->vtable->key_press && widget_stack[i]->vtable->key_press(widget_stack[i], event))) {
       +                                handled = 1;
       +                                break;
       +                        }
       +                }
       +        }
       +        if (!keypresses)
       +                return 1;
       +        ltk_keypress_binding *b = NULL;
       +        for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
       +                b = &ltk_array_get(keypresses, i).b;
       +                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 |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       +                } else if (b->rawtext) {
       +                        if (event->text && !strcmp(b->text, event->text))
       +                                handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       +                } else if (b->sym != LTK_KEY_NONE) {
       +                        if (event->sym == b->sym)
       +                                handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       +                }
       +        }
       +        return 1;
       +}
       +
       +/* FIXME: need to actually check if any of parent widgets are focused and still pass to them even if bottom widget not focused? */
       +static int
       +ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        int handled = 0;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_KEY_EVENT(event)};
       +        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 (ltk_widget_emit_signal(widget_stack[i], LTK_WIDGET_SIGNAL_KEY_RELEASE, (ltk_callback_arglist){args, LENGTH(args)}) ||
       +                            (widget_stack[i]->vtable->key_release && widget_stack[i]->vtable->key_release(widget_stack[i], event))) {
       +                                handled = 1;
       +                                break;
       +                        }
       +                }
       +        }
       +        if (!keyreleases)
       +                return 1;
       +        ltk_keyrelease_binding *b = NULL;
       +        for (size_t i = 0; i < ltk_array_len(keyreleases); i++) {
       +                b = &ltk_array_get(keyreleases, i).b;
       +                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 |= ltk_array_get(keyreleases, i).cb.callback(window, event, handled);
       +                }
       +        }
       +        return 1;
       +}
       +
       +/* FIXME: This is still weird. */
       +static int
       +ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        ltk_widget *widget = get_hover_popup(window, event->x, event->y);
       +        int check_hide = 0;
       +        if (!widget) {
       +                widget = window->root_widget;
       +                check_hide = 1;
       +        }
       +        if (!widget) {
       +                ltk_window_unregister_all_popups(window);
       +                return 1;
       +        }
       +        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);
       +        }
       +
       +        /* FIXME: this doesn't make much sense if the popups aren't a
       +           hierarchy (right now, they're just menus, so that's always
       +           a hierarchy */
       +        /* don't hide popups if they are children of the now pressed widget */
       +        if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
       +                ltk_window_unregister_all_popups(window);
       +
       +        /* FIXME: popups don't always have their children geometrically contained within parents,
       +           so this won't work properly in all cases */
       +        int first = 1;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_BUTTON_EVENT(event)};
       +        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 */
       +                        handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOUSE_PRESS, (ltk_callback_arglist){args, LENGTH(args)});
       +                        if (!handled && 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 && event->type == LTK_BUTTONPRESS_EVENT && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
       +                                ltk_window_set_pressed_widget(window, cur_widget, 0);
       +                                first = 0;
       +                        }
       +                }
       +                if (!handled)
       +                        cur_widget = cur_widget->parent;
       +                else
       +                        break;
       +        }
       +        return 1;
       +}
       +
       +static int
       +ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        /* FIXME: should it first be sent to pressed widget? */
       +        ltk_widget *widget = get_hover_popup(window, event->x, event->y);
       +        if (!widget)
       +                widget = window->root_widget;
       +        if (!widget)
       +                return 1;
       +        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);
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_SCROLL_EVENT(event)};
       +        /* FIXME: same issue with popups like in mouse_press above */
       +        while (cur_widget) {
       +                int handled = 0;
       +                ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
       +                event->x = local.x;
       +                event->y = local.y;
       +                if (cur_widget->state != LTK_DISABLED) {
       +                        /* FIXME: see function above
       +                        if (queue_scroll_event(cur_widget, event->x, event->y, event->dx, event->dy))
       +                                handled = 1; */
       +                        handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOUSE_SCROLL, (ltk_callback_arglist){args, LENGTH(args)});
       +                        if (!handled && cur_widget->vtable->mouse_scroll)
       +                                handled = cur_widget->vtable->mouse_scroll(cur_widget, event);
       +                }
       +                if (!handled)
       +                        cur_widget = cur_widget->parent;
       +                else
       +                        break;
       +        }
       +        return 1;
       +}
       +
       +void
       +ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
       +        ltk_widget *self = LTK_CAST_WIDGET(window);
       +        ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = x, .y = y};
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(&e)};
       +        if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                self->vtable->motion_notify(LTK_CAST_WIDGET(window), &e);
       +        }
       +}
       +
       +static int
       +ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        ltk_widget *widget = window->pressed_widget;
       +        int orig_x = event->x, orig_y = event->y;
       +        /* FIXME: why does this only take pressed widget and popups into account? */
       +        if (!widget) {
       +                widget = get_hover_popup(window, event->x, event->y);
       +                widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
       +        }
       +        /* FIXME: loop up to top of hierarchy if not handled */
       +        /* FIXME: see functions above
       +        if (widget && queue_mouse_event(widget, event->type, event->x, event->y)) { */
       +                /* NOP */
       +        if (widget) {
       +                ltk_callback_arg args[] = {LTK_MAKE_ARG_BUTTON_EVENT(event)};
       +                if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOUSE_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        if (widget->vtable->mouse_release)
       +                                widget->vtable->mouse_release(widget, event);
       +                }
       +        }
       +        if (event->button == LTK_BUTTONL && event->type == LTK_BUTTONRELEASE_EVENT) {
       +                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, orig_x, orig_y);
       +        }
       +        return 1;
       +}
       +
       +static int
       +ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event) {
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        ltk_widget *widget = get_hover_popup(window, event->x, event->y);
       +        int orig_x = event->x, orig_y = event->y;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(event)};
       +        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 (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                                if (widget->vtable->motion_notify)
       +                                        widget->vtable->motion_notify(widget, event);
       +                        }
       +                        return 1;
       +                }
       +                widget = window->root_widget;
       +        }
       +        if (!widget)
       +                return 1;
       +        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 1;
       +        }
       +        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) {
       +                        /* FIXME: see functions above
       +                        if (queue_mouse_event(cur_widget, LTK_MOTION_EVENT, event->x, event->y))
       +                                handled = 1; */
       +                        handled = ltk_widget_emit_signal(cur_widget, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)});
       +                        if (!handled && cur_widget->vtable->motion_notify)
       +                                handled = cur_widget->vtable->motion_notify(cur_widget, event);
       +                        /* set first non-disabled widget to hover widget */
       +                        /* FIXME: should enter/leave event be sent to parent
       +                           when moving from/to widget nested in parent? */
       +                        /* 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;
       +                        }
       +                }
       +                if (!handled)
       +                        cur_widget = cur_widget->parent;
       +                else
       +                        break;
       +        }
       +        if (first) {
       +                event->x = orig_x;
       +                event->y = orig_y;
       +                ltk_window_set_hover_widget(window, NULL, event);
       +        }
       +        return 1;
       +}
       +
       +void
       +ltk_window_set_root_widget(ltk_window *window, ltk_widget *widget) {
       +        window->root_widget = widget;
       +        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);
       +}
       +
       +void
       +ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
       +        if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
       +                window->dirty_rect = rect;
       +        else
       +                window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
       +}
       +
       +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});
       +}
       +
       +static void
       +ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
       +        (void)draw_surf;
       +        (void)x;
       +        (void)y;
       +        (void)clip;
       +        if (!self) return;
       +        ltk_window *window = LTK_CAST_WINDOW(self);
       +        ltk_widget *ptr;
       +        if (window->dirty_rect.x >= window->rect.w) return;
       +        if (window->dirty_rect.y >= window->rect.h) return;
       +        if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
       +                window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
       +        if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
       +                window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
       +        /* FIXME: this should use window->dirty_rect, but that doesn't work
       +           properly with double buffering */
       +        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;
       +                ltk_widget_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];
       +                ltk_widget_draw(ptr, window->surface, ptr->lrect.x, ptr->lrect.y, ltk_rect_relative(ptr->lrect, window->rect));
       +        }
       +        ltk_renderer_swap_buffers(window->renderwindow);
       +        window->dirty_rect.w = 0;
       +        window->dirty_rect.h = 0;
       +}
       +
       +static void
       +ltk_window_other_event(ltk_window *window, ltk_event *event) {
       +        ltk_widget *ptr = window->root_widget;
       +        /* FIXME: decide whether this should be moved to separate resize function in window vtable */
       +        if (event->type == LTK_CONFIGURE_EVENT) {
       +                ltk_window_unregister_all_popups(window);
       +                int w, h;
       +                w = event->configure.w;
       +                h = event->configure.h;
       +                int orig_w = window->rect.w;
       +                int orig_h = window->rect.h;
       +                if (orig_w != w || orig_h != h) {
       +                        window->rect.w = w;
       +                        window->rect.h = h;
       +                        ltk_window_invalidate_rect(window, window->rect);
       +                        ltk_surface_update_size(window->surface, w, h);
       +                        if (ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WIDGET_SIGNAL_RESIZE, LTK_EMPTY_ARGLIST))
       +                                return;
       +                        if (ptr) {
       +                                ptr->lrect.w = w;
       +                                ptr->lrect.h = h;
       +                                ptr->crect = ptr->lrect;
       +                                ltk_widget_resize(ptr);
       +                        }
       +                }
       +        } else if (event->type == LTK_EXPOSE_EVENT) {
       +                ltk_rect r;
       +                r.x = event->expose.x;
       +                r.y = event->expose.y;
       +                r.w = event->expose.w;
       +                r.h = event->expose.h;
       +                ltk_window_invalidate_rect(window, r);
       +        } else if (event->type == LTK_WINDOWCLOSE_EVENT) {
       +                ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, LTK_EMPTY_ARGLIST);
       +        }
       +}
       +
       +/* FIXME: check for duplicates? */
       +void
       +ltk_window_register_popup(ltk_window *window, ltk_widget *popup) {
       +        if (window->popups_num == window->popups_alloc) {
       +                window->popups_alloc = ideal_array_size(
       +                    window->popups_alloc, window->popups_num + 1
       +                );
       +                window->popups = ltk_reallocarray(
       +                    window->popups, window->popups_alloc, sizeof(ltk_widget *)
       +                );
       +        }
       +        window->popups[window->popups_num++] = popup;
       +        popup->popup = 1;
       +}
       +
       +void
       +ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup) {
       +        if (window->popups_locked)
       +                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,
       +                            sizeof(ltk_widget *) * (window->popups_num - i - 1)
       +                        );
       +                        window->popups_num--;
       +                        size_t sz = ideal_array_size(
       +                            window->popups_alloc, window->popups_num
       +                        );
       +                        if (sz != window->popups_alloc) {
       +                                window->popups_alloc = sz;
       +                                window->popups = ltk_reallocarray(
       +                                    window->popups, sz, sizeof(ltk_widget *)
       +                                );
       +                        }
       +                        return;
       +                }
       +        }
       +}
       +
       +/* FIXME: where should actual hiding happen? */
       +void
       +ltk_window_unregister_all_popups(ltk_window *window) {
       +        window->popups_locked = 1;
       +        for (size_t i = 0; i < window->popups_num; i++) {
       +                window->popups[i]->hidden = 1;
       +                window->popups[i]->popup = 0;
       +                ltk_widget_hide(window->popups[i]);
       +        }
       +        window->popups_num = 0;
       +        /* somewhat arbitrary, but should be enough for most cases */
       +        if (window->popups_num > 4) {
       +                window->popups = ltk_reallocarray(
       +                    window->popups, 4, sizeof(ltk_widget *)
       +                );
       +                window->popups_alloc = 4;
       +        }
       +        window->popups_locked = 0;
       +        /* I guess just invalidate everything instead of being smart */
       +        ltk_window_invalidate_rect(window, window->rect);
       +}
       +
       +/* FIXME: support more options like child windows */
       +ltk_window *
       +ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
       +        ltk_window *window = ltk_malloc(sizeof(ltk_window));
       +        /* this is a bit weird because the window entry points to itself */
       +        /* the ideal width isn't needed for a window */
       +        ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0);
       +
       +        window->popups = NULL;
       +        window->popups_num = window->popups_alloc = 0;
       +        window->popups_locked = 0;
       +
       +        window->renderwindow = ltk_renderer_create_window(data, title, x, y, w, h);
       +        ltk_renderer_set_window_properties(window->renderwindow, theme.bg);
       +        window->theme = &theme;
       +
       +        window->root_widget = NULL;
       +        window->hover_widget = NULL;
       +        window->active_widget = NULL;
       +        window->pressed_widget = NULL;
       +
       +        //FIXME: use widget rect
       +        window->rect.w = w;
       +        window->rect.h = h;
       +        window->rect.x = 0;
       +        window->rect.y = 0;
       +        window->dirty_rect.w = 0;
       +        window->dirty_rect.h = 0;
       +        window->dirty_rect.x = 0;
       +        window->dirty_rect.y = 0;
       +
       +        window->surface_cache = ltk_surface_cache_create(window->renderwindow);
       +        window->surface = ltk_surface_from_window(window->renderwindow, w, h);
       +
       +        return window;
       +}
       +
       +/* FIXME: check if widget window matches in all public functions */
       +
       +void
       +ltk_window_destroy_intern(ltk_window *window) {
       +        if (window->root_widget) {
       +                ltk_widget_destroy(window->root_widget, 0);
       +        }
       +        if (window->popups)
       +                ltk_free(window->popups);
       +        ltk_surface_cache_destroy(window->surface_cache);
       +        ltk_surface_destroy(window->surface);
       +        ltk_renderer_destroy_window(window->renderwindow);
       +        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;
       +        ltk_callback_arg args[] = {LTK_MAKE_ARG_MOTION_EVENT(event)};
       +        if (old) {
       +                ltk_widget_state old_state = old->state;
       +                old->state &= ~LTK_HOVER;
       +                ltk_widget_change_state(old, old_state);
       +                ltk_point local = ltk_global_to_widget_pos(old, event->x, event->y);
       +                event->x = local.x;
       +                event->y = local.y;
       +                if (!ltk_widget_emit_signal(old, LTK_WIDGET_SIGNAL_MOUSE_LEAVE, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        if (old->vtable->mouse_leave)
       +                                old->vtable->mouse_leave(old, event);
       +                }
       +                event->x = orig_x;
       +                event->y = orig_y;
       +        }
       +        window->hover_widget = 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 (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_MOUSE_ENTER, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        if (widget->vtable->mouse_enter)
       +                                widget->vtable->mouse_enter(widget, event);
       +                }
       +                ltk_widget_state old_state = widget->state;
       +                widget->state |= LTK_HOVER;
       +                ltk_widget_change_state(widget, old_state);
       +                if ((widget->vtable->flags & LTK_HOVER_IS_ACTIVE) && widget != window->active_widget)
       +                        ltk_window_set_active_widget(window, widget);
       +        }
       +}
       +
       +void
       +ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
       +        if (window->active_widget == widget) {
       +                return;
       +        }
       +        ltk_widget *old = window->active_widget;
       +        /* Note: this has to be set at the beginning to
       +           avoid infinite recursion in some cases */
       +        window->active_widget = widget;
       +        ltk_widget *common_parent = NULL;
       +        if (widget) {
       +                ltk_widget *cur = widget;
       +                while (cur) {
       +                        if (cur->state & LTK_ACTIVE) {
       +                                common_parent = cur;
       +                                break;
       +                        }
       +                        ltk_widget_state old_state = cur->state;
       +                        cur->state |= LTK_ACTIVE;
       +                        /* 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;
       +                }
       +        }
       +        /* FIXME: better variable names; generally make this nicer */
       +        /* special case if old is parent of new active widget */
       +        ltk_widget *tmp = common_parent;
       +        while (tmp) {
       +                if (tmp == old)
       +                        return;
       +                tmp = tmp->parent;
       +        }
       +        if (old) {
       +                old->state &= ~LTK_FOCUSED;
       +                ltk_widget *cur = old;
       +                while (cur) {
       +                        if (cur == common_parent)
       +                                break;
       +                        ltk_widget_state old_state = cur->state;
       +                        cur->state &= ~LTK_ACTIVE;
       +                        ltk_widget_change_state(cur, old_state);
       +                        cur = cur->parent;
       +                }
       +        }
       +}
       +
       +void
       +ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release) {
       +        if (window->pressed_widget == widget)
       +                return;
       +        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 && (old_state & LTK_PRESSED)) {
       +                        if (!ltk_widget_emit_signal(window->pressed_widget, LTK_WIDGET_SIGNAL_RELEASE, LTK_EMPTY_ARGLIST)) {
       +                                if (window->pressed_widget->vtable->release)
       +                                        window->pressed_widget->vtable->release(window->pressed_widget);
       +                        }
       +                }
       +        }
       +        window->pressed_widget = widget;
       +        if (widget) {
       +                if (!ltk_widget_emit_signal(widget, LTK_WIDGET_SIGNAL_PRESS, LTK_EMPTY_ARGLIST)) {
       +                        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);
       +        }
       +}
       +
       +void
       +ltk_window_handle_event(ltk_window *window, ltk_event *event) {
       +        ltk_widget *self = LTK_CAST_WIDGET(window);
       +        ltk_callback_arg args[1];
       +        switch (event->type) {
       +        case LTK_KEYPRESS_EVENT:
       +                args[0] = LTK_MAKE_ARG_KEY_EVENT(&event->key);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_KEY_PRESS, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_key_press_event(self, &event->key);
       +                }
       +                break;
       +        case LTK_KEYRELEASE_EVENT:
       +                args[0] = LTK_MAKE_ARG_KEY_EVENT(&event->key);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_KEY_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_key_release_event(self, &event->key);
       +                }
       +                break;
       +        case LTK_BUTTONPRESS_EVENT:
       +        case LTK_2BUTTONPRESS_EVENT:
       +        case LTK_3BUTTONPRESS_EVENT:
       +                args[0] = LTK_MAKE_ARG_BUTTON_EVENT(&event->button);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_PRESS, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_mouse_press_event(self, &event->button);
       +                }
       +                break;
       +        case LTK_SCROLL_EVENT:
       +                args[0] = LTK_MAKE_ARG_SCROLL_EVENT(&event->scroll);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_SCROLL, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_mouse_scroll_event(self, &event->scroll);
       +                }
       +                break;
       +        case LTK_BUTTONRELEASE_EVENT:
       +        case LTK_2BUTTONRELEASE_EVENT:
       +        case LTK_3BUTTONRELEASE_EVENT:
       +                args[0] = LTK_MAKE_ARG_BUTTON_EVENT(&event->button);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOUSE_RELEASE, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_mouse_release_event(self, &event->button);
       +                }
       +                break;
       +        case LTK_MOTION_EVENT:
       +                args[0] = LTK_MAKE_ARG_MOTION_EVENT(&event->motion);
       +                if (!ltk_widget_emit_signal(self, LTK_WIDGET_SIGNAL_MOTION_NOTIFY, (ltk_callback_arglist){args, LENGTH(args)})) {
       +                        ltk_window_motion_notify_event(self, &event->motion);
       +                }
       +                break;
       +        default:
       +                ltk_window_other_event(window, event);
       +        }
       +}
       +
       +/* x and y are global! */
       +static ltk_widget *
       +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, *local_x_ret, *local_y_ret);
       +                if (!next) {
       +                        break;
       +                } 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;
       +}
       +
       +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]->crect, x, y))
       +                        return window->popups[i];
       +        }
       +        return NULL;
       +}
       +
       +static int
       +is_parent(ltk_widget *parent, ltk_widget *child) {
       +        while (child && child != parent) {
       +                child = child->parent;
       +        }
       +        return child != NULL;
       +}
       +
       +/* 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);
       +        ltk_rect rect = {local.x, local.y, r.w, r.h};
       +        if (widget->vtable->nearest_child)
       +                return widget->vtable->nearest_child(widget, rect);
       +        return NULL;
       +}
       +
       +/* 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};
       +        ltk_widget *last_activatable = NULL;
       +        if (!cur) {
       +                cur = window->root_widget;
       +                if (cur->vtable->flags & act_flags)
       +                        last_activatable = cur;
       +                ltk_rect r = {cur->lrect.w, cur->lrect.h, 0, 0};
       +                while ((new = nearest_child(cur, r))) {
       +                        cur = new;
       +                        if (cur->vtable->flags & act_flags)
       +                                last_activatable = cur;
       +                }
       +        }
       +        if (last_activatable) {
       +                cur = last_activatable;
       +        } else 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};
       +                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 ((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;
       +                                }
       +                        }
       +                }
       +        }
       +        /* 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};
       +        int found_activatable = 0;
       +        if (!cur) {
       +                cur = window->root_widget;
       +                if (!(cur->vtable->flags & act_flags)) {
       +                        while ((new = nearest_child(cur, (ltk_rect){0, 0, 0, 0}))) {
       +                                cur = new;
       +                                if (cur->vtable->flags & act_flags) {
       +                                        found_activatable = 1;
       +                                        break;
       +                                }
       +                        }
       +                }
       +        }
       +        if (!found_activatable) {
       +                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 ((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 ((new = nearest_child(cur, old_rect))) {
       +                                                cur = new;
       +                                                if (cur->vtable->flags & act_flags) {
       +                                                        changed = 1;
       +                                                        break;
       +                                                }
       +                                        }
       +                                        if (changed)
       +                                                break;
       +                                } else {
       +                                        cur = cur->parent;
       +                                }
       +                        }
       +                }
       +        }
       +        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;
       +}
   DIR diff --git a/src/window.h b/src/ltk/window.h
   DIR diff --git a/src/ltkd/.gitignore b/src/ltkd/.gitignore
       t@@ -0,0 +1,4 @@
       +*.o
       +ltkd
       +ltkc
       +ltkc_img
   DIR diff --git a/src/ltkd/box.c b/src/ltkd/box.c
       t@@ -0,0 +1,108 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/box.h>
       +
       +/* box <box id> add <widget id> [sticky] */
       +static int
       +ltkd_box_cmd_add(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_box *box = LTK_CAST_BOX(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
       +                {.type = CMDARG_STICKY, .optional = 1},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (ltk_box_add(box, cmd[0].val.widget->widget, cmd[1].initialized ? cmd[1].val.sticky : 0)) {
       +                err->type = ERR_WIDGET_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* box <box id> remove <widget id> */
       +static int
       +ltkd_box_cmd_remove(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_box *box = LTK_CAST_BOX(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (ltk_box_remove(box, cmd[0].val.widget->widget)) {
       +                err->type = ERR_WIDGET_NOT_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* box <box id> create <orientation> */
       +static int
       +ltkd_box_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_ORIENTATION, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_box *box = ltk_box_create(window, cmd[3].val.orient);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(box), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(box), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info box_cmds[] = {
       +        {"add", &ltkd_box_cmd_add, 0},
       +        {"create", &ltkd_box_cmd_create, 1},
       +        {"remove", &ltkd_box_cmd_remove, 0},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_box_cmd, LTK_WIDGET_BOX, box_cmds)
   DIR diff --git a/src/ltkd/button.c b/src/ltkd/button.c
       t@@ -0,0 +1,72 @@
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +#include "proto_types.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/button.h>
       +
       +static int
       +ltkd_button_press(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        (void)args;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        return ltkd_widget_queue_specific_event(widget, "button", LTKD_PWEVENTMASK_BUTTON_PRESS, "press");
       +}
       +
       +static ltkd_event_handler handlers[] = {
       +        {&ltkd_button_press, LTK_BUTTON_SIGNAL_PRESSED},
       +};
       +
       +/* button <button id> create <text> */
       +static int
       +ltkd_button_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_button *button = ltk_button_create(window, cmd[3].val.str);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(button), cmd[1].val.str, handlers, LENGTH(handlers), err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(button), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info button_cmds[] = {
       +        {"create", &ltkd_button_cmd_create, 1},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_button_cmd, LTK_WIDGET_BUTTON, button_cmds)
   DIR diff --git a/src/ltkd/cmd.c b/src/ltkd/cmd.c
       t@@ -0,0 +1,185 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include "cmd.h"
       +
       +#include <string.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/color.h>
       +#include <ltk/graphics.h>
       +
       +int
       +ltkd_parse_cmd(
       +    ltkd_cmd_token *tokens, size_t num_tokens,
       +    ltkd_cmdarg_parseinfo *parseinfo, size_t num_arg, ltkd_error *err) {
       +        const char *errstr = NULL;
       +        ltk_renderdata *renderdata = ltk_get_renderer();
       +        if (num_tokens > num_arg || (num_tokens < num_arg && !parseinfo[num_tokens].optional)) {
       +                err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
       +                err->arg = -1;
       +                return 1;
       +        }
       +        size_t i = 0;
       +        for (; i < num_tokens; i++) {
       +                if (parseinfo[i].type != CMDARG_DATA && tokens[i].contains_nul) {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = i;
       +                        goto error;
       +                }
       +                switch (parseinfo[i].type) {
       +                case CMDARG_INT:
       +                        parseinfo[i].val.i = ltk_strtonum(tokens[i].text, parseinfo[i].min, parseinfo[i].max, &errstr);
       +                        if (errstr) {
       +                                err->type = ERR_INVALID_ARGUMENT;
       +                                err->arg = i;
       +                                goto error;
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_STICKY:
       +                        parseinfo[i].val.sticky = LTK_STICKY_NONE;
       +                        for (const char *c = tokens[i].text; *c != '\0'; c++) {
       +                                switch (*c) {
       +                                case 't':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_TOP;
       +                                        break;
       +                                case 'b':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_BOTTOM;
       +                                        break;
       +                                case 'r':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_RIGHT;
       +                                        break;
       +                                case 'l':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_LEFT;
       +                                        break;
       +                                case 'w':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_WIDTH;
       +                                        break;
       +                                case 'h':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_SHRINK_HEIGHT;
       +                                        break;
       +                                case 'p':
       +                                        parseinfo[i].val.sticky |= LTK_STICKY_PRESERVE_ASPECT_RATIO;
       +                                        break;
       +                                default:
       +                                        err->type = ERR_INVALID_ARGUMENT;
       +                                        err->arg = i;
       +                                        goto error;
       +                                }
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_WIDGET:
       +                        parseinfo[i].val.widget = ltkd_get_widget(tokens[i].text, parseinfo[i].widget_type, err);
       +                        if (!parseinfo[i].val.widget) {
       +                                err->arg = i;
       +                                goto error;
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_STRING:
       +                        parseinfo[i].val.str = tokens[i].text;
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_DATA:
       +                        parseinfo[i].val.data = tokens[i].text;
       +                        parseinfo[i].initialized = 1;
       +                        parseinfo[i].len = tokens[i].len;
       +                        break;
       +                case CMDARG_COLOR:
       +                        if (!(parseinfo[i].val.color = ltk_color_create(renderdata, tokens[i].text))) {
       +                                /* FIXME: this could fail even if the argument is fine */
       +                                err->type = ERR_INVALID_ARGUMENT;
       +                                err->arg = i;
       +                                goto error;
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_BOOL:
       +                        if (strcmp(tokens[i].text, "true") == 0) {
       +                                parseinfo[i].val.b = 1;
       +                        } else if (strcmp(tokens[i].text, "false") == 0) {
       +                                parseinfo[i].val.b = 0;
       +                        } else {
       +                                err->type = ERR_INVALID_ARGUMENT;
       +                                err->arg = i;
       +                                goto error;
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_ORIENTATION:
       +                        if (strcmp(tokens[i].text, "horizontal") == 0) {
       +                                parseinfo[i].val.orient = LTK_HORIZONTAL;
       +                        } else if (strcmp(tokens[i].text, "vertical") == 0) {
       +                                parseinfo[i].val.orient = LTK_VERTICAL;
       +                        } else {
       +                                err->type = ERR_INVALID_ARGUMENT;
       +                                err->arg = i;
       +                                goto error;
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_BORDERSIDES:
       +                        parseinfo[i].val.border = LTK_BORDER_NONE;
       +                        for (const char *c = tokens[i].text; *c != '\0'; c++) {
       +                                switch (*c) {
       +                                case 't':
       +                                        parseinfo[i].val.border |= LTK_BORDER_TOP;
       +                                        break;
       +                                case 'b':
       +                                        parseinfo[i].val.border |= LTK_BORDER_BOTTOM;
       +                                        break;
       +                                case 'l':
       +                                        parseinfo[i].val.border |= LTK_BORDER_LEFT;
       +                                        break;
       +                                case 'r':
       +                                        parseinfo[i].val.border |= LTK_BORDER_RIGHT;
       +                                        break;
       +                                default:
       +                                        err->type = ERR_INVALID_ARGUMENT;
       +                                        err->arg = i;
       +                                        goto error;
       +                                }
       +                        }
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                case CMDARG_IGNORE:
       +                        parseinfo[i].initialized = 1;
       +                        break;
       +                default:
       +                        ltkd_fatal("Invalid command argument type. This should not happen.\n");
       +                        /* TODO: ltk_assert(0); */
       +                }
       +        }
       +        for (; i < num_arg; i++) {
       +                parseinfo[i].initialized = 0;
       +        }
       +        return 0;
       +error:
       +        for (; i-- > 0;) {
       +                if (parseinfo[i].type == CMDARG_COLOR) {
       +                        ltk_color_destroy(renderdata, parseinfo[i].val.color);
       +                }
       +                parseinfo[i].initialized = 0;
       +        }
       +        return 1;
       +}
   DIR diff --git a/src/ltkd/cmd.h b/src/ltkd/cmd.h
       t@@ -0,0 +1,157 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_CMD_H
       +#define LTKD_CMD_H
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/color.h>
       +#include <ltk/graphics.h>
       +
       +typedef struct {
       +        char *name;
       +        int (*func)(ltk_window *, ltkd_widget *, ltkd_cmd_token *, size_t, ltkd_error *);
       +        int needs_all;
       +} ltkd_cmd_info;
       +
       +typedef enum {
       +        CMDARG_IGNORE,
       +        CMDARG_STRING, /* nul-terminated string */
       +        CMDARG_DATA,   /* also char*, but may contain nul */
       +        CMDARG_COLOR,
       +        CMDARG_INT,
       +        CMDARG_BOOL,
       +        CMDARG_BORDERSIDES,
       +        CMDARG_STICKY,
       +        CMDARG_WIDGET,
       +        CMDARG_ORIENTATION
       +} ltkd_cmdarg_datatype;
       +
       +/* color needs to be destroyed by cmd handling function if it is not needed anymore */
       +/* str must *not* be freed because it is a pointer to the original argument
       +   -> it must be copied if it needs to be kept around */
       +typedef struct {
       +        ltkd_cmdarg_datatype type;
       +        /* Note: Bool and int are both integers, but they are
       +           separate just to make it a bit clearer (same goes for str/data) */
       +        union {
       +                char *str;
       +                char *data;
       +                ltk_color *color;
       +                int i;
       +                int b;
       +                ltk_border_sides border;
       +                ltk_sticky_mask sticky;
       +                ltk_orientation orient;
       +                ltkd_widget *widget;
       +        } val;
       +        size_t len; /* only for data */
       +        int min, max; /* only for integers */ /* FIXME: which integer type is sensible here? */
       +        ltk_widget_type widget_type; /* only for widgets */
       +        int optional;
       +        int initialized;
       +} ltkd_cmdarg_parseinfo;
       +
       +/* Returns 1 on error, 0 on success */
       +/* All optional arguments must be in one block at the end */
       +int ltkd_parse_cmd(
       +    ltkd_cmd_token *tokens, size_t num_tokens,
       +    ltkd_cmdarg_parseinfo *parseinfo, size_t num_arg, ltkd_error *err
       +);
       +
       +/* FIXME: This doesn't really need to be a macro anymore since it doesn't have to be generic for different types anymore */
       +#define GEN_CMD_HELPERS_PROTO(func_name)                                                                        \
       +int func_name(ltk_window *window, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err);
       +
       +
       +#define GEN_CMD_HELPERS(func_name, widget_type, array_name)                                                        \
       +/* FIXME: maybe just get rid of this and rely on array already being sorted? */                                        \
       +static int array_name##_sorted = 0;                                                                                \
       +                                                                                                                \
       +static int                                                                                                        \
       +array_name##_search_helper(const void *keyv, const void *entryv) {                                                \
       +        const char *key = (const char *)keyv;                                                                        \
       +        ltkd_cmd_info *entry = (ltkd_cmd_info *)entryv;                                                                \
       +        return strcmp(key, entry->name);                                                                        \
       +}                                                                                                                \
       +                                                                                                                \
       +static int                                                                                                        \
       +array_name##_sort_helper(const void *entry1v, const void *entry2v) {                                                \
       +        ltkd_cmd_info *entry1 = (ltkd_cmd_info *)entry1v;                                                        \
       +        ltkd_cmd_info *entry2 = (ltkd_cmd_info *)entry2v;                                                        \
       +        return strcmp(entry1->name, entry2->name);                                                                \
       +}                                                                                                                \
       +                                                                                                                \
       +int                                                                                                                \
       +func_name(                                                                                                        \
       +    ltk_window *window,                                                                                                \
       +    ltkd_cmd_token *tokens,                                                                                        \
       +    size_t num_tokens,                                                                                                \
       +    ltkd_error *err) {                                                                                                \
       +        if (num_tokens < 3) {                                                                                        \
       +                err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;                                                        \
       +                err->arg = -1;                                                                                        \
       +                return 1;                                                                                        \
       +        }                                                                                                        \
       +        /* just in case */                                                                                        \
       +        if (!array_name##_sorted) {                                                                                \
       +                qsort(                                                                                                \
       +                    array_name, LENGTH(array_name),                                                                \
       +                    sizeof(array_name[0]), &array_name##_sort_helper);                                                \
       +                array_name##_sorted = 1;                                                                        \
       +        }                                                                                                        \
       +        if (tokens[1].contains_nul) {                                                                                \
       +                err->type = ERR_INVALID_ARGUMENT;                                                                \
       +                err->arg = 1;                                                                                        \
       +                return 1;                                                                                        \
       +        } else if (tokens[2].contains_nul) {                                                                        \
       +                err->type = ERR_INVALID_ARGUMENT;                                                                \
       +                err->arg = 2;                                                                                        \
       +                return 1;                                                                                        \
       +        }                                                                                                        \
       +        ltkd_cmd_info *e = bsearch(                                                                                \
       +            tokens[2].text, array_name, LENGTH(array_name),                                                        \
       +            sizeof(array_name[0]), &array_name##_search_helper                                                        \
       +        );                                                                                                        \
       +        if (!e) {                                                                                                \
       +                err->type = ERR_INVALID_COMMAND;                                                                \
       +                err->arg = -1;                                                                                        \
       +                return 1;                                                                                        \
       +        }                                                                                                        \
       +        if (e->needs_all) {                                                                                        \
       +                return e->func(window, NULL, tokens, num_tokens, err);                                                \
       +        } else {                                                                                                \
       +                ltkd_widget *widget = ltkd_get_widget(tokens[1].text, widget_type, err);                        \
       +                if (!widget) {                                                                                        \
       +                        err->arg = 1;                                                                                \
       +                        return 1;                                                                                \
       +                }                                                                                                \
       +                int ret = e->func(window, widget, tokens + 3, num_tokens - 3, err);                                \
       +                if (ret && err->arg >= 0)                                                                        \
       +                        err->arg += 3;                                                                                \
       +                return ret;                                                                                        \
       +        }                                                                                                        \
       +        return 0; /* Well, I guess this is impossible anyways... */                                                \
       +}
       +
       +#endif /* LTKD_CMD_H */
   DIR diff --git a/src/ltkd/cmd_helpers.h b/src/ltkd/cmd_helpers.h
       t@@ -0,0 +1,31 @@
       +/*
       + * Copyright (c) 2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_CMD_HELPERS_H
       +#define LTKD_CMD_HELPERS_H
       +
       +#include "cmd.h"
       +
       +GEN_CMD_HELPERS_PROTO(ltkd_box_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_button_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_entry_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_grid_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_image_widget_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_label_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_menu_cmd)
       +GEN_CMD_HELPERS_PROTO(ltkd_menuentry_cmd)
       +
       +#endif /* LTKD_CMD_HELPERS_H */
   DIR diff --git a/src/ltkd/entry.c b/src/ltkd/entry.c
       t@@ -0,0 +1,60 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/entry.h>
       +
       +/* FIXME: make text optional, command set-text */
       +/* entry <entry id> create <text> */
       +static int
       +ltkd_entry_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_entry *entry = ltk_entry_create(window, cmd[3].val.str);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(entry), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(entry), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info entry_cmds[] = {
       +        {"create", &ltkd_entry_cmd_create, 1},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_entry_cmd, LTK_WIDGET_ENTRY, entry_cmds)
   DIR diff --git a/src/ltkd/err.c b/src/ltkd/err.c
       t@@ -0,0 +1,46 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include "err.h"
       +
       +static const char *errtable[] = {
       +        "None",
       +        "Widget already in container",
       +        "Widget not in container",
       +        "Widget id already in use",
       +        "Invalid number of arguments",
       +        "Invalid argument",
       +        "Invalid index",
       +        "Invalid widget id",
       +        "Invalid widget type",
       +        "Invalid command",
       +        "Unknown error",
       +        "Menu is not submenu",
       +        "Menu entry already contains submenu",
       +        "Invalid grid position",
       +        "Invalid sequence number",
       +};
       +
       +#define LENGTH(X) (sizeof(X) / sizeof(X[0]))
       +
       +const char *
       +errtype_to_string(ltkd_errtype type) {
       +        if (type < 0 || type >= LENGTH(errtable)) {
       +                /* that's a funny error, now isn't it? */
       +                return "Invalid error";
       +        }
       +        return errtable[type];
       +}
   DIR diff --git a/src/ltkd/err.h b/src/ltkd/err.h
       t@@ -0,0 +1,49 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_ERR_H
       +#define LTKD_ERR_H
       +
       +/* WARNING: THIS NEEDS TO BE KEPT IN SYNC WITH THE TABLE IN err.c! */
       +/* (also, the explicit value setting is redundant, but just in case) */
       +typedef enum {
       +        ERR_NONE = 0,
       +        ERR_WIDGET_IN_CONTAINER = 1,
       +        ERR_WIDGET_NOT_IN_CONTAINER = 2,
       +        ERR_WIDGET_ID_IN_USE = 3,
       +        ERR_INVALID_NUMBER_OF_ARGUMENTS = 4,
       +        ERR_INVALID_ARGUMENT = 5,
       +        ERR_INVALID_INDEX = 6,
       +        ERR_INVALID_WIDGET_ID = 7,
       +        ERR_INVALID_WIDGET_TYPE = 8,
       +        ERR_INVALID_COMMAND = 9,
       +        ERR_UNKNOWN = 10,
       +        /* widget specific */
       +        ERR_MENU_NOT_SUBMENU = 11,
       +        ERR_MENU_ENTRY_CONTAINS_SUBMENU = 12,
       +        ERR_GRID_INVALID_POSITION = 13,
       +        ERR_INVALID_SEQNUM = 14,
       +} ltkd_errtype;
       +
       +typedef struct {
       +        ltkd_errtype type;
       +        /* corresponding argument, -1 if none */
       +        int arg;
       +} ltkd_error;
       +
       +const char *errtype_to_string(ltkd_errtype type);
       +
       +#endif /* LTKD_ERR_H */
   DIR diff --git a/src/ltkd/grid.c b/src/ltkd/grid.c
       t@@ -0,0 +1,174 @@
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/grid.h>
       +
       +/* grid <grid id> add <widget id> <row> <column> <row_span> <column_span> [sticky] */
       +static int
       +ltkd_grid_cmd_add(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_grid *grid = LTK_CAST_GRID(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = grid->rows - 1, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = grid->columns - 1, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = grid->rows, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = grid->columns, .optional = 0},
       +                {.type = CMDARG_STICKY, .optional = 1},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        int row = cmd[1].val.i, col = cmd[2].val.i;
       +        int rowspan = cmd[3].val.i, colspan = cmd[4].val.i;
       +        if (row + rowspan > grid->rows) {
       +                err->type = ERR_GRID_INVALID_POSITION;
       +                err->arg = 1;
       +        }
       +        if (col + colspan > grid->columns) {
       +                err->type = ERR_GRID_INVALID_POSITION;
       +                err->arg = 2;
       +        }
       +        /* FIXME: better error reporting for invalid grid position */
       +        /* FIXME: check if recalculation deals properly with rowspan/columnspan
       +           that goes over the edge of the grid */
       +        if (ltk_grid_add(
       +            grid, cmd[0].val.widget->widget,
       +            row, col, rowspan, colspan,
       +            cmd[5].initialized ? cmd[5].val.sticky : 0)) {
       +                err->type = ERR_WIDGET_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* grid <grid id> remove <widget id> */
       +static int
       +ltkd_grid_cmd_ungrid(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_grid *grid = LTK_CAST_GRID(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_UNKNOWN, .optional = 0}
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (ltk_grid_remove(grid, cmd[0].val.widget->widget)) {
       +                err->type = ERR_WIDGET_NOT_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* FIXME: max size of 64 is completely arbitrary! */
       +/* grid <grid id> create <rows> <columns> */
       +static int
       +ltkd_grid_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_grid *grid = ltk_grid_create(window, cmd[3].val.i, cmd[4].val.i);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(grid), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(grid), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +/* FIXME: 64 is completely arbitrary */
       +/* grid <grid id> set-row-weight <row> <weight> */
       +static int
       +ltkd_grid_cmd_set_row_weight(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_grid *grid = LTK_CAST_GRID(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_INT, .min = 0, .max = grid->rows - 1, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_grid_set_row_weight(grid, cmd[0].val.i, cmd[1].val.i);
       +
       +        return 0;
       +}
       +
       +
       +/* FIXME: 64 is completely arbitrary */
       +/* FIXME: check for overflows in various grid calculations (at least when larger values are allowed) */
       +/* grid <grid id> set-column-weight <column> <weight> */
       +static int
       +ltkd_grid_cmd_set_column_weight(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_grid *grid = LTK_CAST_GRID(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_INT, .min = 0, .max = grid->columns - 1, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = 64, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_grid_set_column_weight(grid, cmd[0].val.i, cmd[1].val.i);
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info grid_cmds[] = {
       +        {"add", &ltkd_grid_cmd_add, 0},
       +        {"create", &ltkd_grid_cmd_create, 1},
       +        {"remove", &ltkd_grid_cmd_ungrid, 0},
       +        {"set-column-weight", &ltkd_grid_cmd_set_column_weight, 0},
       +        {"set-row-weight", &ltkd_grid_cmd_set_row_weight, 0},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_grid_cmd, LTK_WIDGET_GRID, grid_cmds)
   DIR diff --git a/src/ltkd/image_widget.c b/src/ltkd/image_widget.c
       t@@ -0,0 +1,68 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/image.h>
       +#include <ltk/image_widget.h>
       +
       +/* image <image id> create <filename> <data> */
       +static int
       +ltkd_image_widget_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_DATA, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_image *img = ltk_image_create_from_mem(cmd[3].val.str, cmd[4].val.data, cmd[4].len);
       +        if (!img) {
       +                /* FIXME: more sensible error name */
       +                err->type = ERR_UNKNOWN;
       +                err->arg = -1;
       +                return 1;
       +        }
       +        ltk_image_widget *imgw = ltk_image_widget_create(window, img);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(imgw), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(imgw), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info image_widget_cmds[] = {
       +        {"create", &ltkd_image_widget_cmd_create, 1},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_image_widget_cmd, LTK_WIDGET_IMAGE, image_widget_cmds)
   DIR diff --git a/src/ltkd/khash.h b/src/ltkd/khash.h
       t@@ -0,0 +1,627 @@
       +/* The MIT License
       +
       +   Copyright (c) 2008, 2009, 2011 by Attractive Chaos <attractor@live.co.uk>
       +
       +   Permission is hereby granted, free of charge, to any person obtaining
       +   a copy of this software and associated documentation files (the
       +   "Software"), to deal in the Software without restriction, including
       +   without limitation the rights to use, copy, modify, merge, publish,
       +   distribute, sublicense, and/or sell copies of the Software, and to
       +   permit persons to whom the Software is furnished to do so, subject to
       +   the following conditions:
       +
       +   The above copyright notice and this permission notice shall be
       +   included in all copies or substantial portions of the Software.
       +
       +   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
       +   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
       +   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
       +   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
       +   BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
       +   ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
       +   CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
       +   SOFTWARE.
       +*/
       +
       +/*
       +  An example:
       +
       +#include "khash.h"
       +KHASH_MAP_INIT_INT(32, char)
       +int main() {
       +        int ret, is_missing;
       +        khiter_t k;
       +        khash_t(32) *h = kh_init(32);
       +        k = kh_put(32, h, 5, &ret);
       +        kh_value(h, k) = 10;
       +        k = kh_get(32, h, 10);
       +        is_missing = (k == kh_end(h));
       +        k = kh_get(32, h, 5);
       +        kh_del(32, h, k);
       +        for (k = kh_begin(h); k != kh_end(h); ++k)
       +                if (kh_exist(h, k)) kh_value(h, k) = 1;
       +        kh_destroy(32, h);
       +        return 0;
       +}
       +*/
       +
       +/*
       +  2013-05-02 (0.2.8):
       +
       +        * Use quadratic probing. When the capacity is power of 2, stepping function
       +          i*(i+1)/2 guarantees to traverse each bucket. It is better than double
       +          hashing on cache performance and is more robust than linear probing.
       +
       +          In theory, double hashing should be more robust than quadratic probing.
       +          However, my implementation is probably not for large hash tables, because
       +          the second hash function is closely tied to the first hash function,
       +          which reduce the effectiveness of double hashing.
       +
       +        Reference: http://research.cs.vt.edu/AVresearch/hashing/quadratic.php
       +
       +  2011-12-29 (0.2.7):
       +
       +    * Minor code clean up; no actual effect.
       +
       +  2011-09-16 (0.2.6):
       +
       +        * The capacity is a power of 2. This seems to dramatically improve the
       +          speed for simple keys. Thank Zilong Tan for the suggestion. Reference:
       +
       +           - http://code.google.com/p/ulib/
       +           - http://nothings.org/computer/judy/
       +
       +        * Allow to optionally use linear probing which usually has better
       +          performance for random input. Double hashing is still the default as it
       +          is more robust to certain non-random input.
       +
       +        * Added Wang's integer hash function (not used by default). This hash
       +          function is more robust to certain non-random input.
       +
       +  2011-02-14 (0.2.5):
       +
       +    * Allow to declare global functions.
       +
       +  2009-09-26 (0.2.4):
       +
       +    * Improve portability
       +
       +  2008-09-19 (0.2.3):
       +
       +        * Corrected the example
       +        * Improved interfaces
       +
       +  2008-09-11 (0.2.2):
       +
       +        * Improved speed a little in kh_put()
       +
       +  2008-09-10 (0.2.1):
       +
       +        * Added kh_clear()
       +        * Fixed a compiling error
       +
       +  2008-09-02 (0.2.0):
       +
       +        * Changed to token concatenation which increases flexibility.
       +
       +  2008-08-31 (0.1.2):
       +
       +        * Fixed a bug in kh_get(), which has not been tested previously.
       +
       +  2008-08-31 (0.1.1):
       +
       +        * Added destructor
       +*/
       +
       +
       +#ifndef __AC_KHASH_H
       +#define __AC_KHASH_H
       +
       +/*!
       +  @header
       +
       +  Generic hash table library.
       + */
       +
       +#define AC_VERSION_KHASH_H "0.2.8"
       +
       +#include <stdlib.h>
       +#include <string.h>
       +#include <limits.h>
       +
       +/* compiler specific configuration */
       +
       +#if UINT_MAX == 0xffffffffu
       +typedef unsigned int khint32_t;
       +#elif ULONG_MAX == 0xffffffffu
       +typedef unsigned long khint32_t;
       +#endif
       +
       +#if ULONG_MAX == ULLONG_MAX
       +typedef unsigned long khint64_t;
       +#else
       +typedef unsigned long long khint64_t;
       +#endif
       +
       +#ifndef kh_inline
       +#ifdef _MSC_VER
       +#define kh_inline __inline
       +#else
       +#define kh_inline inline
       +#endif
       +#endif /* kh_inline */
       +
       +#ifndef klib_unused
       +#if (defined __clang__ && __clang_major__ >= 3) || (defined __GNUC__ && __GNUC__ >= 3)
       +#define klib_unused __attribute__ ((__unused__))
       +#else
       +#define klib_unused
       +#endif
       +#endif /* klib_unused */
       +
       +typedef khint32_t khint_t;
       +typedef khint_t khiter_t;
       +
       +#define __ac_isempty(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&2)
       +#define __ac_isdel(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&1)
       +#define __ac_iseither(flag, i) ((flag[i>>4]>>((i&0xfU)<<1))&3)
       +#define __ac_set_isdel_false(flag, i) (flag[i>>4]&=~(1ul<<((i&0xfU)<<1)))
       +#define __ac_set_isempty_false(flag, i) (flag[i>>4]&=~(2ul<<((i&0xfU)<<1)))
       +#define __ac_set_isboth_false(flag, i) (flag[i>>4]&=~(3ul<<((i&0xfU)<<1)))
       +#define __ac_set_isdel_true(flag, i) (flag[i>>4]|=1ul<<((i&0xfU)<<1))
       +
       +#define __ac_fsize(m) ((m) < 16? 1 : (m)>>4)
       +
       +#ifndef kroundup32
       +#define kroundup32(x) (--(x), (x)|=(x)>>1, (x)|=(x)>>2, (x)|=(x)>>4, (x)|=(x)>>8, (x)|=(x)>>16, ++(x))
       +#endif
       +
       +#ifndef kcalloc
       +#define kcalloc(N,Z) calloc(N,Z)
       +#endif
       +#ifndef kmalloc
       +#define kmalloc(Z) malloc(Z)
       +#endif
       +#ifndef krealloc
       +#define krealloc(P,Z) realloc(P,Z)
       +#endif
       +#ifndef kfree
       +#define kfree(P) free(P)
       +#endif
       +
       +static const double __ac_HASH_UPPER = 0.77;
       +
       +#define __KHASH_TYPE(name, khkey_t, khval_t) \
       +        typedef struct kh_##name##_s { \
       +                khint_t n_buckets, size, n_occupied, upper_bound; \
       +                khint32_t *flags; \
       +                khkey_t *keys; \
       +                khval_t *vals; \
       +        } kh_##name##_t;
       +
       +#define __KHASH_PROTOTYPES(name, khkey_t, khval_t)                                                 \
       +        extern kh_##name##_t *kh_init_##name(void);                                                        \
       +        extern void kh_destroy_##name(kh_##name##_t *h);                                        \
       +        extern void kh_clear_##name(kh_##name##_t *h);                                                \
       +        extern khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key);         \
       +        extern int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets); \
       +        extern khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret); \
       +        extern void kh_del_##name(kh_##name##_t *h, khint_t x);
       +
       +#define __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
       +        SCOPE kh_##name##_t *kh_init_##name(void) {                                                        \
       +                return (kh_##name##_t*)kcalloc(1, sizeof(kh_##name##_t));                \
       +        }                                                                                                                                        \
       +        SCOPE void kh_destroy_##name(kh_##name##_t *h)                                                \
       +        {                                                                                                                                        \
       +                if (h) {                                                                                                                \
       +                        kfree((void *)h->keys); kfree(h->flags);                                        \
       +                        kfree((void *)h->vals);                                                                                \
       +                        kfree(h);                                                                                                        \
       +                }                                                                                                                                \
       +        }                                                                                                                                        \
       +        SCOPE void kh_clear_##name(kh_##name##_t *h)                                                \
       +        {                                                                                                                                        \
       +                if (h && h->flags) {                                                                                        \
       +                        memset(h->flags, 0xaa, __ac_fsize(h->n_buckets) * sizeof(khint32_t)); \
       +                        h->size = h->n_occupied = 0;                                                                \
       +                }                                                                                                                                \
       +        }                                                                                                                                        \
       +        SCOPE khint_t kh_get_##name(const kh_##name##_t *h, khkey_t key)         \
       +        {                                                                                                                                        \
       +                if (h->n_buckets) {                                                                                                \
       +                        khint_t k, i, last, mask, step = 0; \
       +                        mask = h->n_buckets - 1;                                                                        \
       +                        k = __hash_func(key); i = k & mask;                                                        \
       +                        last = i; \
       +                        while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
       +                                i = (i + (++step)) & mask; \
       +                                if (i == last) return h->n_buckets;                                                \
       +                        }                                                                                                                        \
       +                        return __ac_iseither(h->flags, i)? h->n_buckets : i;                \
       +                } else return 0;                                                                                                \
       +        }                                                                                                                                        \
       +        SCOPE int kh_resize_##name(kh_##name##_t *h, khint_t new_n_buckets) \
       +        { /* This function uses 0.25*n_buckets bytes of working space instead of [sizeof(key_t+val_t)+.25]*n_buckets. */ \
       +                khint32_t *new_flags = 0;                                                                                \
       +                khint_t j = 1;                                                                                                        \
       +                {                                                                                                                                \
       +                        kroundup32(new_n_buckets);                                                                         \
       +                        if (new_n_buckets < 4) new_n_buckets = 4;                                        \
       +                        if (h->size >= (khint_t)(new_n_buckets * __ac_HASH_UPPER + 0.5)) j = 0;        /* requested size is too small */ \
       +                        else { /* hash table size to be changed (shrink or expand); rehash */ \
       +                                new_flags = (khint32_t*)kmalloc(__ac_fsize(new_n_buckets) * sizeof(khint32_t));        \
       +                                if (!new_flags) return -1;                                                                \
       +                                memset(new_flags, 0xaa, __ac_fsize(new_n_buckets) * sizeof(khint32_t)); \
       +                                if (h->n_buckets < new_n_buckets) {        /* expand */                \
       +                                        khkey_t *new_keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
       +                                        if (!new_keys) { kfree(new_flags); return -1; }                \
       +                                        h->keys = new_keys;                                                                        \
       +                                        if (kh_is_map) {                                                                        \
       +                                                khval_t *new_vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
       +                                                if (!new_vals) { kfree(new_flags); return -1; }        \
       +                                                h->vals = new_vals;                                                                \
       +                                        }                                                                                                        \
       +                                } /* otherwise shrink */                                                                \
       +                        }                                                                                                                        \
       +                }                                                                                                                                \
       +                if (j) { /* rehashing is needed */                                                                \
       +                        for (j = 0; j != h->n_buckets; ++j) {                                                \
       +                                if (__ac_iseither(h->flags, j) == 0) {                                        \
       +                                        khkey_t key = h->keys[j];                                                        \
       +                                        khval_t val;                                                                                \
       +                                        khint_t new_mask;                                                                        \
       +                                        new_mask = new_n_buckets - 1;                                                 \
       +                                        if (kh_is_map) val = h->vals[j];                                        \
       +                                        __ac_set_isdel_true(h->flags, j);                                        \
       +                                        while (1) { /* kick-out process; sort of like in Cuckoo hashing */ \
       +                                                khint_t k, i, step = 0; \
       +                                                k = __hash_func(key);                                                        \
       +                                                i = k & new_mask;                                                                \
       +                                                while (!__ac_isempty(new_flags, i)) i = (i + (++step)) & new_mask; \
       +                                                __ac_set_isempty_false(new_flags, i);                        \
       +                                                if (i < h->n_buckets && __ac_iseither(h->flags, i) == 0) { /* kick out the existing element */ \
       +                                                        { khkey_t tmp = h->keys[i]; h->keys[i] = key; key = tmp; } \
       +                                                        if (kh_is_map) { khval_t tmp = h->vals[i]; h->vals[i] = val; val = tmp; } \
       +                                                        __ac_set_isdel_true(h->flags, i); /* mark it as deleted in the old hash table */ \
       +                                                } else { /* write the element and jump out of the loop */ \
       +                                                        h->keys[i] = key;                                                        \
       +                                                        if (kh_is_map) h->vals[i] = val;                        \
       +                                                        break;                                                                                \
       +                                                }                                                                                                \
       +                                        }                                                                                                        \
       +                                }                                                                                                                \
       +                        }                                                                                                                        \
       +                        if (h->n_buckets > new_n_buckets) { /* shrink the hash table */ \
       +                                h->keys = (khkey_t*)krealloc((void *)h->keys, new_n_buckets * sizeof(khkey_t)); \
       +                                if (kh_is_map) h->vals = (khval_t*)krealloc((void *)h->vals, new_n_buckets * sizeof(khval_t)); \
       +                        }                                                                                                                        \
       +                        kfree(h->flags); /* free the working space */                                \
       +                        h->flags = new_flags;                                                                                \
       +                        h->n_buckets = new_n_buckets;                                                                \
       +                        h->n_occupied = h->size;                                                                        \
       +                        h->upper_bound = (khint_t)(h->n_buckets * __ac_HASH_UPPER + 0.5); \
       +                }                                                                                                                                \
       +                return 0;                                                                                                                \
       +        }                                                                                                                                        \
       +        SCOPE khint_t kh_put_##name(kh_##name##_t *h, khkey_t key, int *ret) \
       +        {                                                                                                                                        \
       +                khint_t x;                                                                                                                \
       +                if (h->n_occupied >= h->upper_bound) { /* update the hash table */ \
       +                        if (h->n_buckets > (h->size<<1)) {                                                        \
       +                                if (kh_resize_##name(h, h->n_buckets - 1) < 0) { /* clear "deleted" elements */ \
       +                                        *ret = -1; return h->n_buckets;                                                \
       +                                }                                                                                                                \
       +                        } else if (kh_resize_##name(h, h->n_buckets + 1) < 0) { /* expand the hash table */ \
       +                                *ret = -1; return h->n_buckets;                                                        \
       +                        }                                                                                                                        \
       +                } /* TODO: to implement automatically shrinking; resize() already support shrinking */ \
       +                {                                                                                                                                \
       +                        khint_t k, i, site, last, mask = h->n_buckets - 1, step = 0; \
       +                        x = site = h->n_buckets; k = __hash_func(key); i = k & mask; \
       +                        if (__ac_isempty(h->flags, i)) x = i; /* for speed up */        \
       +                        else {                                                                                                                \
       +                                last = i; \
       +                                while (!__ac_isempty(h->flags, i) && (__ac_isdel(h->flags, i) || !__hash_equal(h->keys[i], key))) { \
       +                                        if (__ac_isdel(h->flags, i)) site = i;                                \
       +                                        i = (i + (++step)) & mask; \
       +                                        if (i == last) { x = site; break; }                                        \
       +                                }                                                                                                                \
       +                                if (x == h->n_buckets) {                                                                \
       +                                        if (__ac_isempty(h->flags, i) && site != h->n_buckets) x = site; \
       +                                        else x = i;                                                                                        \
       +                                }                                                                                                                \
       +                        }                                                                                                                        \
       +                }                                                                                                                                \
       +                if (__ac_isempty(h->flags, x)) { /* not present at all */                \
       +                        h->keys[x] = key;                                                                                        \
       +                        __ac_set_isboth_false(h->flags, x);                                                        \
       +                        ++h->size; ++h->n_occupied;                                                                        \
       +                        *ret = 1;                                                                                                        \
       +                } else if (__ac_isdel(h->flags, x)) { /* deleted */                                \
       +                        h->keys[x] = key;                                                                                        \
       +                        __ac_set_isboth_false(h->flags, x);                                                        \
       +                        ++h->size;                                                                                                        \
       +                        *ret = 2;                                                                                                        \
       +                } else *ret = 0; /* Don't touch h->keys[x] if present and not deleted */ \
       +                return x;                                                                                                                \
       +        }                                                                                                                                        \
       +        SCOPE void kh_del_##name(kh_##name##_t *h, khint_t x)                                \
       +        {                                                                                                                                        \
       +                if (x != h->n_buckets && !__ac_iseither(h->flags, x)) {                        \
       +                        __ac_set_isdel_true(h->flags, x);                                                        \
       +                        --h->size;                                                                                                        \
       +                }                                                                                                                                \
       +        }
       +
       +#define KHASH_DECLARE(name, khkey_t, khval_t)                                                         \
       +        __KHASH_TYPE(name, khkey_t, khval_t)                                                                 \
       +        __KHASH_PROTOTYPES(name, khkey_t, khval_t)
       +
       +#define KHASH_INIT2(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
       +        __KHASH_TYPE(name, khkey_t, khval_t)                                                                 \
       +        __KHASH_IMPL(name, SCOPE, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
       +
       +#define KHASH_INIT(name, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal) \
       +        KHASH_INIT2(name, static kh_inline klib_unused, khkey_t, khval_t, kh_is_map, __hash_func, __hash_equal)
       +
       +/* --- BEGIN OF HASH FUNCTIONS --- */
       +
       +/*! @function
       +  @abstract     Integer hash function
       +  @param  key   The integer [khint32_t]
       +  @return       The hash value [khint_t]
       + */
       +#define kh_int_hash_func(key) (khint32_t)(key)
       +/*! @function
       +  @abstract     Integer comparison function
       + */
       +#define kh_int_hash_equal(a, b) ((a) == (b))
       +/*! @function
       +  @abstract     64-bit integer hash function
       +  @param  key   The integer [khint64_t]
       +  @return       The hash value [khint_t]
       + */
       +#define kh_int64_hash_func(key) (khint32_t)((key)>>33^(key)^(key)<<11)
       +/*! @function
       +  @abstract     64-bit integer comparison function
       + */
       +#define kh_int64_hash_equal(a, b) ((a) == (b))
       +/*! @function
       +  @abstract     const char* hash function
       +  @param  s     Pointer to a null terminated string
       +  @return       The hash value
       + */
       +static kh_inline khint_t __ac_X31_hash_string(const char *s)
       +{
       +        khint_t h = (khint_t)*s;
       +        if (h) for (++s ; *s; ++s) h = (h << 5) - h + (khint_t)*s;
       +        return h;
       +}
       +/*! @function
       +  @abstract     Another interface to const char* hash function
       +  @param  key   Pointer to a null terminated string [const char*]
       +  @return       The hash value [khint_t]
       + */
       +#define kh_str_hash_func(key) __ac_X31_hash_string(key)
       +/*! @function
       +  @abstract     Const char* comparison function
       + */
       +#define kh_str_hash_equal(a, b) (strcmp(a, b) == 0)
       +
       +static kh_inline khint_t __ac_Wang_hash(khint_t key)
       +{
       +    key += ~(key << 15);
       +    key ^=  (key >> 10);
       +    key +=  (key << 3);
       +    key ^=  (key >> 6);
       +    key += ~(key << 11);
       +    key ^=  (key >> 16);
       +    return key;
       +}
       +#define kh_int_hash_func2(key) __ac_Wang_hash((khint_t)key)
       +
       +/* --- END OF HASH FUNCTIONS --- */
       +
       +/* Other convenient macros... */
       +
       +/*!
       +  @abstract Type of the hash table.
       +  @param  name  Name of the hash table [symbol]
       + */
       +#define khash_t(name) kh_##name##_t
       +
       +/*! @function
       +  @abstract     Initiate a hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @return       Pointer to the hash table [khash_t(name)*]
       + */
       +#define kh_init(name) kh_init_##name()
       +
       +/*! @function
       +  @abstract     Destroy a hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       + */
       +#define kh_destroy(name, h) kh_destroy_##name(h)
       +
       +/*! @function
       +  @abstract     Reset a hash table without deallocating memory.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       + */
       +#define kh_clear(name, h) kh_clear_##name(h)
       +
       +/*! @function
       +  @abstract     Resize a hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  s     New size [khint_t]
       + */
       +#define kh_resize(name, h, s) kh_resize_##name(h, s)
       +
       +/*! @function
       +  @abstract     Insert a key to the hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  k     Key [type of keys]
       +  @param  r     Extra return code: -1 if the operation failed;
       +                0 if the key is present in the hash table;
       +                1 if the bucket is empty (never used); 2 if the element in
       +                                the bucket has been deleted [int*]
       +  @return       Iterator to the inserted element [khint_t]
       + */
       +#define kh_put(name, h, k, r) kh_put_##name(h, k, r)
       +
       +/*! @function
       +  @abstract     Retrieve a key from the hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  k     Key [type of keys]
       +  @return       Iterator to the found element, or kh_end(h) if the element is absent [khint_t]
       + */
       +#define kh_get(name, h, k) kh_get_##name(h, k)
       +
       +/*! @function
       +  @abstract     Remove a key from the hash table.
       +  @param  name  Name of the hash table [symbol]
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  k     Iterator to the element to be deleted [khint_t]
       + */
       +#define kh_del(name, h, k) kh_del_##name(h, k)
       +
       +/*! @function
       +  @abstract     Test whether a bucket contains data.
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  x     Iterator to the bucket [khint_t]
       +  @return       1 if containing data; 0 otherwise [int]
       + */
       +#define kh_exist(h, x) (!__ac_iseither((h)->flags, (x)))
       +
       +/*! @function
       +  @abstract     Get key given an iterator
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  x     Iterator to the bucket [khint_t]
       +  @return       Key [type of keys]
       + */
       +#define kh_key(h, x) ((h)->keys[x])
       +
       +/*! @function
       +  @abstract     Get value given an iterator
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  x     Iterator to the bucket [khint_t]
       +  @return       Value [type of values]
       +  @discussion   For hash sets, calling this results in segfault.
       + */
       +#define kh_val(h, x) ((h)->vals[x])
       +
       +/*! @function
       +  @abstract     Alias of kh_val()
       + */
       +#define kh_value(h, x) ((h)->vals[x])
       +
       +/*! @function
       +  @abstract     Get the start iterator
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @return       The start iterator [khint_t]
       + */
       +#define kh_begin(h) (khint_t)(0)
       +
       +/*! @function
       +  @abstract     Get the end iterator
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @return       The end iterator [khint_t]
       + */
       +#define kh_end(h) ((h)->n_buckets)
       +
       +/*! @function
       +  @abstract     Get the number of elements in the hash table
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @return       Number of elements in the hash table [khint_t]
       + */
       +#define kh_size(h) ((h)->size)
       +
       +/*! @function
       +  @abstract     Get the number of buckets in the hash table
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @return       Number of buckets in the hash table [khint_t]
       + */
       +#define kh_n_buckets(h) ((h)->n_buckets)
       +
       +/*! @function
       +  @abstract     Iterate over the entries in the hash table
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  kvar  Variable to which key will be assigned
       +  @param  vvar  Variable to which value will be assigned
       +  @param  code  Block of code to execute
       + */
       +#define kh_foreach(h, kvar, vvar, code) { khint_t __i;                \
       +        for (__i = kh_begin(h); __i != kh_end(h); ++__i) {                \
       +                if (!kh_exist(h,__i)) continue;                                                \
       +                (kvar) = kh_key(h,__i);                                                                \
       +                (vvar) = kh_val(h,__i);                                                                \
       +                code;                                                                                                \
       +        } }
       +
       +/*! @function
       +  @abstract     Iterate over the values in the hash table
       +  @param  h     Pointer to the hash table [khash_t(name)*]
       +  @param  vvar  Variable to which value will be assigned
       +  @param  code  Block of code to execute
       + */
       +#define kh_foreach_value(h, vvar, code) { khint_t __i;                \
       +        for (__i = kh_begin(h); __i != kh_end(h); ++__i) {                \
       +                if (!kh_exist(h,__i)) continue;                                                \
       +                (vvar) = kh_val(h,__i);                                                                \
       +                code;                                                                                                \
       +        } }
       +
       +/* More conenient interfaces */
       +
       +/*! @function
       +  @abstract     Instantiate a hash set containing integer keys
       +  @param  name  Name of the hash table [symbol]
       + */
       +#define KHASH_SET_INIT_INT(name)                                                                                \
       +        KHASH_INIT(name, khint32_t, char, 0, kh_int_hash_func, kh_int_hash_equal)
       +
       +/*! @function
       +  @abstract     Instantiate a hash map containing integer keys
       +  @param  name  Name of the hash table [symbol]
       +  @param  khval_t  Type of values [type]
       + */
       +#define KHASH_MAP_INIT_INT(name, khval_t)                                                                \
       +        KHASH_INIT(name, khint32_t, khval_t, 1, kh_int_hash_func, kh_int_hash_equal)
       +
       +/*! @function
       +  @abstract     Instantiate a hash map containing 64-bit integer keys
       +  @param  name  Name of the hash table [symbol]
       + */
       +#define KHASH_SET_INIT_INT64(name)                                                                                \
       +        KHASH_INIT(name, khint64_t, char, 0, kh_int64_hash_func, kh_int64_hash_equal)
       +
       +/*! @function
       +  @abstract     Instantiate a hash map containing 64-bit integer keys
       +  @param  name  Name of the hash table [symbol]
       +  @param  khval_t  Type of values [type]
       + */
       +#define KHASH_MAP_INIT_INT64(name, khval_t)                                                                \
       +        KHASH_INIT(name, khint64_t, khval_t, 1, kh_int64_hash_func, kh_int64_hash_equal)
       +
       +typedef const char *kh_cstr_t;
       +/*! @function
       +  @abstract     Instantiate a hash map containing const char* keys
       +  @param  name  Name of the hash table [symbol]
       + */
       +#define KHASH_SET_INIT_STR(name)                                                                                \
       +        KHASH_INIT(name, kh_cstr_t, char, 0, kh_str_hash_func, kh_str_hash_equal)
       +
       +/*! @function
       +  @abstract     Instantiate a hash map containing const char* keys
       +  @param  name  Name of the hash table [symbol]
       +  @param  khval_t  Type of values [type]
       + */
       +#define KHASH_MAP_INIT_STR(name, khval_t)                                                                \
       +        KHASH_INIT(name, kh_cstr_t, khval_t, 1, kh_str_hash_func, kh_str_hash_equal)
       +
       +#endif /* __AC_KHASH_H */
   DIR diff --git a/src/ltkd/label.c b/src/ltkd/label.c
       t@@ -0,0 +1,59 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/label.h>
       +
       +/* label <label id> create <text> */
       +static int
       +ltkd_label_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_label *label = ltk_label_create(window, cmd[3].val.str);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(label), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(label), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +
       +        return 0;
       +}
       +
       +static ltkd_cmd_info label_cmds[] = {
       +        {"create", &ltkd_label_cmd_create, 1},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_label_cmd, LTK_WIDGET_LABEL, label_cmds)
   DIR diff --git a/src/ltkd/ltkc.c b/src/ltkd/ltkc.c
       t@@ -0,0 +1,272 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <errno.h>
       +#include <stdio.h>
       +#include <string.h>
       +#include <stddef.h>
       +#include <unistd.h>
       +#include <time.h>
       +#include <inttypes.h>
       +#include <sys/select.h>
       +#include <sys/socket.h>
       +#include <sys/un.h>
       +
       +#include "util.h"
       +
       +#include <ltk/util.h>
       +#include <ltk/memory.h>
       +#include <ltk/macros.h>
       +
       +#define BLK_SIZE 128
       +char tmp_buf[BLK_SIZE];
       +
       +static struct {
       +        char *in_buffer;  /* text that is read from stdin and written to the socket */
       +        int in_len;
       +        int in_alloc;
       +        char *out_buffer; /* text that is read from the socket and written to stdout */
       +        int out_len;
       +        int out_alloc;
       +} io_buffers;
       +
       +static char *ltkd_dir = NULL;
       +static char *sock_path = NULL;
       +static int sockfd = -1;
       +
       +void
       +ltkc_log_msg(const char *mode, const char *format, va_list args) {
       +        fprintf(stderr, "ltkc %s: ", mode);
       +        vfprintf(stderr, format, args);
       +}
       +
       +static void
       +ltkc_cleanup() {
       +        if (sockfd >= 0)
       +                close(sockfd);
       +        if (ltkd_dir)
       +                ltk_free(ltkd_dir);
       +        if (sock_path)
       +                ltk_free(sock_path);
       +        if (io_buffers.in_buffer)
       +                ltk_free(io_buffers.in_buffer);
       +        if (io_buffers.out_buffer)
       +                ltk_free(io_buffers.out_buffer);
       +}
       +
       +LTK_GEN_LOG_FUNC_PROTO(ltkc)
       +LTK_GEN_LOG_FUNCS(ltkc, ltkc_log_msg, ltkc_cleanup)
       +
       +int main(int argc, char *argv[]) {
       +        char num[12];
       +        int bs = 0;
       +        int last_newline = 1;
       +        int in_str = 0;
       +        uint32_t seq = 0;
       +        int maxfd;
       +        int infd = fileno(stdin);
       +        int outfd = fileno(stdout);
       +        struct sockaddr_un un;
       +        fd_set rfds, wfds, rallfds, wallfds;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 20000;
       +        size_t path_size;
       +
       +        if (argc != 2) {
       +                (void)fprintf(stderr, "USAGE: ltkc <socket id>\n");
       +                return 1;
       +        }
       +
       +        ltkd_dir = ltk_setup_directory("LTKDDIR", ".ltkd", 1);
       +        if (!ltkd_dir) {
       +                (void)fprintf(stderr, "Unable to setup ltk directory.\n");
       +                return 1;
       +        }
       +
       +        /* 7 because of "/", ".sock", and '\0' */
       +        path_size = strlen(ltkd_dir) + strlen(argv[1]) + 7;
       +        sock_path = ltk_malloc(path_size);
       +        snprintf(sock_path, path_size, "%s/%s.sock", ltkd_dir, argv[1]);
       +
       +        if ((sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
       +                perror("Socket error");
       +                return -1;
       +        }
       +        memset(&un, 0, sizeof(un));
       +        un.sun_family = AF_UNIX;
       +        if (path_size > sizeof(un.sun_path)) {
       +                (void)fprintf(stderr, "Socket path too long.\n");
       +                return 1;
       +        }
       +        strcpy(un.sun_path, sock_path);
       +        if (connect(sockfd, (struct sockaddr *)&un, offsetof(struct sockaddr_un, sun_path) + path_size) < 0) {
       +                perror("Socket error");
       +                return -2;
       +        }
       +        if (ltkd_set_nonblock(sockfd)) {
       +                (void)fprintf(stderr, "Unable to set socket to non-blocking mode.\n");
       +                return 1;
       +        } else if (ltkd_set_nonblock(infd)) {
       +                (void)fprintf(stderr, "Unable to set stdin to non-blocking mode.\n");
       +                return 1;
       +        } else if (ltkd_set_nonblock(outfd)) {
       +                (void)fprintf(stderr, "Unable to set stdout to non-blocking mode.\n");
       +                return 1;
       +        }
       +
       +        io_buffers.in_buffer = ltk_malloc(BLK_SIZE);
       +        io_buffers.in_alloc = BLK_SIZE;
       +
       +        io_buffers.out_buffer = ltk_malloc(BLK_SIZE);
       +        io_buffers.out_alloc = BLK_SIZE;
       +
       +        FD_ZERO(&rallfds);
       +        FD_ZERO(&wallfds);
       +
       +        FD_SET(sockfd, &rallfds);
       +        FD_SET(infd, &rallfds);
       +        FD_SET(sockfd, &wallfds);
       +        FD_SET(outfd, &wallfds);
       +        maxfd = sockfd > infd ? sockfd : infd;
       +        if (maxfd < outfd)
       +                maxfd = outfd;
       +
       +        struct timespec now, elapsed, last, sleep_time;
       +        clock_gettime(CLOCK_MONOTONIC, &last);
       +        sleep_time.tv_sec = 0;
       +
       +        while (1) {
       +                if (!FD_ISSET(sockfd, &rallfds) && io_buffers.out_len == 0)
       +                        break;
       +                rfds = rallfds;
       +                wfds = wallfds;
       +                select(maxfd + 1, &rfds, &wfds, NULL, &tv);
       +
       +                /* FIXME: make all this buffer handling a bit more intelligent */
       +                if (FD_ISSET(sockfd, &rfds)) {
       +                        while (1) {
       +                                ltk_grow_string(&io_buffers.out_buffer,
       +                                                &io_buffers.out_alloc,
       +                                                io_buffers.out_len + BLK_SIZE);
       +                                int nread = read(sockfd,
       +                                                 io_buffers.out_buffer + io_buffers.out_len,
       +                                                 BLK_SIZE);
       +                                if (nread < 0) {
       +                                        /* FIXME: distinguish errors */
       +                                        break;
       +                                } else if (nread == 0) {
       +                                        FD_CLR(sockfd, &rallfds);
       +                                        FD_CLR(sockfd, &wallfds);
       +                                        break;
       +                                } else {
       +                                        io_buffers.out_len += nread;
       +                                }
       +                        }
       +                }
       +
       +                if (FD_ISSET(infd, &rfds)) {
       +                        while (1) {
       +                                int nread = read(infd, tmp_buf, BLK_SIZE);
       +                                if (nread < 0) {
       +                                        break;
       +                                } else if (nread == 0) {
       +                                        FD_CLR(infd, &rallfds);
       +                                        break;
       +                                } else {
       +                                        for (int i = 0; i < nread; i++) {
       +                                                if (last_newline) {
       +                                                        int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", seq);
       +                                                        if (numlen < 0 || (unsigned)numlen >= sizeof(num))
       +                                                                ltkc_fatal("There's a bug in the universe.\n");
       +                                                        ltk_grow_string(
       +                                                            &io_buffers.in_buffer,
       +                                                            &io_buffers.in_alloc,
       +                                                            io_buffers.in_len + numlen
       +                                                        );
       +                                                        memcpy(io_buffers.in_buffer + io_buffers.in_len, num, numlen);
       +                                                        io_buffers.in_len += numlen;
       +                                                        last_newline = 0;
       +                                                        seq++;
       +                                                }
       +                                                if (tmp_buf[i] == '\\') {
       +                                                        bs++;
       +                                                        bs %= 2;
       +                                                } else if (tmp_buf[i] == '"' && !bs) {
       +                                                        in_str = !in_str;
       +                                                } else if (tmp_buf[i] == '\n' && !in_str) {
       +                                                        last_newline = 1;
       +                                                } else {
       +                                                        bs = 0;
       +                                                }
       +                                                if (io_buffers.in_len == io_buffers.in_alloc) {
       +                                                        ltk_grow_string(
       +                                                            &io_buffers.in_buffer,
       +                                                            &io_buffers.in_alloc,
       +                                                            io_buffers.in_len + 1
       +                                                        );
       +                                                }
       +                                                io_buffers.in_buffer[io_buffers.in_len++] = tmp_buf[i];
       +                                        }
       +                                }
       +                        }
       +                }
       +
       +                if (FD_ISSET(sockfd, &wfds)) {
       +                        while (io_buffers.in_len > 0) {
       +                                int maxwrite = BLK_SIZE > io_buffers.in_len ?
       +                                               io_buffers.in_len : BLK_SIZE;
       +                                int nwritten = write(sockfd, io_buffers.in_buffer, maxwrite);
       +                                if (nwritten <= 0) {
       +                                        break;
       +                                } else {
       +                                        memmove(io_buffers.in_buffer,
       +                                                io_buffers.in_buffer + nwritten,
       +                                                io_buffers.in_len - nwritten);
       +                                        io_buffers.in_len -= nwritten;
       +                                }
       +                        }
       +                }
       +
       +                if (FD_ISSET(outfd, &wfds)) {
       +                        while (io_buffers.out_len > 0) {
       +                                int maxwrite = BLK_SIZE > io_buffers.out_len ?
       +                                               io_buffers.out_len : BLK_SIZE;
       +                                int nwritten = write(outfd, io_buffers.out_buffer, maxwrite);
       +                                if (nwritten <= 0) {
       +                                        break;
       +                                } else {
       +                                        memmove(io_buffers.out_buffer,
       +                                                io_buffers.out_buffer + nwritten,
       +                                                io_buffers.out_len - nwritten);
       +                                        io_buffers.out_len -= nwritten;
       +                                }
       +                        }
       +                }
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                last = now;
       +        }
       +
       +        ltkc_cleanup();
       +
       +        return 0;
       +}
   DIR diff --git a/src/ltkd/ltkc_img.c b/src/ltkd/ltkc_img.c
       t@@ -0,0 +1,42 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +/* This is just a temporary hack to preprocess an image for sending it to
       +   ltkd. The nicer way for this would be to have a special case for the
       +   "image create" command in ltkc, but I was too lazy to implement that
       +   right now. */
       +
       +#include <stdio.h>
       +
       +int main(int argc, char *argv[]) {
       +        (void)argc;
       +        (void)argv;
       +        int c;
       +        while ((c = getchar()) != EOF) {
       +                switch (c) {
       +                case '\\':
       +                        fputs("\\\\", stdout);
       +                        break;
       +                case '"':
       +                        fputs("\\\"", stdout);
       +                        break;
       +                default:
       +                        putchar(c);
       +                }
       +        }
       +
       +        return 0;
       +}
   DIR diff --git a/src/ltkd/ltkd.c b/src/ltkd/ltkd.c
       t@@ -0,0 +1,1097 @@
       +/* FIXME: Figure out how to properly print window id */
       +/* FIXME: error checking in tokenizer (is this necessary?) */
       +/* FIXME: strip whitespace at end of lines in socket format */
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <time.h>
       +#include <stdio.h>
       +#include <fcntl.h>
       +#include <errno.h>
       +#include <stdlib.h>
       +#include <string.h>
       +#include <stdarg.h>
       +#include <unistd.h>
       +#include <signal.h>
       +#include <locale.h>
       +#include <inttypes.h>
       +
       +#include <sys/un.h>
       +#include <sys/select.h>
       +#include <sys/socket.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "util.h"
       +#include "widget.h"
       +#include "cmd_helpers.h"
       +#include "proto_types.h"
       +
       +#include <ltk/memory.h>
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/text.h>
       +#include <ltk/macros.h>
       +#include <ltk/graphics.h>
       +
       +#define MAX_SOCK_CONNS 20
       +#define READ_BLK_SIZE 128
       +#define WRITE_BLK_SIZE 128
       +
       +struct token_list {
       +        ltkd_cmd_token *tokens;
       +        /* FIXME: size_t everywhere */
       +        int num_tokens;
       +        int num_alloc;
       +};
       +
       +/* FIXME: switch to size_t */
       +static struct ltkd_sock_info {
       +        int fd;                    /* file descriptor for socket connection */
       +        int event_mask;            /* events to send to socket */
       +        char *read;                /* text read from socket */
       +        int read_len;              /* length of text in read buffer */
       +        int read_alloc;            /* size of read buffer */
       +        char *to_write;            /* text to be written to socket */
       +        int write_len;             /* length of text in write buffer */
       +        int write_cur;             /* length of text already written */
       +        int write_alloc;           /* size of write buffer */
       +        /* stuff for tokenizing */
       +        int in_token;              /* last read char is inside token */
       +        int offset;                /* offset from removing backslashes */
       +        int in_str;                /* last read char is inside string */
       +        int read_cur;              /* length of text already tokenized */
       +        int bs;                    /* last char was non-escaped backslash */
       +        struct token_list tokens;  /* current tokens */
       +        uint32_t last_seq;         /* sequence number of last request processed */
       +} sockets[MAX_SOCK_CONNS];
       +
       +static int daemonize_flag = 1;
       +
       +static void ltkd_mainloop(ltk_window *window);
       +static char *get_sock_path(char *basedir, unsigned long id);
       +static FILE *open_log(char *dir);
       +static void daemonize(void);
       +static int read_sock(struct ltkd_sock_info *sock);
       +static int push_token(struct token_list *tl, char *token);
       +static int read_sock(struct ltkd_sock_info *sock);
       +static int write_sock(struct ltkd_sock_info *sock);
       +static int tokenize_command(struct ltkd_sock_info *sock);
       +static int ltkd_set_root_widget_cmd(ltk_window *window, ltkd_cmd_token *tokens, int num_tokens, ltkd_error *err);
       +static int process_commands(ltk_window *window, int client);
       +static int add_client(int fd);
       +static int listen_sock(const char *sock_path);
       +static int accept_sock(int listenfd);
       +static void ltkd_quit(void);
       +static void ltkd_cleanup(void);
       +
       +static short maxsocket = -1;
       +static short running = 1;
       +static short sock_write_available = 0;
       +static int base_dir_fd = -1;
       +static char *ltkd_dir = NULL;
       +static FILE *ltkd_logfile = NULL;
       +static char *sock_path = NULL;
       +/* Note: Most functions still take this explicitly because it wasn't
       +   global originally, but that's just the way it is. */
       +static ltk_window *main_window = NULL;
       +
       +typedef struct {
       +        char *name;
       +        int (*cmd)(ltk_window *, ltkd_cmd_token *, size_t, ltkd_error *);
       +} ltkd_widget_funcs;
       +
       +/* FIXME: use binary search when searching for the widget */
       +ltkd_widget_funcs widget_funcs[] = {
       +        {
       +                .name = "box",
       +                .cmd = &ltkd_box_cmd
       +        },
       +        {
       +                .name = "button",
       +                .cmd = &ltkd_button_cmd
       +        },
       +        {
       +                .name = "entry",
       +                .cmd = &ltkd_entry_cmd
       +        },
       +        {
       +                .name = "grid",
       +                .cmd = &ltkd_grid_cmd
       +        },
       +        {
       +                .name = "label",
       +                .cmd = &ltkd_label_cmd
       +        },
       +        {
       +                .name = "image",
       +                .cmd = &ltkd_image_widget_cmd
       +        },
       +        {
       +                .name = "menu",
       +                .cmd = &ltkd_menu_cmd
       +        },
       +        {
       +                .name = "menuentry",
       +                .cmd = &ltkd_menuentry_cmd
       +        },
       +        {
       +                .name = "submenu",
       +                .cmd = &ltkd_menu_cmd
       +        },
       +};
       +
       +static int
       +ltkd_window_close(ltk_widget *self, ltk_callback_arglist args, ltk_callback_arg data) {
       +        (void)self;
       +        (void)args;
       +        (void)data;
       +        ltkd_quit();
       +        return 1;
       +}
       +
       +int
       +main(int argc, char *argv[]) {
       +        setlocale(LC_CTYPE, "");
       +        /* FIXME: decide where to add this (currently called in ltk, but kind of weird) */
       +        /*XSetLocaleModifiers("");*/
       +        int ch;
       +        char *title = "LTK Window";
       +        while ((ch = getopt(argc, argv, "dt:")) != -1) {
       +                switch (ch) {
       +                        case 't':
       +                                title = optarg;
       +                                break;
       +                        case 'd':
       +                                daemonize_flag = 0;
       +                                break;
       +                        default:
       +                                ltkd_fatal("USAGE: ltkd [-t title]\n");
       +                }
       +        }
       +
       +        ltkd_dir = ltk_setup_directory("LTKDDIR", ".ltkd", 1);
       +        if (!ltkd_dir) ltkd_fatal_errno("Unable to setup ltkd directory.\n");
       +        /* FIXME: this is only used to use unlinkat for deleting the socket in
       +           the end in case ltkd_dir is relative. It would probably be better
       +           to just use getcwd to turn that into an absolute path instead.
       +           Or maybe ltkd should just stay in the current directory instead of
       +           moving to / in daemonize(). */
       +        base_dir_fd = open(".", O_RDONLY);
       +        if (base_dir_fd < 0)
       +                ltkd_fatal_errno("Unable to open current directory.\n");
       +        ltkd_logfile = open_log(ltkd_dir);
       +        if (!ltkd_logfile) ltkd_fatal_errno("Unable to open log file.\n");
       +
       +        ltk_init();
       +        ltkd_widgets_init();
       +
       +        /* FIXME: set window size properly - I only run it in a tiling WM
       +           anyways, so it doesn't matter, but still... */
       +        main_window = ltk_window_create(title, 0, 0, 500, 500);
       +        ltk_widget_register_signal_handler(LTK_CAST_WIDGET(main_window), LTK_WINDOW_SIGNAL_CLOSE, &ltkd_window_close, LTK_ARG_VOID);
       +
       +        /* This hack is necessary to make the daemonization work properly when using Pango.
       +           This may not be entirely accurate, but from what I gather, newer versions of Pango
       +           initialize Fontconfig in a separate thread to avoid startup overhead. This leads
       +           to non-deterministic behavior because the Fontconfig initialization doesn't work
       +           properly after daemonization. Creating a text line and getting the size waits until
       +           Fontconfig is initialized. Getting the size is important because Pango doesn't
       +           actually do much until you try to use the line for something. */
       +        /* FIXME: I guess just calling FcInit manually in the text backend could work as well. */
       +        /* FIXME: Maybe just call this when actually daemonizing. */
       +        ltk_text_line *tmp = ltk_text_line_create_default(10, "hi", 0, -1);
       +        int tw, th;
       +        ltk_text_line_get_size(tmp, &tw, &th);
       +        ltk_text_line_destroy(tmp);
       +
       +        sock_path = get_sock_path(ltkd_dir, ltk_renderer_get_window_id(main_window->renderwindow));
       +        if (!sock_path) ltkd_fatal_errno("Unable to allocate memory for socket path.\n");
       +
       +        /* Note: sockets should be initialized to 0 because it is static */
       +        for (int i = 0; i < MAX_SOCK_CONNS; i++) {
       +                sockets[i].fd = -1; /* socket unused */
       +                /* initialize these just because I'm paranoid */
       +                sockets[i].read = NULL;
       +                sockets[i].to_write = NULL;
       +                sockets[i].tokens.tokens = NULL;
       +        }
       +
       +        ltkd_mainloop(main_window);
       +        return 0;
       +}
       +
       +/* FIXME: need to recalculate maxfd when removing client */
       +static struct {
       +        fd_set rallfds, wallfds;
       +        int maxfd;
       +        int listenfd;
       +} sock_state;
       +
       +/* FIXME: this is extremely dangerous right now because pretty much any command
       +   can be executed, so for instance the widget that caused the lock could also
       +   be destroyed, causing issues when this function returns */
       +int
       +ltkd_handle_lock_client(ltk_window *window, int client) {
       +        if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
       +                return 0;
       +        fd_set rfds, wfds, rallfds, wallfds;
       +        int clifd = sockets[client].fd;
       +        FD_ZERO(&rallfds);
       +        FD_ZERO(&wallfds);
       +        FD_SET(clifd, &rallfds);
       +        FD_SET(clifd, &wallfds);
       +        int retval;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 0;
       +        struct timespec now, elapsed, last, sleep_time;
       +        clock_gettime(CLOCK_MONOTONIC, &last);
       +        sleep_time.tv_sec = 0;
       +        while (1) {
       +                rfds = rallfds;
       +                wfds = wallfds;
       +                retval = select(clifd + 1, &rfds, &wfds, NULL, &tv);
       +
       +                if (retval > 0) {
       +                        if (FD_ISSET(clifd, &rfds)) {
       +                                int ret;
       +                                while ((ret = read_sock(&sockets[client])) == 1) {
       +                                        int pret;
       +                                        if ((pret = process_commands(window, client)) == 1)
       +                                                return 1;
       +                                        else if (pret == -1)
       +                                                return 0;
       +                                }
       +                                /* FIXME: maybe also return on read error? or would that be dangerous? */
       +                                if (ret == 0) {
       +                                        FD_CLR(clifd, &sock_state.rallfds);
       +                                        FD_CLR(clifd, &sock_state.wallfds);
       +                                        ltkd_widget_remove_client(client);
       +                                        sockets[clifd].fd = -1;
       +                                        close(clifd);
       +                                        int newmaxsocket = -1;
       +                                        for (int j = 0; j <= maxsocket; j++) {
       +                                                if (sockets[j].fd >= 0)
       +                                                        newmaxsocket = j;
       +                                        }
       +                                        maxsocket = newmaxsocket;
       +                                        if (maxsocket == -1) {
       +                                                ltkd_quit();
       +                                                break;
       +                                        }
       +                                        return 0;
       +                                }
       +                        }
       +                        if (FD_ISSET(clifd, &wfds)) {
       +                                /* FIXME: call in loop like above */
       +                                write_sock(&sockets[client]);
       +                        }
       +                }
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &elapsed);
       +                /* FIXME: configure framerate */
       +                if (elapsed.tv_sec == 0 && elapsed.tv_nsec < 20000000LL) {
       +                        sleep_time.tv_nsec = 20000000LL - elapsed.tv_nsec;
       +                        nanosleep(&sleep_time, NULL);
       +                }
       +                last = now;
       +        }
       +        return 0;
       +}
       +
       +/* FIXME: need to remove event masks from all widgets when removing client */
       +static void
       +ltkd_mainloop(ltk_window *window) {
       +        fd_set rfds, wfds;
       +        int retval;
       +        int clifd;
       +        struct timeval tv;
       +        tv.tv_sec = 0;
       +        tv.tv_usec = 0;
       +
       +        FD_ZERO(&sock_state.rallfds);
       +        FD_ZERO(&sock_state.wallfds);
       +
       +        if ((sock_state.listenfd = listen_sock(sock_path)) < 0)
       +                ltkd_fatal_errno("Error listening on socket.\n");
       +
       +        FD_SET(sock_state.listenfd, &sock_state.rallfds);
       +        sock_state.maxfd = sock_state.listenfd;
       +
       +        printf("%lu", ltk_renderer_get_window_id(main_window->renderwindow));
       +        fflush(stdout);
       +        if (daemonize_flag)
       +                daemonize();
       +
       +        ltk_mainloop_init();
       +
       +        while (running) {
       +                rfds = sock_state.rallfds;
       +                wfds = sock_state.wallfds;
       +                retval = select(sock_state.maxfd + 1, &rfds, &wfds, NULL, &tv);
       +                if (retval > 0) {
       +                        if (FD_ISSET(sock_state.listenfd, &rfds)) {
       +                                if ((clifd = accept_sock(sock_state.listenfd)) < 0) {
       +                                        /* FIXME: Just log this! */
       +                                        ltkd_fatal_errno("Error accepting socket connection.\n");
       +                                }
       +                                int i = add_client(clifd);
       +                                FD_SET(clifd, &sock_state.rallfds);
       +                                FD_SET(clifd, &sock_state.wallfds);
       +                                if (clifd > sock_state.maxfd)
       +                                        sock_state.maxfd = clifd;
       +                                if (i > maxsocket)
       +                                        maxsocket = i;
       +                                continue;
       +                        }
       +                        for (int i = 0; i <= maxsocket; i++) {
       +                                if ((clifd = sockets[i].fd) < 0)
       +                                        continue;
       +                                if (FD_ISSET(clifd, &rfds)) {
       +                                        /* FIXME: better error handling - this assumes error
       +                                           is always because read would block */
       +                                        /* FIXME: maybe maximum number of iterations here to
       +                                           avoid choking on a lot of data? although such a
       +                                           large amount of data would probably cause other
       +                                           problems anyways */
       +                                        /* or maybe measure time and break after max time? */
       +                                        int ret;
       +                                        while ((ret = read_sock(&sockets[i])) == 1) {
       +                                                process_commands(window, i);
       +                                        }
       +                                        if (ret == 0) {
       +                                                ltkd_widget_remove_client(i);
       +                                                FD_CLR(clifd, &sock_state.rallfds);
       +                                                FD_CLR(clifd, &sock_state.wallfds);
       +                                                sockets[i].fd = -1;
       +                                                /* FIXME: what to do on error? */
       +                                                close(clifd);
       +                                                int newmaxsocket = -1;
       +                                                for (int j = 0; j <= maxsocket; j++) {
       +                                                        if (sockets[j].fd >= 0)
       +                                                                newmaxsocket = j;
       +                                                }
       +                                                maxsocket = newmaxsocket;
       +                                                if (maxsocket == -1) {
       +                                                        ltkd_quit();
       +                                                        break;
       +                                                }
       +                                        }
       +                                }
       +                                /* FIXME: maybe ignore SIGPIPE signal - then don't call FD_CLR
       +                                   for wallfds above but rather when write fails with EPIPE */
       +                                /* -> this would possibly allow data to be written still in the
       +                                   hypothetical scenario that only the writing end of the socket
       +                                   is closed (and ltkd wouldn't crash if only the reading end is
       +                                   closed) */
       +                                if (FD_ISSET(clifd, &wfds)) {
       +                                        /* FIXME: also call in loop like reading above */
       +                                        write_sock(&sockets[i]);
       +                                }
       +                        }
       +                }
       +
       +                ltk_mainloop_step(1);
       +        }
       +
       +        ltkd_cleanup();
       +}
       +
       +/* largely copied from APUE */
       +static void
       +daemonize(void) {
       +        pid_t pid;
       +        struct sigaction sa;
       +
       +        fflush(stdout);
       +        fflush(stderr);
       +        fflush(ltkd_logfile);
       +
       +        if ((pid = fork()) < 0)
       +                ltkd_fatal_errno("Can't fork.\n");
       +        else if (pid != 0)
       +                exit(0);
       +        setsid();
       +
       +        sa.sa_handler = SIG_IGN;
       +        sigemptyset(&sa.sa_mask);
       +        sa.sa_flags = 0;
       +        if (sigaction(SIGHUP, &sa, NULL) < 0)
       +                ltkd_fatal_errno("Unable to ignore SIGHUP.\n");
       +        if ((pid  = fork()) < 0)
       +                ltkd_fatal_errno("Can't fork.\n");
       +        else if (pid != 0)
       +                exit(0);
       +
       +        if (chdir("/") < 0)
       +                ltkd_fatal_errno("Can't change directory to root.\n");
       +
       +        /* FIXME: error handling */
       +        int devnull = open("/dev/null", O_RDONLY);
       +        if (devnull >= 0)
       +                dup2(devnull, fileno(stdin));
       +        dup2(fileno(ltkd_logfile), fileno(stdout));
       +        dup2(fileno(ltkd_logfile), fileno(stderr));
       +}
       +
       +static char *
       +get_sock_path(char *basedir, unsigned long id) {
       +        int len;
       +        char *path;
       +
       +        len = strlen(basedir);
       +        /* FIXME: MAKE SURE THIS IS ACTUALLY BIG ENOUGH! */
       +        path = ltk_malloc(len + 20);
       +        /* FIXME: also check for less than 0 */
       +        if (snprintf(path, len + 20, "%s/%lu.sock", basedir, id) >= len + 20)
       +                ltkd_fatal("Tell lumidify to fix his code.\n");
       +
       +        return path;
       +}
       +
       +static FILE *
       +open_log(char *dir) {
       +        FILE *f;
       +        char *path;
       +
       +        path = ltk_strcat_useful(dir, "/ltkd.log");
       +        if (!path)
       +                return NULL;
       +        f = fopen(path, "a");
       +        if (!f) {
       +                ltk_free(path);
       +                return NULL;
       +        }
       +        ltk_free(path);
       +
       +        return f;
       +}
       +
       +static void
       +ltkd_cleanup(void) {
       +        if (sock_path) {
       +                /* FIXME: somewhat misleading warning message */
       +                if (base_dir_fd >= 0)
       +                        unlinkat(base_dir_fd, sock_path, 0);
       +                else
       +                        ltk_warn("Unable to remove socket file!\n");
       +                ltk_free(sock_path);
       +        }
       +        if (base_dir_fd >= 0)
       +                close(base_dir_fd);
       +        if (sock_state.listenfd >= 0)
       +                close(sock_state.listenfd);
       +        if (ltkd_dir)
       +                ltk_free(ltkd_dir);
       +        if (ltkd_logfile)
       +                fclose(ltkd_logfile);
       +
       +        for (int i = 0; i < MAX_SOCK_CONNS; i++) {
       +                if (sockets[i].fd >= 0)
       +                        close(sockets[i].fd);
       +                if (sockets[i].read)
       +                        ltk_free(sockets[i].read);
       +                if (sockets[i].to_write)
       +                        ltk_free(sockets[i].to_write);
       +                if (sockets[i].tokens.tokens)
       +                        ltk_free(sockets[i].tokens.tokens);
       +        }
       +
       +        ltkd_widgets_cleanup();
       +        main_window = NULL;
       +        ltk_deinit();
       +}
       +
       +static void
       +ltkd_quit(void) {
       +        running = 0;
       +}
       +
       +static void
       +ltkd_log_msg(const char *mode, const char *format, va_list args) {
       +        char logtime[25]; /* FIXME: This should always be big enough, right? */
       +        time_t clock;
       +        struct tm *timeptr;
       +
       +        time(&clock);
       +        timeptr = localtime(&clock);
       +        strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
       +
       +        if (main_window)
       +                fprintf(stderr, "%s ltkd(%lu) %s: ", logtime, ltk_renderer_get_window_id(main_window->renderwindow), mode);
       +        else
       +                fprintf(stderr, "%s ltkd(?) %s: ", logtime, mode);
       +        vfprintf(stderr, format, args);
       +}
       +
       +static int
       +ltkd_set_root_widget_cmd(
       +    ltk_window *window,
       +    ltkd_cmd_token *tokens,
       +    int num_tokens,
       +    ltkd_error *err) {
       +        ltkd_widget *widget;
       +        if (num_tokens != 2) {
       +                err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
       +                err->arg = -1;
       +                return 1;
       +        } else if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
       +        }
       +        widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
       +        if (!widget) {
       +                err->arg = 1;
       +                return 1;
       +        }
       +        ltk_window_set_root_widget(window, widget->widget);
       +
       +        return 0;
       +}
       +
       +/* Push a token onto `token_list`, resizing the buffer if necessary.
       +   Returns -1 on error, 0 otherwise.
       +   Note: The token is not copied, it is only added directly. */
       +static int
       +push_token(struct token_list *tl, char *token) {
       +        int new_size;
       +        if (tl->num_tokens >= tl->num_alloc) {
       +                new_size = (tl->num_alloc * 2) > (tl->num_tokens + 1) ?
       +                           (tl->num_alloc * 2) : (tl->num_tokens + 1);
       +                ltkd_cmd_token *new = ltk_reallocarray(tl->tokens, new_size, sizeof(ltkd_cmd_token));
       +                if (!new) return -1;
       +                tl->tokens = new;
       +                tl->num_alloc = new_size;
       +        }
       +        tl->tokens[tl->num_tokens].text = token;
       +        tl->tokens[tl->num_tokens].len = 0;
       +        tl->tokens[tl->num_tokens++].contains_nul = 0;
       +
       +        return 0;
       +}
       +
       +/* Add a new client to the socket list and return the index in `sockets`.
       +   Returns -1 if there is no space for a new client. */
       +static int
       +add_client(int fd) {
       +        for (int i = 0; i < MAX_SOCK_CONNS; i++) {
       +                if (sockets[i].fd == -1) {
       +                        sockets[i].fd = fd;
       +                        sockets[i].event_mask = ~0; /* FIXME */
       +                        sockets[i].read_len = 0;
       +                        sockets[i].write_len = 0;
       +                        sockets[i].write_cur = 0;
       +                        sockets[i].offset = 0;
       +                        sockets[i].in_str = 0;
       +                        sockets[i].read_cur = 0;
       +                        sockets[i].bs = 0;
       +                        sockets[i].tokens.num_tokens = 0;
       +                        sockets[i].last_seq = 0;
       +                        return i;
       +                }
       +        }
       +
       +        return -1;
       +}
       +
       +/* largely copied from APUE */
       +/* Listen on the socket at `sock_path`.
       +   Returns the file descriptor of the opened socket on success.
       +   Returns -1 if `sock_path` is too long
       +           -2 if the socket could not be created
       +           -3 if the socket could not be bound to the path
       +           -4 if the socket could not be listened on */
       +static int
       +listen_sock(const char *sock_path) {
       +        int fd, len, err, rval;
       +        struct sockaddr_un un;
       +
       +        if (strlen(sock_path) >= sizeof(un.sun_path)) {
       +                errno = ENAMETOOLONG;
       +                return -1;
       +        }
       +
       +        if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)
       +                return -2;
       +
       +        unlink(sock_path);
       +
       +        memset(&un, 0, sizeof(un));
       +        un.sun_family = AF_UNIX;
       +        strcpy(un.sun_path, sock_path);
       +        len = offsetof(struct sockaddr_un, sun_path) + strlen(sock_path);
       +        if (bind(fd, (struct sockaddr *)&un, len) < 0) {
       +                rval = -3;
       +                goto errout;
       +        }
       +
       +        if (listen(fd, 10) < 0) {
       +                rval = -4;
       +                goto errout;
       +        }
       +
       +        return fd;
       +
       +errout:
       +        err = errno;
       +        close(fd);
       +        errno = err;
       +        return rval;
       +}
       +
       +/* Accept a socket connection on the listening socket `listenfd`.
       +   Returns the file descriptor of the accepted client on success.
       +   Returns -1 if there was an error. */
       +static int
       +accept_sock(int listenfd) {
       +        int clifd;
       +        socklen_t len;
       +        struct sockaddr_un un;
       +
       +        len = sizeof(un);
       +        if ((clifd = accept(listenfd, (struct sockaddr *)&un, &len)) < 0) {
       +                return -1;
       +        }
       +        if (ltkd_set_nonblock(clifd)) {
       +                /* FIXME: what could even be done if close fails? */
       +                close(clifd);
       +                return -1;
       +        }
       +
       +        return clifd;
       +}
       +
       +/* Read up to READ_BLK_SIZE bytes from the socket `sock`.
       +   Returns -1 if an error occurred, 0 if the connection was closed, 1 otherwise.
       +   Note: Returning 1 on success is weird, but it could also be confusing to
       +   return 0 on success when `read` returns that to mean that the connection
       +   was closed. */
       +static int
       +read_sock(struct ltkd_sock_info *sock) {
       +        int nread;
       +        char *old = sock->read;
       +        ltk_grow_string(&sock->read, &sock->read_alloc, sock->read_len + READ_BLK_SIZE);
       +        /* move tokens to new addresses - this was added as an
       +           afterthought and really needs to be cleaned up */
       +        if (sock->read != old) {
       +                for (int i = 0; i < sock->tokens.num_tokens; i++) {
       +                        sock->tokens.tokens[i].text = sock->read + (sock->tokens.tokens[i].text - old);
       +                }
       +        }
       +        nread = read(sock->fd, sock->read + sock->read_len, READ_BLK_SIZE);
       +        if (nread == -1 || nread == 0)
       +                return nread;
       +        sock->read_len += nread;
       +
       +        return 1;
       +}
       +
       +/* Write up to WRITE_BLK_SIZE bytes to the socket.
       +   Returns -1 on error, 0 otherwise. */
       +static int
       +write_sock(struct ltkd_sock_info *sock) {
       +        if (sock->write_len == sock->write_cur)
       +                return 0;
       +        int write_len = WRITE_BLK_SIZE > sock->write_len - sock->write_cur ?
       +                        sock->write_len - sock->write_cur : WRITE_BLK_SIZE;
       +        int nwritten = write(sock->fd, sock->to_write + sock->write_cur, write_len);
       +        if (nwritten == -1)
       +                return nwritten;
       +        sock->write_cur += nwritten;
       +
       +        /* check if any sockets have text to write */
       +        if (sock->write_cur == sock->write_len) {
       +                int found = 0;
       +                for (int i = 0; i < maxsocket; i++) {
       +                        if (sockets[i].fd != -1 &&
       +                            sockets[i].write_cur != sockets[i].write_len) {
       +                                found = 1;
       +                                break;
       +                        }
       +                }
       +                if (!found)
       +                        sock_write_available = 0;
       +        }
       +
       +        return 0;
       +}
       +
       +static void
       +move_write_pos(struct ltkd_sock_info *sock) {
       +        /* FIXME: also resize if too large */
       +        if (sock->write_cur > 0) {
       +                memmove(sock->to_write, sock->to_write + sock->write_cur,
       +                        sock->write_len - sock->write_cur);
       +                sock->write_len -= sock->write_cur;
       +                sock->write_cur = 0;
       +        }
       +}
       +
       +/* Queue `str` to be written to the socket. If len is < 0, it is set to `strlen(str)`.
       +   Returns -1 on error, 0 otherwise.
       +   Note: The string must include all '\n', etc. as defined in the protocol. This
       +   function just adds the given string verbatim. */
       +int
       +ltkd_queue_sock_write(int client, const char *str, int len) {
       +        if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
       +                return 1;
       +        /* this is always large enough to hold a uint32_t and " \0" */
       +        char num[12];
       +        struct ltkd_sock_info *sock = &sockets[client];
       +        move_write_pos(sock);
       +        if (len < 0)
       +                len = strlen(str);
       +
       +        int numlen = snprintf(num, sizeof(num), "%"PRIu32" ", sock->last_seq);
       +        if (numlen < 0 || (unsigned)numlen >= sizeof(num))
       +                ltkd_fatal("There's a bug in the universe.\n");
       +        if (sock->write_alloc - sock->write_len < len + numlen)
       +                ltk_grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len + numlen);
       +
       +        (void)strncpy(sock->to_write + sock->write_len, num, numlen);
       +        (void)strncpy(sock->to_write + sock->write_len + numlen, str, len);
       +        sock->write_len += len + numlen;
       +
       +        sock_write_available = 1;
       +
       +        return 0;
       +}
       +
       +int
       +ltkd_queue_sock_write_fmt(int client, const char *fmt, ...) {
       +        if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
       +                return 1;
       +        struct ltkd_sock_info *sock = &sockets[client];
       +        /* just to print the sequence number */
       +        ltkd_queue_sock_write(client, "", 0);
       +        va_list args;
       +        va_start(args, fmt);
       +        int len = vsnprintf(sock->to_write + sock->write_len, sock->write_alloc - sock->write_len, fmt, args);
       +        if (len < 0) {
       +                ltkd_fatal("Unable to print formatted text to socket.\n");
       +        } else if (len >= sock->write_alloc - sock->write_len) {
       +                va_end(args);
       +                va_start(args, fmt);
       +                /* snprintf always writes '\0', even though we don't actually need it here */
       +                ltk_grow_string(&sock->to_write, &sock->write_alloc, sock->write_len + len + 1);
       +                vsnprintf(sock->to_write + sock->write_len, sock->write_alloc - sock->write_len, fmt, args);
       +        }
       +        va_end(args);
       +        sock->write_len += len;
       +        sock_write_available = 1;
       +
       +        return 0;
       +}
       +
       +/* Tokenize the current read buffer in `sock`.
       +   Returns 0 immediately if the end of a command was encountered, 1 otherwise. */
       +static int
       +tokenize_command(struct ltkd_sock_info *sock) {
       +        for (; sock->read_cur < sock->read_len; sock->read_cur++) {
       +                /* FIXME: strip extra whitespace? Or maybe just have a "hard" protocol where it always has to be one space. */
       +                if (!sock->in_token) {
       +                        push_token(&sock->tokens, sock->read + sock->read_cur - sock->offset);
       +                        sock->in_token = 1;
       +                }
       +                if (sock->read[sock->read_cur] == '\\') {
       +                        sock->bs++;
       +                        if (sock->bs / 2)
       +                                sock->offset++;
       +                        else
       +                                sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
       +                        sock->bs %= 2;
       +                        sock->read[sock->read_cur-sock->offset] = '\\';
       +                } else if (sock->read[sock->read_cur] == '\n' && !sock->in_str) {
       +                        sock->read[sock->read_cur-sock->offset] = '\0';
       +                        sock->read_cur++;
       +                        sock->offset = 0;
       +                        sock->in_token = 0;
       +                        sock->bs = 0;
       +                        return 0;
       +                } else if (sock->read[sock->read_cur] == '"') {
       +                        sock->offset++;
       +                        if (sock->bs) {
       +                                sock->read[sock->read_cur-sock->offset] = '"';
       +                                sock->bs = 0;
       +                        } else {
       +                                sock->in_str = !sock->in_str;
       +                        }
       +                } else if (sock->read[sock->read_cur] == ' ' && !sock->in_str) {
       +                        sock->read[sock->read_cur-sock->offset] = '\0';
       +                        sock->in_token = !sock->in_token;
       +                        sock->bs = 0;
       +                } else {
       +                        sock->read[sock->read_cur-sock->offset] = sock->read[sock->read_cur];
       +                        /* FIXME: assert that num_tokens > 0 */
       +                        sock->tokens.tokens[sock->tokens.num_tokens - 1].len++;
       +                        if (sock->read[sock->read_cur] == '\0')
       +                                sock->tokens.tokens[sock->tokens.num_tokens - 1].contains_nul = 1;
       +                        sock->bs = 0;
       +                }
       +        }
       +
       +        return 1;
       +}
       +
       +/* FIXME: currently no type-checking when setting specific widget mask */
       +/* FIXME: this is really ugly and inefficient right now - it will be replaced with something
       +   more generic at some point (or maybe just with a binary protocol?) */
       +static int
       +handle_mask_command(int client, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err) {
       +        if (num_tokens != 4 && num_tokens != 5) {
       +                err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
       +                err->arg = -1;
       +                return 1;
       +        }
       +        /* FIXME: make this nicer */
       +        /* -> use generic cmd handling like the widgets */
       +        if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
       +        } else if (tokens[2].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 2;
       +                return 1;
       +        } else if (tokens[3].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 3;
       +                return 1;
       +        } else if (num_tokens == 5 && tokens[4].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 4;
       +                return 1;
       +        }
       +        uint32_t mask = 0;
       +        int lock = 0;
       +        int special = 0;
       +        ltkd_widget *widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
       +        if (!widget) {
       +                err->arg = 1;
       +                return 1;
       +        }
       +        if (!strcmp(tokens[2].text, "widget")) {
       +                if (!strcmp(tokens[3].text, "mousepress")) {
       +                        mask = LTKD_PEVENTMASK_MOUSEPRESS;
       +                } else if (!strcmp(tokens[3].text, "mouserelease")) {
       +                        mask = LTKD_PEVENTMASK_MOUSERELEASE;
       +                } else if (!strcmp(tokens[3].text, "mousemotion")) {
       +                        mask = LTKD_PEVENTMASK_MOUSEMOTION;
       +                } else if (!strcmp(tokens[3].text, "resize")) {
       +                        mask = LTKD_PEVENTMASK_RESIZE;
       +                } else if (!strcmp(tokens[3].text, "statechange")) {
       +                        mask = LTKD_PEVENTMASK_STATECHANGE;
       +                } else if (!strcmp(tokens[3].text, "none")) {
       +                        mask = LTKD_PEVENTMASK_NONE;
       +                } else {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = 3;
       +                        return 1;
       +                }
       +        } else if (!strcmp(tokens[2].text, "menuentry")) {
       +                if (!strcmp(tokens[3].text, "press")) {
       +                        mask = LTKD_PWEVENTMASK_MENUENTRY_PRESS;
       +                } else if (!strcmp(tokens[3].text, "none")) {
       +                        mask = LTKD_PWEVENTMASK_MENUENTRY_NONE;
       +                } else {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = 3;
       +                        return 1;
       +                }
       +                special = 1;
       +        } else if (!strcmp(tokens[2].text, "button")) {
       +                if (!strcmp(tokens[3].text, "press")) {
       +                        mask = LTKD_PWEVENTMASK_BUTTON_PRESS;
       +                } else if (!strcmp(tokens[3].text, "none")) {
       +                        mask = LTKD_PWEVENTMASK_BUTTON_NONE;
       +                } else {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = 3;
       +                        return 1;
       +                }
       +                special = 1;
       +        } else {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 2;
       +                return 1;
       +        }
       +        if (num_tokens == 5) {
       +                if (!strcmp(tokens[4].text, "lock")) {
       +                        lock = 1;
       +                } else {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = 4;
       +                        return 1;
       +                }
       +        }
       +
       +        if (!strcmp(tokens[0].text, "mask-add")) {
       +                if (lock) {
       +                        if (special)
       +                                ltkd_widget_add_to_event_lwmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_add_to_event_lmask(widget, client, mask);
       +                } else {
       +                        if (special)
       +                                ltkd_widget_add_to_event_wmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_add_to_event_mask(widget, client, mask);
       +                }
       +        } else if (!strcmp(tokens[0].text, "mask-set")) {
       +                if (lock) {
       +                        if (special)
       +                                ltkd_widget_set_event_lwmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_set_event_lmask(widget, client, mask);
       +                } else {
       +                        if (special)
       +                                ltkd_widget_set_event_wmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_set_event_mask(widget, client, mask);
       +                }
       +        } else if (!strcmp(tokens[0].text, "mask-remove")) {
       +                if (lock) {
       +                        if (special)
       +                                ltkd_widget_remove_from_event_lwmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_remove_from_event_lmask(widget, client, mask);
       +                } else {
       +                        if (special)
       +                                ltkd_widget_remove_from_event_wmask(widget, client, mask);
       +                        else
       +                                ltkd_widget_remove_from_event_mask(widget, client, mask);
       +                }
       +        } else {
       +                err->type = ERR_INVALID_COMMAND;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* Process the commands as they are read from the socket. */
       +/* Returns 1 if command was 'event-unlock true',
       +   -1 if command was 'event-unlock false', 0 otherwise. */
       +static int
       +process_commands(ltk_window *window, int client) {
       +        if (client < 0 || client >= MAX_SOCK_CONNS || sockets[client].fd == -1)
       +                return 0;
       +        struct ltkd_sock_info *sock = &sockets[client];
       +        ltkd_cmd_token *tokens;
       +        int num_tokens;
       +        ltkd_error errdetail = {ERR_NONE, -1};
       +        int err;
       +        int retval = 0;
       +        int last = 0;
       +        uint32_t seq;
       +        const char *errstr;
       +        int contains_nul = 0;
       +        while (!tokenize_command(sock)) {
       +                contains_nul = 0;
       +                err = 0;
       +                tokens = sock->tokens.tokens;
       +                num_tokens = sock->tokens.num_tokens;
       +                if (num_tokens < 2) {
       +                        errdetail.type = ERR_INVALID_COMMAND;
       +                        errdetail.arg = -1;
       +                        err = 1;
       +                } else {
       +                        contains_nul = tokens[0].contains_nul;
       +                        seq = (uint32_t)ltk_strtonum(tokens[0].text, 0, UINT32_MAX, &errstr);
       +                        tokens++;
       +                        num_tokens--;
       +                        if (errstr || contains_nul) {
       +                                errdetail.type = ERR_INVALID_SEQNUM;
       +                                errdetail.arg = -1;
       +                                err = 1;
       +                                seq = sock->last_seq;
       +                        } else if (tokens[0].contains_nul) {
       +                                errdetail.type = ERR_INVALID_ARGUMENT;
       +                                errdetail.arg = 0;
       +                                err = 1;
       +                                seq = sock->last_seq;
       +                        } else if (strcmp(tokens[0].text, "set-root-widget") == 0) {
       +                                err = ltkd_set_root_widget_cmd(window, tokens, num_tokens, &errdetail);
       +                        } else if (strcmp(tokens[0].text, "quit") == 0) {
       +                                ltkd_quit();
       +                                last = 1;
       +                        } else if (strcmp(tokens[0].text, "destroy") == 0) {
       +                                err = ltkd_widget_destroy_cmd(window, tokens, num_tokens, &errdetail);
       +                        } else if (strncmp(tokens[0].text, "mask", 4) == 0) {
       +                                err = handle_mask_command(client, tokens, num_tokens, &errdetail);
       +                        } else if (strcmp(tokens[0].text, "event-unlock") == 0) {
       +                                if (num_tokens != 2) {
       +                                        errdetail.type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
       +                                        errdetail.arg = -1;
       +                                        err = 1;
       +                                } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "true") == 0) {
       +                                        retval = 1;
       +                                } else if (!tokens[1].contains_nul && strcmp(tokens[1].text, "false") == 0) {
       +                                        retval = -1;
       +                                } else {
       +                                        err = 1;
       +                                        errdetail.type = ERR_INVALID_ARGUMENT;
       +                                        errdetail.arg = 1;
       +                                }
       +                                last = 1;
       +                        } else {
       +                                int found = 0;
       +                                for (size_t i = 0; i < LENGTH(widget_funcs); i++) {
       +                                        if (widget_funcs[i].cmd && !strcmp(tokens[0].text, widget_funcs[i].name)) {
       +                                                err = widget_funcs[i].cmd(window, tokens, num_tokens, &errdetail);
       +                                                found = 1;
       +                                        }
       +                                }
       +                                if (!found) {
       +                                        errdetail.type = ERR_INVALID_COMMAND;
       +                                        errdetail.arg = -1;
       +                                        err = 1;
       +                                }
       +                        }
       +                        sock->tokens.num_tokens = 0;
       +                        sock->last_seq = seq;
       +                }
       +                if (err) {
       +                        const char *errmsg = errtype_to_string(errdetail.type);
       +                        if (ltkd_queue_sock_write_fmt(client, "err %d %d \"%s\"\n", errdetail.type, errdetail.arg, errmsg))
       +                                ltkd_fatal("Unable to queue socket write.\n");
       +                } else {
       +                        if (ltkd_queue_sock_write(client, "res ok\n", -1)) {
       +                                ltkd_fatal("Unable to queue socket write.\n");
       +                        }
       +                }
       +                if (last)
       +                        break;
       +        }
       +        if (sock->tokens.num_tokens > 0 && sock->tokens.tokens[0].text != sock->read) {
       +                memmove(sock->read, sock->tokens.tokens[0].text, sock->read + sock->read_len - sock->tokens.tokens[0].text);
       +                ptrdiff_t offset = sock->tokens.tokens[0].text - sock->read;
       +                /* Hmm, seems a bit ugly... */
       +                for (int i = 0; i < sock->tokens.num_tokens; i++) {
       +                        sock->tokens.tokens[i].text -= offset;
       +                }
       +                sock->read_len -= offset;
       +                sock->read_cur -= offset;
       +        } else if (sock->tokens.num_tokens == 0) {
       +                sock->read_len = 0;
       +                sock->read_cur = 0;
       +        }
       +        return retval;
       +}
       +
       +LTK_GEN_LOG_FUNCS(ltkd, ltkd_log_msg, ltkd_cleanup)
   DIR diff --git a/src/ltkd/ltkd.h b/src/ltkd/ltkd.h
       t@@ -0,0 +1,43 @@
       +/*
       + * Copyright (c) 2016-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_H
       +#define LTKD_H
       +
       +#include <stddef.h>
       +#include <ltk/ltk.h>
       +
       +typedef struct {
       +        char *text;
       +        size_t len;
       +        int contains_nul;
       +} ltkd_cmd_token;
       +
       +typedef enum {
       +        LTK_EVENT_RESIZE = 1 << 0,
       +        LTK_EVENT_BUTTON = 1 << 1,
       +        LTK_EVENT_KEY = 1 << 2,
       +        LTK_EVENT_MENU = 1 << 3
       +} ltkd_userevent_type;
       +
       +void ltkd_queue_event(ltk_window *window, ltkd_userevent_type type, const char *id, const char *data);
       +int ltkd_handle_lock_client(ltk_window *window, int client);
       +int ltkd_queue_sock_write(int client, const char *str, int len);
       +int ltkd_queue_sock_write_fmt(int client, const char *fmt, ...);
       +
       +LTK_GEN_LOG_FUNC_PROTO(ltkd)
       +
       +#endif /* LTKD_H */
   DIR diff --git a/src/ltkd/menu.c b/src/ltkd/menu.c
       t@@ -0,0 +1,272 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <stddef.h>
       +#include <string.h>
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "cmd.h"
       +#include "proto_types.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/util.h>
       +#include <ltk/menu.h>
       +
       +/* [sub]menu <menu id> create */
       +static int
       +ltkd_menu_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_menu *menu;
       +        if (!strcmp(cmd[0].val.str, "menu")) {
       +                menu = ltk_menu_create(window);
       +        } else {
       +                menu = ltk_submenu_create(window);
       +        }
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(menu), cmd[1].val.str, NULL, 0, err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(menu), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menu <menu id> insert-entry <entry widget id> <index> */
       +static int
       +ltkd_menu_cmd_insert_entry(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_menu *menu = LTK_CAST_MENU(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
       +                {.type = CMDARG_INT, .min = 0, .max = menu->num_entries, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        int ret;
       +        if ((ret = ltk_menu_insert_entry(menu, LTK_CAST_MENUENTRY(cmd[0].val.widget->widget), cmd[1].val.i))) {
       +                err->type = ret == 1 ? ERR_WIDGET_IN_CONTAINER : ERR_INVALID_INDEX;
       +                err->arg = err->type == ERR_WIDGET_IN_CONTAINER ? 0 : 1;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menu <menu id> add-entry <entry widget id> */
       +static int
       +ltkd_menu_cmd_add_entry(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_menu *menu = LTK_CAST_MENU(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (ltk_menu_add_entry(menu, LTK_CAST_MENUENTRY(cmd[0].val.widget->widget))) {
       +                err->type = ERR_WIDGET_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-entry-index <entry index> */
       +static int
       +ltkd_menu_cmd_remove_entry_index(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_menu *menu = LTK_CAST_MENU(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_INT, .min = 0, .max = menu->num_entries - 1, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        if (!ltk_menu_remove_entry_index(menu, cmd[0].val.i)) {
       +                err->type = ERR_WIDGET_NOT_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-entry-id <entry id> */
       +static int
       +ltkd_menu_cmd_remove_entry_id(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_menu *menu = LTK_CAST_MENU(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_menuentry *entry = LTK_CAST_MENUENTRY(cmd[0].val.widget->widget);
       +        if (!ltk_menu_remove_entry(menu, entry)) {
       +                err->type = ERR_WIDGET_NOT_IN_CONTAINER;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-all-entries */
       +static int
       +ltkd_menu_cmd_remove_all_entries(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        (void)tokens;
       +        (void)num_tokens;
       +        (void)err;
       +        ltk_menu *menu = LTK_CAST_MENU(widget->widget);
       +        ltk_menu_remove_all_entries(menu);
       +        return 0;
       +}
       +
       +static int
       +ltkd_menuentry_press(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        (void)args;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        return ltkd_widget_queue_specific_event(widget, "menuentry", LTKD_PWEVENTMASK_MENUENTRY_PRESS, "press");
       +}
       +
       +static ltkd_event_handler entry_handlers[] = {
       +        {&ltkd_menuentry_press, LTK_MENUENTRY_SIGNAL_PRESSED},
       +};
       +
       +/* menuentry <id> create <text> */
       +static int
       +ltkd_menuentry_cmd_create(
       +    ltk_window *window,
       +    ltkd_widget *widget_unneeded,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)widget_unneeded;
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +                {.type = CMDARG_IGNORE, .optional = 0},
       +                {.type = CMDARG_STRING, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        ltk_menuentry *e = ltk_menuentry_create(window, cmd[3].val.str);
       +        if (!ltkd_widget_create(LTK_CAST_WIDGET(e), cmd[1].val.str, entry_handlers, LENGTH(entry_handlers), err)) {
       +                ltk_widget_destroy(LTK_CAST_WIDGET(e), 1);
       +                err->arg = 1;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menuentry <menuentry id> attach-submenu <submenu id> */
       +static int
       +ltkd_menuentry_cmd_attach_submenu(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        ltk_menuentry *e = LTK_CAST_MENUENTRY(widget->widget);
       +        ltkd_cmdarg_parseinfo cmd[] = {
       +                {.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENU, .optional = 0},
       +        };
       +        if (ltkd_parse_cmd(tokens, num_tokens, cmd, LENGTH(cmd), err))
       +                return 1;
       +        int ret;
       +        if ((ret = ltk_menuentry_attach_submenu(e, LTK_CAST_MENU(cmd[0].val.widget->widget)))) {
       +                /* FIXME: allow setting err->arg to arg before the args given to function */
       +                /*err->arg = err->type == ERR_MENU_NOT_SUBMENU ? 0 : -2;*/
       +                err->type = ret == 1 ? ERR_MENU_NOT_SUBMENU : ERR_MENU_ENTRY_CONTAINS_SUBMENU;
       +                err->arg = 0;
       +                return 1;
       +        }
       +        return 0;
       +}
       +
       +/* menuentry <menuentry id> detach-submenu */
       +static int
       +ltkd_menuentry_cmd_detach_submenu(
       +    ltk_window *window,
       +    ltkd_widget *widget,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        (void)tokens;
       +        (void)num_tokens;
       +        (void)err;
       +        ltk_menuentry *e = LTK_CAST_MENUENTRY(widget->widget);
       +        ltk_menuentry_detach_submenu(e);
       +        return 0;
       +}
       +
       +/* FIXME: sort out menu/submenu - it's weird right now */
       +/* FIXME: distinguish between menu/submenu in commands other than create? */
       +
       +static ltkd_cmd_info menu_cmds[] = {
       +        {"add-entry", &ltkd_menu_cmd_add_entry, 0},
       +        {"create", &ltkd_menu_cmd_create, 1},
       +        {"insert-entry", &ltkd_menu_cmd_insert_entry, 0},
       +        {"remove-all-entries", &ltkd_menu_cmd_remove_all_entries, 0},
       +        {"remove-entry-index", &ltkd_menu_cmd_remove_entry_index, 0},
       +        {"remove-entry-id", &ltkd_menu_cmd_remove_entry_id, 0},
       +};
       +
       +static ltkd_cmd_info menuentry_cmds[] = {
       +        {"attach-submenu", &ltkd_menuentry_cmd_attach_submenu, 0},
       +        {"create", &ltkd_menuentry_cmd_create, 1},
       +        {"detach-submenu", &ltkd_menuentry_cmd_detach_submenu, 0},
       +};
       +
       +GEN_CMD_HELPERS(ltkd_menu_cmd, LTK_WIDGET_MENU, menu_cmds)
       +GEN_CMD_HELPERS(ltkd_menuentry_cmd, LTK_WIDGET_MENUENTRY, menuentry_cmds)
   DIR diff --git a/src/ltkd/proto_types.h b/src/ltkd/proto_types.h
       t@@ -0,0 +1,51 @@
       +/*
       + * Copyright (c) 2022-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_PROTO_TYPES_H
       +#define LTKD_PROTO_TYPES_H
       +
       +/* P == protocol; W == widget */
       +
       +#define LTKD_PEVENT_MOUSEPRESS     0
       +#define LTKD_PEVENT_MOUSERELEASE   1
       +#define LTKD_PEVENT_MOUSEMOTION    2
       +#define LTKD_PEVENT_MOUSESCROLL    3
       +#define LTKD_PEVENT_KEYPRESS       4
       +#define LTKD_PEVENT_KEYRELEASE     5
       +#define LTKD_PEVENT_RESIZE         6
       +#define LTKD_PEVENT_STATECHANGE    7
       +
       +/* FIXME: standardize names - internally, buttonpress is used, here it's mousepress... */
       +#define LTKD_PEVENTMASK_NONE           (UINT32_C(0))
       +#define LTKD_PEVENTMASK_MOUSEPRESS     (UINT32_C(1) << LTKD_PEVENT_MOUSEPRESS)
       +#define LTKD_PEVENTMASK_MOUSERELEASE   (UINT32_C(1) << LTKD_PEVENT_MOUSERELEASE)
       +#define LTKD_PEVENTMASK_MOUSEMOTION    (UINT32_C(1) << LTKD_PEVENT_MOUSEMOTION)
       +#define LTKD_PEVENTMASK_KEYPRESS       (UINT32_C(1) << LTKD_PEVENT_KEYPRESS)
       +#define LTKD_PEVENTMASK_KEYRELEASE     (UINT32_C(1) << LTKD_PEVENT_KEYRELEASE)
       +#define LTKD_PEVENTMASK_RESIZE         (UINT32_C(1) << LTKD_PEVENT_RESIZE)
       +#define LTKD_PEVENTMASK_EXPOSE         (UINT32_C(1) << LTKD_PEVENT_EXPOSE)
       +#define LTKD_PEVENTMASK_STATECHANGE    (UINT32_C(1) << LTKD_PEVENT_STATECHANGE)
       +#define LTKD_PEVENTMASK_MOUSESCROLL    (UINT32_C(1) << LTKD_PEVENT_MOUSESCROLL)
       +
       +#define LTKD_PWEVENT_MENUENTRY_PRESS     0
       +#define LTKD_PWEVENTMASK_MENUENTRY_NONE  (UINT32_C(0))
       +#define LTKD_PWEVENTMASK_MENUENTRY_PRESS (UINT32_C(1) << LTKD_PWEVENT_MENUENTRY_PRESS)
       +
       +#define LTKD_PWEVENT_BUTTON_PRESS     0
       +#define LTKD_PWEVENTMASK_BUTTON_NONE  (UINT32_C(0))
       +#define LTKD_PWEVENTMASK_BUTTON_PRESS (UINT32_C(1) << LTKD_PWEVENT_BUTTON_PRESS)
       +
       +#endif /* LTKD_PROTO_TYPES_H */
   DIR diff --git a/src/ltkd/socket_format.txt b/src/ltkd/socket_format.txt
       t@@ -0,0 +1,106 @@
       +General:
       +
       +All requests, responses, errors, and events start with a sequence number.
       +This number starts at 0 and is incremented by the client with each request.
       +When the server sends a response, error, or event, it starts with the last
       +sequence number that the client sent. The client 'ltkc' adds sequence
       +numbers automatically (perhaps it should hide them in its output as well,
       +but I'm too lazy to implement that right now). A more advanced client could
       +use the numbers to properly check that requests really were received.
       +It isn't clear yet what should happen in the pathological case that the
       +uint32_t used to store the sequence number overflows before any of the
       +requests have been handled (i.e. it isn't clear anymore which request a
       +reply is for). I guess this could technically happen when running over a
       +broken connection or something, but I don't have a solution right now.
       +It doesn't seem like a very realistic scenario, though, considering that
       +all the requests/responses would need to be buffered somewhere, which
       +would be somewhat unrealistic considering the size of uint32_t.
       +
       +Requests:
       +
       +<widget type> <widget id> <command> <args>
       +> grid grd1 create 2 2
       +
       +If the command takes a string, the string may contain newlines:
       +> button btn1 create "I'm a
       +> button!"
       +
       +The command line is read until the first newline that is not
       +within a string.
       +
       +Double quotes must be escaped in strings, like so:
       +> button btn1 create "Bla\"bla"
       +
       +Essentially, the individual messages are separated by line
       +breaks (\n), but line breaks within strings don't break the
       +message.
       +
       +Responses:
       +
       +Not properly implemented yet.
       +Currently, all requests that don't generate errors just get the response
       +"<sequence> res ok". Of course, this will be changed once other response
       +types are available (e.g. get-text and others).
       +
       +It might be good to allow ignoring responses to avoid lots of useless traffic.
       +On the client side, the usage could be similar to XCB's checked/unchecked.
       +
       +Errors:
       +
       +err <error number> <number of bad argument or -1> <string description of error>
       +
       +Events:
       +
       +event[l] <widget id> <widget type or "widget" for generic events> <event name> [further data]
       +
       +By default, no events are reported. An event mask has to be set first:
       +
       +mask-add <widget id> <type> <event name> [lock]
       +mask-remove <widget id> <type> <event name> [lock]
       +mask-set <widget id> <type> <event name> [lock]
       +
       +<type> is either "widget" for generic events (mousepress, mouserelease,
       +mousemotion, resize, statechange) or the widget type for specific events.
       +The only specific event currently supported is "press" for both "button"
       +and "menuentry". Note that currently, only a single mask type can be
       +added/removed at once instead of combining multiple in one request
       +(i.e. it isn't possible to do something like "mousepress|mouserelease" yet).
       +
       +If "lock" is set, the "lock mask" is manipulated instead of the normal one.
       +If an event occurs that is included in the lock mask, the event will start
       +with "eventl" instead of "event", and the ltk server blocks until it gets the
       +request "event-unlock [true/false]" from the client that received the event.
       +Note that if multiple clients have an event in their lock mask, all of them will
       +receive the event, but only one of them is chosen to listen for the event-unlock
       +(basically, this is unspecified behavior that should be avoided). If event-unlock
       +includes "true", the event is not processed further by the ltk server. If "false"
       +is given instead, the event is processed as usual by the ltk server.
       +
       +This was added to allow functionality like in regular GUI toolkits where it is
       +possible to override events completely. The problem is that it currently isn't
       +really clear where exactly the command should be emitted and whether it really
       +makes sense to block all further processing (some processing has to be done
       +even now for it to make any sense at all). That could possibly lead to very
       +weird bugs. It also currently isn't possible to do much after locking because
       +no useful low-level functions for widgets exist (yet?). All in all, I'm not
       +entirely sure how to make this work nicely so it is actually useful.
       +Since all of this is pushed over a socket and will probably be able to run
       +over a network connection eventually, it will also cause problems with latency.
       +
       +Miscellaneous:
       +
       +It probably isn't too great for security when anyone can do anything with the
       +window. Maybe it would be better to allow different clients to have different
       +permissions? For instance, maybe only the main client could change things, but
       +other clients could have readonly permissions for things like screenreaders.
       +That would probably get very over-complicated, though.
       +
       +I'm also seriously considering switching to a binary socket format. It's nice
       +to have a text format, but it's an absolute pain to process, because everything
       +has to be converted from/to text. It also isn't nearly as efficient, especially
       +if more complicated things are done, such as listening for all mousemotion events.
       +Of course, it could be made much more efficient with various lookup tables
       +(it isn't implemented very efficiently currently), but still not nearly as good
       +as a binary protocol. The idea would be to have a binary protocol, but to still
       +have something like ltkc that converts the protocol to a text format so simple
       +shell clients can still exist, but more complicated programs aren't hindered by it.
   DIR diff --git a/src/ltkd/util.c b/src/ltkd/util.c
       t@@ -0,0 +1,31 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <fcntl.h>
       +#include <errno.h>
       +
       +#include "ltkd.h"
       +#include "util.h"
       +
       +int
       +ltkd_set_nonblock(int fd) {
       +        int flags = fcntl(fd, F_GETFL, 0);
       +        if (flags == -1)
       +                return -1;
       +        if (fcntl(fd, F_SETFL, flags | O_NONBLOCK))
       +                return -1;
       +        return 0;
       +}
   DIR diff --git a/src/ltkd/util.h b/src/ltkd/util.h
       t@@ -0,0 +1,22 @@
       +/*
       + * Copyright (c) 2023-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_UTIL_H
       +#define LTKD_UTIL_H
       +
       +int ltkd_set_nonblock(int fd);
       +
       +#endif /* LTKD_UTIL_H */
   DIR diff --git a/src/ltkd/widget.c b/src/ltkd/widget.c
       t@@ -0,0 +1,553 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#include <string.h>
       +
       +#include "khash.h"
       +
       +#include "err.h"
       +#include "ltkd.h"
       +#include "widget.h"
       +#include "proto_types.h"
       +
       +#include <ltk/ltk.h>
       +#include <ltk/event.h>
       +#include <ltk/eventdefs.h>
       +#include <ltk/memory.h>
       +#include <ltk/rect.h>
       +#include <ltk/util.h>
       +
       +KHASH_MAP_INIT_STR(widget, ltkd_widget *)
       +static khash_t(widget) *widget_hash = NULL;
       +/* Hack to make ltkd_destroy_widget_hash work */
       +/* FIXME: any better way to do this? */
       +static int hash_locked = 0;
       +
       +static int ltkd_widget_button_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
       +static int ltkd_widget_motion_notify(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
       +static int ltkd_widget_scroll_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
       +static int ltkd_widget_resize(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
       +static int ltkd_widget_change_state(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg);
       +
       +/* FIXME: generic event handling functions that take the actual uint32_t event type, etc. */
       +int
       +ltkd_widget_queue_specific_event(ltkd_widget *widget, const char *type, uint32_t mask, const char *data) {
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].lwmask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "eventl %s %s %s\n", widget->id, type, data
       +                        );
       +                        if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
       +                                return 1;
       +                } else if (widget->event_masks[i].wmask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "event %s %s %s\n", widget->id, type, data
       +                        );
       +                }
       +        }
       +        return 0;
       +}
       +
       +static int
       +ltkd_widget_resize(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        (void)args;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].lmask & LTKD_PEVENTMASK_RESIZE) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "eventl %s widget configure %d %d %d %d\n",
       +                            widget->id, widget->widget->lrect.x, widget->widget->lrect.y,
       +                            widget->widget->lrect.w, widget->widget->lrect.h
       +                        );
       +                        if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
       +                                return 1;
       +                } else if (widget->event_masks[i].mask & LTKD_PEVENTMASK_RESIZE) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "event %s widget configure %d %d %d %d\n",
       +                            widget->id, widget->widget->lrect.x, widget->widget->lrect.y,
       +                            widget->widget->lrect.w, widget->widget->lrect.h
       +                        );
       +                }
       +        }
       +        return 0;
       +}
       +
       +static int
       +ltkd_widget_change_state(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        (void)args;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        /* FIXME: give old and new state in event */
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].lmask & LTKD_PEVENTMASK_STATECHANGE) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "eventl %s widget statechange\n", widget->id
       +                        );
       +                        if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
       +                                return 1;
       +                } else if (widget->event_masks[i].mask & LTKD_PEVENTMASK_STATECHANGE) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "event %s widget statechange\n", widget->id
       +                        );
       +                }
       +        }
       +        return 0;
       +}
       +
       +static void
       +ltkd_destroy_widget_hash(void) {
       +        hash_locked = 1;
       +        khint_t k;
       +        ltkd_widget *ptr;
       +        for (k = kh_begin(widget_hash); k != kh_end(widget_hash); k++) {
       +                if (kh_exist(widget_hash, k)) {
       +                        ptr = kh_value(widget_hash, k);
       +                        ltk_free((char *)kh_key(widget_hash, k));
       +                        ltkd_widget_destroy(ptr, 1);
       +                }
       +        }
       +        kh_destroy(widget, widget_hash);
       +        widget_hash = NULL;
       +        hash_locked = 0;
       +}
       +
       +void
       +ltkd_widgets_cleanup(void) {
       +        if (widget_hash)
       +                ltkd_destroy_widget_hash();
       +}
       +
       +static client_event_mask *
       +get_mask_struct(ltkd_widget *widget, int client) {
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].client == client)
       +                        return &widget->event_masks[i];
       +        }
       +        widget->masks_alloc = ideal_array_size(widget->masks_alloc, widget->masks_num + 1);
       +        widget->event_masks = ltk_reallocarray(widget->event_masks, widget->masks_alloc, sizeof(client_event_mask));
       +        client_event_mask *m = &widget->event_masks[widget->masks_num];
       +        widget->masks_num++;
       +        m->client = client;
       +        m->mask = m->lmask = m->wmask = m->lwmask = 0;
       +        return m;
       +}
       +
       +static ltkd_event_handler widget_handlers[] = {
       +        {&ltkd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_PRESS},
       +        {&ltkd_widget_button_event, LTK_WIDGET_SIGNAL_MOUSE_RELEASE},
       +        {&ltkd_widget_motion_notify, LTK_WIDGET_SIGNAL_MOTION_NOTIFY},
       +        {&ltkd_widget_scroll_event, LTK_WIDGET_SIGNAL_MOUSE_SCROLL},
       +        {NULL, LTK_WIDGET_SIGNAL_KEY_PRESS}, /* FIXME: add key press here */
       +        {NULL, LTK_WIDGET_SIGNAL_KEY_RELEASE},
       +        {&ltkd_widget_resize, LTK_WIDGET_SIGNAL_RESIZE},
       +        {&ltkd_widget_change_state, LTK_WIDGET_SIGNAL_CHANGE_STATE},
       +};
       +
       +static uint32_t
       +get_widget_mask(ltkd_widget *widget) {
       +        uint32_t cur_mask = 0;
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                cur_mask |= widget->event_masks[i].mask;
       +                cur_mask |= widget->event_masks[i].lmask;
       +        }
       +        return cur_mask;
       +}
       +
       +static uint32_t
       +get_widget_special_mask(ltkd_widget *widget) {
       +        uint32_t cur_mask = 0;
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                cur_mask |= widget->event_masks[i].wmask;
       +                cur_mask |= widget->event_masks[i].lwmask;
       +        }
       +        return cur_mask;
       +}
       +
       +static void
       +set_event_handlers(ltkd_widget *widget, uint32_t before, uint32_t after, ltkd_event_handler *handlers, size_t num_handlers) {
       +        for (size_t i = 0; i < num_handlers; i++) {
       +                if (!(before & 1) && (after & 1)) {
       +                        if (handlers[i].callback) {
       +                                ltk_widget_register_signal_handler(
       +                                        widget->widget, handlers[i].type,
       +                                        handlers[i].callback, LTK_MAKE_ARG_VOIDP(widget)
       +                                );
       +                        }
       +                } else if ((before & 1) && !(after & 1)) {
       +                        ltk_widget_remove_signal_handler_by_callback(widget->widget, handlers[i].callback);
       +                }
       +                before >>= 1;
       +                after >>= 1;
       +        }
       +}
       +
       +static void
       +ltkd_widget_set_event_handlers(ltkd_widget *widget, uint32_t *set_mask, uint32_t new_mask) {
       +        uint32_t before = get_widget_mask(widget);
       +        *set_mask = new_mask;
       +        uint32_t after = get_widget_mask(widget);
       +        set_event_handlers(widget, before, after, widget_handlers, LENGTH(widget_handlers));
       +}
       +
       +static void
       +ltkd_widget_set_special_event_handlers(ltkd_widget *widget, uint32_t *set_mask, uint32_t new_mask) {
       +        uint32_t before = get_widget_special_mask(widget);
       +        *set_mask = new_mask;
       +        uint32_t after = get_widget_special_mask(widget);
       +        set_event_handlers(widget, before, after, widget->event_handlers, widget->num_event_handlers);
       +}
       +
       +void
       +ltkd_widget_set_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->mask, mask);
       +}
       +
       +void
       +ltkd_widget_set_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->lmask, mask);
       +}
       +
       +void
       +ltkd_widget_set_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->wmask, mask);
       +}
       +
       +void
       +ltkd_widget_set_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->lwmask, mask);
       +}
       +
       +void
       +ltkd_widget_add_to_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->mask, m->mask | mask);
       +}
       +
       +void
       +ltkd_widget_add_to_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->lmask, m->lmask | mask);
       +}
       +
       +void
       +ltkd_widget_add_to_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->wmask, m->wmask | mask);
       +}
       +
       +void
       +ltkd_widget_add_to_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->lwmask, m->lwmask | mask);
       +}
       +
       +void
       +ltkd_widget_remove_from_event_mask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->mask, m->mask & ~mask);
       +}
       +
       +void
       +ltkd_widget_remove_from_event_lmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_event_handlers(widget, &m->lmask, m->lmask & ~mask);
       +}
       +
       +void
       +ltkd_widget_remove_from_event_wmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->wmask, m->wmask & ~mask);
       +}
       +
       +void
       +ltkd_widget_remove_from_event_lwmask(ltkd_widget *widget, int client, uint32_t mask) {
       +        client_event_mask *m = get_mask_struct(widget, client);
       +        ltkd_widget_set_special_event_handlers(widget, &m->lwmask, m->lwmask & ~mask);
       +}
       +
       +/* FIXME: any way to optimize the whole event mask handling a bit? */
       +void
       +ltkd_widget_remove_client(int client) {
       +        khint_t k;
       +        ltkd_widget *ptr;
       +        for (k = kh_begin(widget_hash); k != kh_end(widget_hash); k++) {
       +                if (kh_exist(widget_hash, k)) {
       +                        ptr = kh_value(widget_hash, k);
       +                        for (size_t i = 0; i < ptr->masks_num; i++) {
       +                                if (ptr->event_masks[i].client == client) {
       +                                        uint32_t before = get_widget_mask(ptr);
       +                                        uint32_t befores = get_widget_special_mask(ptr);
       +                                        memmove(ptr->event_masks + i, ptr->event_masks + i + 1, ptr->masks_num - i - 1);
       +                                        ptr->masks_num--;
       +                                        /* FIXME: maybe reset to NULL in that case? */
       +                                        if (ptr->masks_num > 0) {
       +                                                size_t sz = ideal_array_size(ptr->masks_alloc, ptr->masks_num);
       +                                                if (sz != ptr->masks_alloc) {
       +                                                        ptr->masks_alloc = sz;
       +                                                        ptr->event_masks = ltk_reallocarray(ptr->event_masks, sz, sizeof(client_event_mask));
       +                                                }
       +                                        }
       +                                        uint32_t after = get_widget_mask(ptr);
       +                                        uint32_t afters = get_widget_special_mask(ptr);
       +                                        set_event_handlers(ptr, before, after, widget_handlers, LENGTH(widget_handlers));
       +                                        set_event_handlers(ptr, befores, afters, ptr->event_handlers, ptr->num_event_handlers);
       +                                        break;
       +                                }
       +                        }
       +                }
       +        }
       +}
       +
       +void
       +ltkd_widgets_init() {
       +        widget_hash = kh_init(widget);
       +        if (!widget_hash) ltkd_fatal_errno("Unable to initialize widget hash table.\n");
       +}
       +
       +/* FIXME: fix global and local coordinates! */
       +static int
       +queue_mouse_event(ltkd_widget *widget, ltk_event_type type, int x, int y) {
       +        uint32_t mask;
       +        char *typename;
       +        switch (type) {
       +        case LTK_MOTION_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSEMOTION;
       +                typename = "mousemotion";
       +                break;
       +        case LTK_2BUTTONPRESS_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSEPRESS;
       +                typename = "2mousepress";
       +                break;
       +        case LTK_3BUTTONPRESS_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSEPRESS;
       +                typename = "3mousepress";
       +                break;
       +        case LTK_BUTTONRELEASE_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSERELEASE;
       +                typename = "mouserelease";
       +                break;
       +        case LTK_2BUTTONRELEASE_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSERELEASE;
       +                typename = "2mouserelease";
       +                break;
       +        case LTK_3BUTTONRELEASE_EVENT:
       +                mask = LTKD_PEVENTMASK_MOUSERELEASE;
       +                typename = "3mouserelease";
       +                break;
       +        case LTK_BUTTONPRESS_EVENT:
       +        default:
       +                mask = LTKD_PEVENTMASK_MOUSEPRESS;
       +                typename = "mousepress";
       +                break;
       +        }
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].lmask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "eventl %s widget %s %d %d %d %d\n",
       +                            widget->id, typename, x, y, x, y
       +                            /* x - widget->rect.x, y - widget->rect.y */
       +                        );
       +                        if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
       +                                return 1;
       +                } else if (widget->event_masks[i].mask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "event %s widget %s %d %d %d %d\n",
       +                            widget->id, typename, x, y, x, y
       +                            /* x - widget->rect.x, y - widget->rect.y */
       +                        );
       +                }
       +        }
       +        return 0;
       +}
       +
       +static int
       +ltkd_widget_button_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        ltk_button_event *event = LTK_GET_ARG_BUTTON_EVENT(args, 0);
       +        return queue_mouse_event(widget, event->type, event->x, event->y);
       +}
       +
       +static int
       +ltkd_widget_motion_notify(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        ltk_motion_event *event = LTK_GET_ARG_MOTION_EVENT(args, 0);
       +        return queue_mouse_event(widget, event->type, event->x, event->y);
       +}
       +
       +/* FIXME: global/local coords (like above) */
       +static int
       +ltkd_widget_scroll_event(ltk_widget *widget_unused, ltk_callback_arglist args, ltk_callback_arg arg) {
       +        (void)widget_unused;
       +        ltk_scroll_event *event = LTK_GET_ARG_SCROLL_EVENT(args, 0);
       +        ltkd_widget *widget = LTK_CAST_ARG_VOIDP(arg);
       +        uint32_t mask = LTKD_PEVENTMASK_MOUSESCROLL;
       +        for (size_t i = 0; i < widget->masks_num; i++) {
       +                if (widget->event_masks[i].lmask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "eventl %s widget %s %d %d %d %d %d %d\n",
       +                            widget->id, "mousescroll", event->x, event->y, event->x, event->y, event->dx, event->dy
       +                            /* x - widget->widget->rect.x, y - widget->widget->rect.y */
       +                        );
       +                        if (ltkd_handle_lock_client(widget->widget->window, widget->event_masks[i].client))
       +                                return 1;
       +                } else if (widget->event_masks[i].mask & mask) {
       +                        ltkd_queue_sock_write_fmt(
       +                            widget->event_masks[i].client,
       +                            "event %s widget %s %d %d %d %d %d %d\n",
       +                            widget->id, "mousescroll", event->x, event->y, event->x, event->y, event->dx, event->dy
       +                            /* x - widget->widget->rect.x, y - widget->widget->rect.y */
       +                        );
       +                }
       +        }
       +        return 0;
       +}
       +
       +static int
       +ltkd_widget_id_free(const char *id) {
       +        khint_t k;
       +        k = kh_get(widget, widget_hash, id);
       +        if (k != kh_end(widget_hash)) {
       +                return 0;
       +        }
       +        return 1;
       +}
       +
       +ltkd_widget *
       +ltkd_get_widget(const char *id, ltk_widget_type type, ltkd_error *err) {
       +        khint_t k;
       +        ltkd_widget *widget;
       +        k = kh_get(widget, widget_hash, id);
       +        if (k == kh_end(widget_hash)) {
       +                err->type = ERR_INVALID_WIDGET_ID;
       +                return NULL;
       +        }
       +        widget = kh_value(widget_hash, k);
       +        if (type != LTK_WIDGET_UNKNOWN && widget->widget->vtable->type != type) {
       +                err->type = ERR_INVALID_WIDGET_TYPE;
       +                return NULL;
       +        }
       +        return widget;
       +}
       +
       +static void
       +ltkd_set_widget(ltkd_widget *widget, const char *id) {
       +        int ret;
       +        khint_t k;
       +        /* FIXME: make sure no widget is overwritten here */
       +        char *tmp = ltk_strdup(id);
       +        k = kh_put(widget, widget_hash, tmp, &ret);
       +        kh_value(widget_hash, k) = widget;
       +}
       +
       +static void
       +ltkd_remove_widget(const char *id) {
       +        if (hash_locked)
       +                return;
       +        khint_t k;
       +        k = kh_get(widget, widget_hash, id);
       +        if (k != kh_end(widget_hash)) {
       +                ltk_free((char *)kh_key(widget_hash, k));
       +                kh_del(widget, widget_hash, k);
       +        }
       +}
       +
       +ltkd_widget *
       +ltkd_widget_create(
       +    ltk_widget *widget, const char *id,
       +    ltkd_event_handler *event_handlers, size_t num_event_handlers, ltkd_error *err){
       +        if (!ltkd_widget_id_free(id)) {
       +                err->type = ERR_WIDGET_ID_IN_USE;
       +                return NULL;
       +        }
       +        ltkd_widget *w = ltk_malloc(sizeof(ltkd_widget));
       +        w->widget = widget;
       +        w->id = ltk_strdup(id);
       +        w->event_masks = NULL;
       +        w->masks_num = w->masks_alloc = 0;
       +        w->event_handlers = event_handlers;
       +        w->num_event_handlers = num_event_handlers;
       +        ltkd_set_widget(w, id);
       +        return w;
       +}
       +
       +void
       +ltkd_widget_destroy(ltkd_widget *widget, int shallow) {
       +        ltkd_remove_widget(widget->id);
       +        ltk_free(widget->id);
       +        widget->id = NULL;
       +        ltk_free(widget->event_masks);
       +        widget->event_masks = NULL;
       +        ltk_widget_destroy(widget->widget, shallow);
       +        ltk_free(widget);
       +}
       +
       +int
       +ltkd_widget_destroy_cmd(
       +    ltk_window *window,
       +    ltkd_cmd_token *tokens,
       +    size_t num_tokens,
       +    ltkd_error *err) {
       +        (void)window;
       +        int shallow = 1;
       +        if (num_tokens != 2 && num_tokens != 3) {
       +                err->type = ERR_INVALID_NUMBER_OF_ARGUMENTS;
       +                err->arg = -1;
       +                return 1;
       +        }
       +        if (tokens[1].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 1;
       +                return 1;
       +        } else if (num_tokens == 3 && tokens[2].contains_nul) {
       +                err->type = ERR_INVALID_ARGUMENT;
       +                err->arg = 2;
       +                return 1;
       +        }
       +        if (num_tokens == 3) {
       +                if (strcmp(tokens[2].text, "deep") == 0) {
       +                        shallow = 0;
       +                } else if (strcmp(tokens[2].text, "shallow") == 0) {
       +                        shallow = 1;
       +                } else {
       +                        err->type = ERR_INVALID_ARGUMENT;
       +                        err->arg = 2;
       +                        return 1;
       +                }
       +        }
       +        ltkd_widget *widget = ltkd_get_widget(tokens[1].text, LTK_WIDGET_UNKNOWN, err);
       +        if (!widget) {
       +                err->arg = 1;
       +                return 1;
       +        }
       +        ltkd_widget_destroy(widget, shallow);
       +        return 0;
       +}
   DIR diff --git a/src/ltkd/widget.h b/src/ltkd/widget.h
       t@@ -0,0 +1,87 @@
       +/*
       + * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       + *
       + * Permission to use, copy, modify, and/or distribute this software for any
       + * purpose with or without fee is hereby granted, provided that the above
       + * copyright notice and this permission notice appear in all copies.
       + *
       + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       + */
       +
       +#ifndef LTKD_WIDGET_H
       +#define LTKD_WIDGET_H
       +
       +#include <stddef.h>
       +#include <stdint.h>
       +
       +#include "ltkd.h"
       +#include "err.h"
       +
       +#include <ltk/ltk.h>
       +
       +typedef struct {
       +        int client;      /* index of client */
       +        uint32_t mask;   /* generic event mask */
       +        uint32_t lmask;  /* generic lock mask */
       +        uint32_t wmask;  /* event mask for specific widget type */
       +        uint32_t lwmask; /* lock event mask for specific widget type */
       +} client_event_mask;
       +
       +typedef struct {
       +        ltk_signal_callback callback;
       +        int type;
       +} ltkd_event_handler;
       +
       +typedef struct {
       +        ltk_widget *widget;
       +        char *id;
       +        client_event_mask *event_masks;
       +        /* FIXME: kind of a waste of space to use size_t here */
       +        size_t masks_num;
       +        size_t masks_alloc;
       +        ltkd_event_handler *event_handlers;
       +        size_t num_event_handlers;
       +} ltkd_widget;
       +
       +/* FIXME: document that multiple clients locking on event is undefined
       +   (or at least order is undefined) */
       +
       +void ltkd_widgets_init();
       +ltkd_widget *ltkd_widget_create(
       +        ltk_widget *widget, const char *id,
       +        ltkd_event_handler *event_handlers, size_t num_event_handlers, ltkd_error *err
       +);
       +ltkd_widget *ltkd_get_widget(const char *id, ltk_widget_type type, ltkd_error *err);
       +
       +int ltkd_widget_queue_specific_event(ltkd_widget *widget, const char *type, uint32_t mask, const char *data);
       +
       +/*int ltkd_widget_id_free(const char *id);
       +void ltkd_set_widget(ltkd_widget *widget, const char *id);
       +void ltkd_remove_widget(const char *id);*/
       +
       +void ltkd_widget_destroy(ltkd_widget *widget, int shallow);
       +int ltkd_widget_destroy_cmd(ltk_window *window, ltkd_cmd_token *tokens, size_t num_tokens, ltkd_error *err);
       +void ltkd_widget_remove_client(int client);
       +
       +void ltkd_widgets_cleanup(void);
       +
       +void ltkd_widget_set_event_mask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_set_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_set_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_set_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_add_to_event_mask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_add_to_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_add_to_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_add_to_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_remove_from_event_mask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_remove_from_event_lmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_remove_from_event_wmask(ltkd_widget *widget, int client, uint32_t mask);
       +void ltkd_widget_remove_from_event_lwmask(ltkd_widget *widget, int client, uint32_t mask);
       +
       +#endif /* LTKD_WIDGET_H */
   DIR diff --git a/src/util.c b/src/util.c
       t@@ -1,436 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#include <pwd.h>
       -#include <time.h>
       -#include <ctype.h>
       -#include <errno.h>
       -#include <stdio.h>
       -#include <stdlib.h>
       -#include <string.h>
       -#include <stdarg.h>
       -#include <unistd.h>
       -#include <sys/stat.h>
       -
       -#include "util.h"
       -#include "array.h"
       -#include "memory.h"
       -#include "txtbuf.h"
       -
       -/* FIXME: Should these functions really fail on memory error? */
       -
       -char *
       -ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret) {
       -        long len;
       -        char *file_contents;
       -        FILE *file;
       -
       -        /* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
       -        file = fopen(filename, "r");
       -        if (!file) goto error;
       -        if (fseek(file, 0, SEEK_END)) goto errorclose;
       -        len = ftell(file);
       -        if (len < 0) goto errorclose;
       -        if (fseek(file, 0, SEEK_SET)) goto errorclose;
       -        file_contents = ltk_malloc((size_t)len + 1);
       -        clearerr(file);
       -        fread(file_contents, 1, (size_t)len, file);
       -        if (ferror(file)) goto errorclose;
       -        file_contents[len] = '\0';
       -        if (fclose(file)) goto error;
       -        *len_ret = (size_t)len;
       -        return file_contents;
       -error:
       -        if (errstr_ret)
       -                *errstr_ret = strerror(errno);
       -        return NULL;
       -errorclose:
       -        if (errstr_ret)
       -                *errstr_ret = strerror(errno);
       -        fclose(file);
       -        return NULL;
       -}
       -
       -/* FIXME: not sure if errno actually is set usefully after all these functions */
       -int
       -ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret) {
       -        FILE *file = fopen(path, "w");
       -        if (!file) goto error;
       -        clearerr(file);
       -        if (fwrite(data, 1, len, file) < len) goto errorclose;
       -        if (fclose(file)) goto error;
       -        return 0;
       -error:
       -        if (errstr_ret)
       -                *errstr_ret = strerror(errno);
       -        return 1;
       -errorclose:
       -        if (errstr_ret)
       -                *errstr_ret = strerror(errno);
       -        fclose(file);
       -        return 1;
       -}
       -
       -/* FIXME: maybe have a few standard array types defined somewhere else */
       -LTK_ARRAY_INIT_DECL_STATIC(cmd, char *)
       -LTK_ARRAY_INIT_IMPL_STATIC(cmd, char *)
       -
       -static void
       -free_helper(char *ptr) {
       -        ltk_free(ptr);
       -}
       -
       -/* FIXME: this is really ugly */
       -/* FIXME: parse command only once in beginning instead of each time it is run? */
       -/* FIXME: this handles double-quote, but the config parser already uses that, so
       -   it's kind of weird because it's parsed twice (also backslashes are parsed twice). */
       -int
       -ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename) {
       -        int bs = 0;
       -        int in_sqstr = 0;
       -        int in_dqstr = 0;
       -        int in_ws = 1;
       -        char c;
       -        size_t cur_start = 0;
       -        int offset = 0;
       -        txtbuf *cur_arg = txtbuf_new();
       -        ltk_array(cmd) *cmd = ltk_array_create(cmd, 4);
       -        char *cmdcopy = ltk_strndup(cmdtext, len);
       -        for (size_t i = 0; i < len; i++) {
       -                c = cmdcopy[i];
       -                if (c == '\\') {
       -                        if (bs) {
       -                                offset++;
       -                                bs = 0;
       -                        } else {
       -                                bs = 1;
       -                        }
       -                } else if (isspace(c)) {
       -                        if (!in_sqstr && !in_dqstr) {
       -                                if (bs) {
       -                                        if (in_ws) {
       -                                                in_ws = 0;
       -                                                cur_start = i;
       -                                                offset = 0;
       -                                        } else {
       -                                                offset++;
       -                                        }
       -                                        bs = 0;
       -                                } else if (!in_ws) {
       -                                        /* FIXME: shouldn't this be < instead of <=? */
       -                                        if (cur_start <= i - offset)
       -                                                txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       -                                        /* FIXME: cmd is named horribly */
       -                                        ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       -                                        txtbuf_clear(cur_arg);
       -                                        in_ws = 1;
       -                                        offset = 0;
       -                                }
       -                        /* FIXME: parsing weird here - bs just ignored */
       -                        } else if (bs) {
       -                                bs = 0;
       -                        }
       -                } else if (c == '%') {
       -                        if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (!in_sqstr && filename && i < len - 1 && cmdcopy[i + 1] == 'f') {
       -                                if (!in_ws && cur_start < i - offset)
       -                                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, i - cur_start - offset);
       -                                txtbuf_append(cur_arg, filename);
       -                                i++;
       -                                cur_start = i + 1;
       -                                offset = 0;
       -                        } else if (in_ws) {
       -                                cur_start = i;
       -                                offset = 0;
       -                        }
       -                        in_ws = 0;
       -                } else if (c == '"') {
       -                        if (in_sqstr) {
       -                                bs = 0;
       -                        } else if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (in_dqstr) {
       -                                offset++;
       -                                in_dqstr = 0;
       -                                continue;
       -                        } else {
       -                                in_dqstr = 1;
       -                                if (in_ws) {
       -                                        cur_start = i + 1;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                        continue;
       -                                }
       -                        }
       -                        in_ws = 0;
       -                } else if (c == '\'') {
       -                        if (in_dqstr) {
       -                                bs = 0;
       -                        } else if (bs) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                                bs = 0;
       -                        } else if (in_sqstr) {
       -                                offset++;
       -                                in_sqstr = 0;
       -                                continue;
       -                        } else {
       -                                in_sqstr = 1;
       -                                if (in_ws) {
       -                                        cur_start = i + 1;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                        continue;
       -                                }
       -                        }
       -                        in_ws = 0;
       -                } else if (bs) {
       -                        if (!in_sqstr && !in_dqstr) {
       -                                if (in_ws) {
       -                                        cur_start = i;
       -                                        offset = 0;
       -                                } else {
       -                                        offset++;
       -                                }
       -                        }
       -                        bs = 0;
       -                        in_ws = 0;
       -                } else {
       -                        if (in_ws) {
       -                                cur_start = i;
       -                                offset = 0;
       -                        }
       -                        in_ws = 0;
       -                }
       -                cmdcopy[i - offset] = cmdcopy[i];
       -        }
       -        if (in_sqstr || in_dqstr) {
       -                ltk_warn("Unterminated string in command\n");
       -                goto error;
       -        }
       -        if (!in_ws) {
       -                if (cur_start <= len - offset)
       -                        txtbuf_appendn(cur_arg, cmdcopy + cur_start, len - cur_start - offset);
       -                ltk_array_append(cmd, cmd, txtbuf_get_textcopy(cur_arg));
       -        }
       -        if (cmd->len == 0) {
       -                ltk_warn("Empty command\n");
       -                goto error;
       -        }
       -        ltk_array_append(cmd, cmd, NULL); /* necessary for execvp */
       -        int fret = -1;
       -        if ((fret = fork()) < 0) {
       -                ltk_warn("Unable to fork\n");
       -                goto error;
       -        } else if (fret == 0) {
       -                if (execvp(cmd->buf[0], cmd->buf) == -1) {
       -                        /* FIXME: what to do on error here? */
       -                        exit(1);
       -                }
       -        } else {
       -                ltk_free(cmdcopy);
       -                txtbuf_destroy(cur_arg);
       -                ltk_array_destroy_deep(cmd, cmd, &free_helper);
       -                return fret;
       -        }
       -error:
       -        ltk_free(cmdcopy);
       -        txtbuf_destroy(cur_arg);
       -        ltk_array_destroy_deep(cmd, cmd, &free_helper);
       -        return -1;
       -}
       -
       -/* If `needed` is larger than `*alloc_size`, resize `*str` to
       -   `max(needed, *alloc_size * 2)`. Aborts program on error. */
       -void
       -ltk_grow_string(char **str, int *alloc_size, int needed) {
       -        if (needed <= *alloc_size) return;
       -        int new_size = needed > (*alloc_size * 2) ? needed : (*alloc_size * 2);
       -        char *new = ltk_realloc(*str, new_size);
       -        *str = new;
       -        *alloc_size = new_size;
       -}
       -
       -/* Get the directory to store ltk files in and create it if it doesn't exist yet.
       -   This first checks the environment variable LTKDIR and, if that doesn't
       -   exist, the home directory with "/.ltk" appended.
       -   Returns NULL on error. */
       -char *
       -ltk_setup_directory(void) {
       -        char *dir, *dir_orig;
       -        struct passwd *pw;
       -        uid_t uid;
       -        int len;
       -
       -        dir_orig = getenv("LTKDIR");
       -        if (dir_orig) {
       -                dir = ltk_strdup(dir_orig);
       -                /*
       -                if (!dir)
       -                        return NULL;
       -                */
       -        } else {
       -                uid = getuid();
       -                pw = getpwuid(uid);
       -                if (!pw)
       -                        return NULL;
       -                len = strlen(pw->pw_dir);
       -                dir = ltk_malloc(len + 6);
       -                /*
       -                if (!dir)
       -                        return NULL;
       -                */
       -                strcpy(dir, pw->pw_dir);
       -                strcpy(dir + len, "/.ltk");
       -        }
       -
       -        if (mkdir(dir, 0770) < 0) {
       -                if (errno != EEXIST)
       -                        return NULL;
       -        }
       -
       -        return dir;
       -}
       -
       -/* Concatenate the two given strings and return the result.
       -   This allocates new memory for the result string, unlike
       -   the actual strcat. Aborts program on error */
       -char *
       -ltk_strcat_useful(const char *str1, const char *str2) {
       -        int len1, len2;
       -        char *ret;
       -
       -        len1 = strlen(str1);
       -        len2 = strlen(str2);
       -        ret = ltk_malloc(len1 + len2 + 1);
       -        strcpy(ret, str1);
       -        strcpy(ret + len1, str2);
       -
       -        return ret;
       -}
       -
       -void
       -ltk_log_msg(const char *mode, const char *format, va_list args) {
       -        char logtime[25]; /* FIXME: This should always be big enough, right? */
       -        time_t clock;
       -        struct tm *timeptr;
       -
       -        time(&clock);
       -        timeptr = localtime(&clock);
       -        strftime(logtime, 25, "%Y-%m-%d %H:%M:%S", timeptr);
       -
       -        fprintf(stderr, "%s ltk %s: ", logtime, mode);
       -        vfprintf(stderr, format, args);
       -}
       -
       -void
       -ltk_warn(const char *format, ...) {
       -        va_list args;
       -        va_start(args, format);
       -        ltk_log_msg("Warning", format, args);
       -        va_end(args);
       -}
       -
       -void
       -ltk_fatal(const char *format, ...) {
       -        va_list args;
       -        va_start(args, format);
       -        ltk_log_msg("Fatal", format, args);
       -        va_end(args);
       -        ltk_deinit();
       -
       -        exit(1);
       -}
       -
       -void
       -ltk_warn_errno(const char *format, ...) {
       -        va_list args;
       -        char *errstr = strerror(errno);
       -        va_start(args, format);
       -        ltk_log_msg("Warning", format, args);
       -        va_end(args);
       -        ltk_warn("system error: %s\n", errstr);
       -}
       -
       -void
       -ltk_fatal_errno(const char *format, ...) {
       -        va_list args;
       -        char *errstr = strerror(errno);
       -        va_start(args, format);
       -        ltk_log_msg("Fatal", format, args);
       -        va_end(args);
       -        ltk_fatal("system error: %s\n", errstr);
       -}
       -
       -int
       -str_array_equal(const char *terminated, const 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;
       -}
       -
       -size_t
       -prev_utf8(char *text, size_t index) {
       -        if (index == 0)
       -                return 0;
       -        size_t i = index - 1;
       -        /* find valid utf8 char - this probably needs to be improved */
       -        while (i > 0 && ((text[i] & 0xC0) == 0x80))
       -                i--;
       -        return i;
       -}
       -
       -size_t
       -next_utf8(char *text, size_t len, size_t index) {
       -        if (index >= len)
       -                return len;
       -        size_t i = index + 1;
       -        while (i < len && ((text[i] & 0xC0) == 0x80))
       -                i++;
       -        return i;
       -}
       -
       -void
       -ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr)
       -{
       -        (void)fprintf(stderr,
       -            "assertion \"%s\" failed: file \"%s\", line %d, function \"%s\"\n",
       -            failedexpr, file, line, func);
       -        abort();
       -        /* NOTREACHED */
       -}
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -1,62 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#ifndef LTK_UTIL_H
       -#define LTK_UTIL_H
       -
       -#include <stdarg.h>
       -#include <stddef.h>
       -
       -long long ltk_strtonum(
       -    const char *numstr, long long minval,
       -    long long maxval, const char **errstrp
       -);
       -
       -char *ltk_read_file(const char *filename, size_t *len_ret, char **errstr_ret);
       -int ltk_write_file(const char *path, const char *data, size_t len, char **errstr_ret);
       -int ltk_parse_run_cmd(const char *cmdtext, size_t len, const char *filename);
       -void ltk_grow_string(char **str, int *alloc_size, int needed);
       -char *ltk_setup_directory(void);
       -char *ltk_strcat_useful(const char *str1, const char *str2);
       -
       -/* Note: this is actually implemented in ltk.c (it is just
       -   declared here so it can be used by the utility functions */
       -void ltk_deinit(void);
       -
       -void ltk_log_msg(const char *mode, const char *format, va_list args);
       -void ltk_fatal_errno(const char *format, ...);
       -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(const char *terminated, const char *array, size_t len);
       -
       -size_t prev_utf8(char *text, size_t index);
       -size_t next_utf8(char *text, size_t len, size_t index);
       -
       -/* based on the assert found in OpenBSD */
       -void ltk_assert_impl(const char *file, int line, const char *func, const char *failedexpr);
       -#define ltk_assert(e) ((e) ? (void)0 : ltk_assert_impl(__FILE__, __LINE__, __func__, #e))
       -
       -#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,241 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#include <string.h>
       -
       -#include "rect.h"
       -#include "widget.h"
       -#include "window.h"
       -#include "memory.h"
       -#include "array.h"
       -
       -LTK_ARRAY_INIT_FUNC_DECL_STATIC(signal, ltk_signal_callback_info)
       -LTK_ARRAY_INIT_IMPL_STATIC(signal, ltk_signal_callback_info)
       -
       -void
       -ltk_fill_widget_defaults(ltk_widget *widget, ltk_window *window,
       -    struct ltk_widget_vtable *vtable, int w, int h) {
       -        widget->window = window;
       -        widget->parent = NULL;
       -
       -        /* FIXME: possibly check that draw and destroy aren't NULL */
       -        widget->vtable = vtable;
       -
       -        widget->state = LTK_NORMAL;
       -        widget->row = 0;
       -        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->row = 0;
       -        widget->column = 0;
       -        widget->row_span = 0;
       -        widget->column_span = 0;
       -        widget->sticky = 0;
       -        widget->dirty = 1;
       -        widget->hidden = 0;
       -        widget->vtable_copied = 0;
       -        widget->signal_cbs = NULL;
       -        /* FIXME: null other members! */
       -}
       -
       -void
       -ltk_widget_hide(ltk_widget *widget) {
       -        if (widget->vtable->hide)
       -                widget->vtable->hide(widget);
       -        widget->hidden = 1;
       -        /* remove hover state */
       -        /* FIXME: this needs to call change_state but that might cause issues */
       -        ltk_widget *hover = widget->window->hover_widget;
       -        while (hover) {
       -                if (hover == widget) {
       -                        widget->window->hover_widget->state &= ~LTK_HOVER;
       -                        widget->window->hover_widget = NULL;
       -                        break;
       -                }
       -                hover = hover->parent;
       -        }
       -        ltk_widget *pressed = widget->window->pressed_widget;
       -        while (pressed) {
       -                if (pressed == widget) {
       -                        widget->window->pressed_widget->state &= ~LTK_PRESSED;
       -                        widget->window->pressed_widget = NULL;
       -                        break;
       -                }
       -                pressed = pressed->parent;
       -        }
       -        ltk_widget *active = widget->window->active_widget;
       -        /* if current active widget is child, set active widget to widget above in hierarchy */
       -        int set_next = 0;
       -        while (active) {
       -                if (active == widget) {
       -                        set_next = 1;
       -                /* 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;
       -                }
       -                active = active->parent;
       -        }
       -        if (set_next && !active)
       -                ltk_window_set_active_widget(active->window, NULL);
       -}
       -
       -/* FIXME: Maybe pass the new width as arg here?
       -   That would make a bit more sense */
       -/* FIXME: maybe give global and local position in event */
       -void
       -ltk_widget_resize(ltk_widget *widget) {
       -        if (widget->vtable->resize)
       -                widget->vtable->resize(widget);
       -        widget->dirty = 1;
       -}
       -
       -void
       -ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state) {
       -        if (old_state == widget->state)
       -                return;
       -        if (widget->vtable->change_state)
       -                widget->vtable->change_state(widget, old_state);
       -        if (widget->vtable->flags & LTK_NEEDS_REDRAW) {
       -                widget->dirty = 1;
       -                ltk_window_invalidate_widget_rect(widget->window, widget);
       -        }
       -}
       -
       -/* FIXME: document that it's really dangerous to overwrite remove_child or destroy */
       -int
       -ltk_widget_destroy(ltk_widget *widget, int shallow) {
       -        /* widget->parent->remove_child should never be NULL because of the fact that
       -           the widget is set as parent, but let's just check anyways... */
       -        int invalid = 0;
       -        if (widget->parent) {
       -                if (widget->parent->vtable->remove_child)
       -                        invalid = widget->parent->vtable->remove_child(widget->parent, widget);
       -        }
       -        if (widget->vtable_copied) {
       -                ltk_free(widget->vtable);
       -                widget->vtable = NULL;
       -        }
       -        if (widget->signal_cbs) {
       -                ltk_array_destroy(signal, widget->signal_cbs);
       -                widget->signal_cbs = NULL;
       -        }
       -        widget->vtable->destroy(widget, shallow);
       -
       -        return invalid;
       -}
       -
       -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};
       -}
       -
       -int
       -ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data) {
       -        if ((type >= LTK_WIDGET_SIGNAL_INVALID) || type <= widget->vtable->invalid_signal)
       -                return 1;
       -        if (!widget->signal_cbs) {
       -                widget->signal_cbs = ltk_array_create(signal, 1);
       -        }
       -        ltk_array_append_signal(widget->signal_cbs, (ltk_signal_callback_info){callback, data, type});
       -        return 0;
       -}
       -
       -int
       -ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args) {
       -        if (!widget->signal_cbs)
       -                return 0;
       -        int handled = 0;
       -        for (size_t i = 0; i < ltk_array_len(widget->signal_cbs); i++) {
       -                if (ltk_array_get(widget->signal_cbs, i).type == type) {
       -                        handled |= ltk_array_get(widget->signal_cbs, i).callback(widget, args, ltk_array_get(widget->signal_cbs, i).data);
       -                }
       -        }
       -        return handled;
       -}
       -
       -static int
       -filter_by_type(ltk_signal_callback_info *info, void *data) {
       -        return info->type == *(int *)data;
       -}
       -
       -size_t
       -ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type) {
       -        if (!widget->signal_cbs)
       -                return 0;
       -        return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_type, &type);
       -}
       -
       -struct func_wrapper {
       -        ltk_signal_callback callback;
       -};
       -
       -static int
       -filter_by_callback(ltk_signal_callback_info *info, void *data) {
       -        return info->callback == ((struct func_wrapper *)data)->callback;
       -}
       -
       -size_t
       -ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback) {
       -        if (!widget->signal_cbs)
       -                return 0;
       -        /* callback can't be passed directly because ISO C forbids
       -           conversion of object pointer to function pointer */
       -        struct func_wrapper data = {callback};
       -        return ltk_array_remove_if(signal, widget->signal_cbs, &filter_by_callback, &data);
       -}
       -
       -int ltk_widget_register_type(void); /* FIXME */
       -
       -ltk_widget_vtable *
       -ltk_widget_get_editable_vtable(ltk_widget *widget) {
       -        if (!widget->vtable_copied) {
       -                ltk_widget_vtable *vtable = ltk_malloc(sizeof(ltk_widget_vtable));
       -                memcpy(vtable, widget->vtable, sizeof(ltk_widget_vtable));
       -                widget->vtable_copied = 1;
       -        }
       -        return widget->vtable;
       -}
   DIR diff --git a/src/widget.h b/src/widget.h
       t@@ -1,320 +0,0 @@
       -/*
       - * Copyright (c) 2021-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -/* need to check what happens when registering signal for destroy, but then calling ltk_destroy_widget from
       -that handler - maybe loop if container widget deletes children but they call parent->delete_child again */
       -#ifndef LTK_WIDGET_H
       -#define LTK_WIDGET_H
       -
       -/* FIXME: destroy signal for widgets (also window) */
       -
       -#include <stddef.h>
       -#include "array.h"
       -#include "event.h"
       -#include "graphics.h"
       -#include "rect.h"
       -#include "util.h"
       -
       -struct ltk_widget;
       -struct ltk_window;
       -typedef struct ltk_widget ltk_widget;
       -
       -typedef enum {
       -        LTK_WIDGET_UNKNOWN = 0,
       -        LTK_WIDGET_ANY,
       -        LTK_WIDGET_GRID,
       -        LTK_WIDGET_BUTTON,
       -        LTK_WIDGET_LABEL,
       -        LTK_WIDGET_BOX,
       -        LTK_WIDGET_MENU,
       -        LTK_WIDGET_MENUENTRY,
       -        LTK_WIDGET_ENTRY,
       -        LTK_WIDGET_IMAGE,
       -        LTK_WIDGET_WINDOW,
       -        LTK_WIDGET_SCROLLBAR,
       -        LTK_NUM_WIDGETS,
       -} ltk_widget_type;
       -
       -/* FIXME: SORT OUT INCLUDES PROPERLY! */
       -
       -typedef enum {
       -        LTK_ACTIVATABLE_NORMAL = 1,
       -        /* only activatable when "all-activatable"
       -           is set to true in the config */
       -        LTK_ACTIVATABLE_SPECIAL = 2,
       -        LTK_ACTIVATABLE_ALWAYS = 1|2,
       -        /* FIXME: redundant or needs better name - is implied by entries in vtable
       -           - if there are widgets that have keyboard functions in the vtable but
       -             shouldn't have this set, then it's a bad name */
       -        LTK_NEEDS_KEYBOARD = 4,
       -        LTK_NEEDS_REDRAW = 8,
       -        LTK_HOVER_IS_ACTIVE = 16,
       -} ltk_widget_flags;
       -
       -/* FIXME: "sticky" is maybe not the correct name anymore */
       -typedef enum {
       -        LTK_STICKY_NONE = 0,
       -        LTK_STICKY_LEFT = 1 << 0,
       -        LTK_STICKY_RIGHT = 1 << 1,
       -        LTK_STICKY_TOP = 1 << 2,
       -        LTK_STICKY_BOTTOM = 1 << 3,
       -        LTK_STICKY_SHRINK_WIDTH = 1 << 4,
       -        LTK_STICKY_SHRINK_HEIGHT = 1 << 5,
       -        LTK_STICKY_PRESERVE_ASPECT_RATIO = 1 << 6,
       -} ltk_sticky_mask;
       -
       -typedef enum {
       -        LTK_VERTICAL,
       -        LTK_HORIZONTAL
       -} ltk_orientation;
       -
       -typedef enum {
       -        LTK_NORMAL = 0,
       -        LTK_HOVER = 1,
       -        LTK_PRESSED = 2,
       -        LTK_ACTIVE = 4,
       -        LTK_HOVERACTIVE = 1 | 4,
       -        LTK_FOCUSED = 8,
       -        LTK_DISABLED = 16,
       -} ltk_widget_state;
       -
       -/* FIXME: need "ltk_register_type" just to get unique integer for type checking */
       -
       -typedef struct {
       -        union {
       -                int i;
       -                size_t sz;
       -                char c;
       -                ltk_widget *widget;
       -                char *str;
       -                const char *cstr;
       -                ltk_key_event *key_event;
       -                ltk_button_event *button_event;
       -                ltk_scroll_event *scroll_event;
       -                ltk_motion_event *motion_event;
       -                ltk_surface *surface;
       -                void *v;
       -                /* FIXME: maybe rewrite the functions to take
       -                   pointers instead so this doesn't increase
       -                   the size of the union (thereby increasing
       -                   the size of every arg in the arglist) */
       -                ltk_rect rect;
       -        } arg;
       -        enum {
       -                LTK_TYPE_INT,
       -                LTK_TYPE_SIZE_T,
       -                LTK_TYPE_CHAR,
       -                LTK_TYPE_WIDGET,
       -                LTK_TYPE_STRING,
       -                LTK_TYPE_CONST_STRING,
       -                LTK_TYPE_KEY_EVENT,
       -                LTK_TYPE_BUTTON_EVENT,
       -                LTK_TYPE_SCROLL_EVENT,
       -                LTK_TYPE_MOTION_EVENT,
       -                LTK_TYPE_SURFACE,
       -                LTK_TYPE_RECT,
       -                LTK_TYPE_VOID,
       -                LTK_TYPE_VOIDP,
       -        } type;
       -} ltk_callback_arg;
       -
       -/* FIXME: STRING should be CHARP for consistency */
       -#define LTK_MAKE_ARG_INT(data) ((ltk_callback_arg){.type = LTK_TYPE_INT, .arg = {.i = (data)}})
       -#define LTK_MAKE_ARG_SIZE_T(data) ((ltk_callback_arg){.type = LTK_TYPE_SIZE_T, .arg = {.sz = (data)}})
       -#define LTK_MAKE_ARG_CHAR(data) ((ltk_callback_arg){.type = LTK_TYPE_CHAR, .arg = {.c = (data)}})
       -#define LTK_MAKE_ARG_WIDGET(data) ((ltk_callback_arg){.type = LTK_TYPE_WIDGET, .arg = {.widget = (data)}})
       -#define LTK_MAKE_ARG_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_STRING, .arg = {.str = (data)}})
       -#define LTK_MAKE_ARG_CONST_STRING(data) ((ltk_callback_arg){.type = LTK_TYPE_CONST_STRING, .arg = {.cstr = (data)}})
       -#define LTK_MAKE_ARG_KEY_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_KEY_EVENT, .arg = {.key_event = (data)}})
       -#define LTK_MAKE_ARG_BUTTON_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_BUTTON_EVENT, .arg = {.button_event = (data)}})
       -#define LTK_MAKE_ARG_SCROLL_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_SCROLL_EVENT, .arg = {.scroll_event = (data)}})
       -#define LTK_MAKE_ARG_MOTION_EVENT(data) ((ltk_callback_arg){.type = LTK_TYPE_MOTION_EVENT, .arg = {.motion_event = (data)}})
       -#define LTK_MAKE_ARG_SURFACE(data) ((ltk_callback_arg){.type = LTK_TYPE_SURFACE, .arg = {.surface = (data)}})
       -#define LTK_MAKE_ARG_RECT(data) ((ltk_callback_arg){.type = LTK_TYPE_RECT, .arg = {.rect = (data)}})
       -#define LTK_MAKE_ARG_VOIDP(data) ((ltk_callback_arg){.type = LTK_TYPE_VOIDP, .arg = {.v = (data)}})
       -
       -#define LTK_ARG_VOID ((ltk_callback_arg){.type = LTK_TYPE_VOID})
       -
       -#define LTK_CAST_ARG_INT(carg) (ltk_assert(carg.type == LTK_TYPE_INT), carg.arg.i)
       -#define LTK_CAST_ARG_SIZE_T(carg) (ltk_assert(carg.type == LTK_TYPE_SIZE_T), carg.arg.sz)
       -#define LTK_CAST_ARG_CHAR(carg) (ltk_assert(carg.type == LTK_TYPE_CHAR), carg.arg.c)
       -#define LTK_CAST_ARG_WIDGET(carg) (ltk_assert(carg.type == LTK_TYPE_WIDGET), carg.arg.widget)
       -#define LTK_CAST_ARG_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_STRING), carg.arg.str)
       -#define LTK_CAST_ARG_CONST_STRING(carg) (ltk_assert(carg.type == LTK_TYPE_CONST_STRING), carg.arg.cstr)
       -#define LTK_CAST_ARG_KEY_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_KEY_EVENT), carg.arg.key_event)
       -#define LTK_CAST_ARG_BUTTON_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_BUTTON_EVENT), carg.arg.button_event)
       -#define LTK_CAST_ARG_SCROLL_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_SCROLL_EVENT), carg.arg.scroll_event)
       -#define LTK_CAST_ARG_MOTION_EVENT(carg) (ltk_assert(carg.type == LTK_TYPE_MOTION_EVENT), carg.arg.motion_event)
       -#define LTK_CAST_ARG_SURFACE(carg) (ltk_assert(carg.type == LTK_TYPE_SURFACE), carg.arg.surface)
       -#define LTK_CAST_ARG_RECT(carg) (ltk_assert(carg.type == LTK_TYPE_RECT), carg.arg.rect)
       -#define LTK_CAST_ARG_VOIDP(carg) (ltk_assert(carg.type == LTK_TYPE_VOIDP), carg.arg.v)
       -
       -#define LTK_GET_ARG_INT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_INT(cargs.args[i]))
       -#define LTK_GET_ARG_SIZE_T(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SIZE_T(cargs.args[i]))
       -#define LTK_GET_ARG_CHAR(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CHAR(cargs.args[i]))
       -#define LTK_GET_ARG_WIDGET(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_WIDGET(cargs.args[i]))
       -#define LTK_GET_ARG_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_STRING(cargs.args[i]))
       -#define LTK_GET_ARG_CONST_STRING(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_CONST_STRING(cargs.args[i]))
       -#define LTK_GET_ARG_KEY_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_KEY_EVENT(cargs.args[i]))
       -#define LTK_GET_ARG_BUTTON_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_BUTTON_EVENT(cargs.args[i]))
       -#define LTK_GET_ARG_SCROLL_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SCROLL_EVENT(cargs.args[i]))
       -#define LTK_GET_ARG_MOTION_EVENT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_MOTION_EVENT(cargs.args[i]))
       -#define LTK_GET_ARG_SURFACE(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_SURFACE(cargs.args[i]))
       -#define LTK_GET_ARG_RECT(cargs, i) (ltk_assert(i >= 0 && i < cargs.num), LTK_CAST_ARG_RECT(cargs.args[i]))
       -
       -#define LTK_CAST_WIDGET(w) (&(w)->widget)
       -#define LTK_CAST_WINDOW(w) (ltk_assert(w->vtable->type == LTK_WIDGET_WINDOW), (ltk_window *)(w))
       -#define LTK_CAST_LABEL(w) (ltk_assert(w->vtable->type == LTK_WIDGET_LABEL), (ltk_label *)(w))
       -#define LTK_CAST_BUTTON(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BUTTON), (ltk_button *)(w))
       -#define LTK_CAST_GRID(w) (ltk_assert(w->vtable->type == LTK_WIDGET_GRID), (ltk_grid *)(w))
       -#define LTK_CAST_IMAGE_WIDGET(w) (ltk_assert(w->vtable->type == LTK_WIDGET_IMAGE), (ltk_image_widget *)(w))
       -#define LTK_CAST_ENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_ENTRY), (ltk_entry *)(w))
       -#define LTK_CAST_MENU(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENU), (ltk_menu *)(w))
       -#define LTK_CAST_MENUENTRY(w) (ltk_assert(w->vtable->type == LTK_WIDGET_MENUENTRY), (ltk_menuentry *)(w))
       -#define LTK_CAST_SCROLLBAR(w) (ltk_assert(w->vtable->type == LTK_WIDGET_SCROLLBAR), (ltk_scrollbar *)(w))
       -#define LTK_CAST_BOX(w) (ltk_assert(w->vtable->type == LTK_WIDGET_BOX), (ltk_box *)(w))
       -
       -#define LTK_WIDGET_SIGNAL_INVALID 0
       -
       -typedef struct {
       -        ltk_callback_arg *args;
       -        size_t num;
       -} ltk_callback_arglist;
       -
       -#define LTK_EMPTY_ARGLIST ((ltk_callback_arglist){NULL, 0})
       -
       -/* FIXME: should signals just return int to make it simpler? */
       -typedef int (*ltk_signal_callback)(ltk_widget *widget, ltk_callback_arglist args, ltk_callback_arg data);
       -typedef struct {
       -        ltk_signal_callback callback;
       -        ltk_callback_arg data;
       -        int type;
       -} ltk_signal_callback_info;
       -
       -int ltk_widget_register_signal_handler(ltk_widget *widget, int type, ltk_signal_callback callback, ltk_callback_arg data);
       -int ltk_widget_emit_signal(ltk_widget *widget, int type, ltk_callback_arglist args);
       -size_t ltk_widget_remove_signal_handler_by_type(ltk_widget *widget, int type);
       -size_t ltk_widget_remove_signal_handler_by_callback(ltk_widget *widget, ltk_signal_callback callback);
       -int ltk_widget_register_type(void);
       -
       -LTK_ARRAY_INIT_STRUCT_DECL(signal, ltk_signal_callback_info)
       -
       -struct ltk_widget {
       -        struct ltk_window *window;
       -        struct ltk_widget *parent;
       -
       -        struct ltk_widget_vtable *vtable;
       -
       -        /* 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;
       -
       -        /* maybe mask to determine quickly which callbacks are included?
       -        default signals only allowed to have one callback? */
       -        ltk_array(signal) *signal_cbs;
       -
       -        ltk_widget_state state;
       -        /* FIXME: store this in grid/box - for row_span, column_span the other cells could be marked with "not top left cell of widget" so they can be skipped */
       -        ltk_sticky_mask sticky;
       -        unsigned short row;
       -        unsigned short column;
       -        unsigned short row_span;
       -        unsigned short column_span;
       -        /* ALSO NEED SIGNALS LIKE ADD-TEXT (called *before* text is inserted to check validity) - these would need argument
       -        FIGURE OUT HOW TO DO KEY MAPPINGS - should reuse parts of builtin mapping handling
       -        -> maybe something like tk
       -        -> or maybe just say everyone needs to override event handler? but makes simple stuff more difficult
       -        -> also need "global" mappings/key event handler for global shortcuts */
       -        /* needed to properly handle handle local coordinates since
       -           popups are positioned globally instead of locally */
       -        char popup;
       -        char dirty;
       -        char hidden;
       -        char vtable_copied;
       -};
       -
       -typedef struct ltk_widget_vtable {
       -        int (*key_press)(struct ltk_widget *, ltk_key_event *);
       -        int (*key_release)(struct ltk_widget *, ltk_key_event *);
       -        /* press/release also receive double/triple-click/release */
       -        int (*mouse_press)(struct ltk_widget *, ltk_button_event *);
       -        int (*mouse_release)(struct ltk_widget *, ltk_button_event *);
       -        int (*mouse_scroll)(struct ltk_widget *, ltk_scroll_event *);
       -        int (*motion_notify)(struct ltk_widget *, ltk_motion_event *);
       -        int (*mouse_leave)(struct ltk_widget *, ltk_motion_event *);
       -        int (*mouse_enter)(struct ltk_widget *, ltk_motion_event *);
       -        int (*press)(struct ltk_widget *);
       -        int (*release)(struct ltk_widget *);
       -        void (*cmd_return)(struct ltk_widget *self, char *text, size_t len);
       -
       -        void (*resize)(struct ltk_widget *);
       -        void (*hide)(struct ltk_widget *);
       -        /* 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_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 *);
       -        int (*remove_child)(struct ltk_widget *, struct ltk_widget *);
       -        /* 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;
       -        int invalid_signal;
       -} ltk_widget_vtable;
       -
       -void ltk_widget_hide(ltk_widget *widget);
       -int ltk_widget_destroy(ltk_widget *widget, int shallow);
       -void ltk_fill_widget_defaults(
       -    ltk_widget *widget, struct ltk_window *window,
       -    struct ltk_widget_vtable *vtable, int w, int h
       -);
       -void ltk_widget_change_state(ltk_widget *widget, ltk_widget_state old_state);
       -void ltk_widget_resize(ltk_widget *widget);
       -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);
       -
       -ltk_widget_vtable *ltk_widget_get_editable_vtable(ltk_widget *widget);
       -
       -#endif /* LTK_WIDGET_H */
   DIR diff --git a/src/window.c b/src/window.c
       t@@ -1,1270 +0,0 @@
       -/*
       - * Copyright (c) 2020-2024 lumidify <nobody@lumidify.org>
       - *
       - * Permission to use, copy, modify, and/or distribute this software for any
       - * purpose with or without fee is hereby granted, provided that the above
       - * copyright notice and this permission notice appear in all copies.
       - *
       - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
       - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
       - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
       - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
       - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
       - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
       - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
       - */
       -
       -#include <stdlib.h>
       -#include <string.h>
       -
       -#include "ltk.h"
       -#include "util.h"
       -#include "keys.h"
       -#include "array.h"
       -#include "theme.h"
       -#include "widget.h"
       -#include "window.h"
       -#include "memory.h"
       -#include "eventdefs.h"
       -
       -#define MAX_WINDOW_FONT_SIZE 200
       -
       -static void gen_widget_stack(ltk_widget *bottom);
       -static ltk_widget *get_hover_popup(ltk_window *window, int x, int y);
       -static int is_parent(ltk_widget *parent, ltk_widget *child);
       -static ltk_widget *get_widget_under_pointer(ltk_widget *widget, int x, int y, int *local_x_ret, int *local_y_ret);
       -
       -static int ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event);
       -static int ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event);
       -static int ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event);
       -static int ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event);
       -static int ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event);
       -static int ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event);
       -static void ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip);
       -
       -/* FIXME: actually use this properly */
       -static struct ltk_widget_vtable vtable = {
       -        .key_press = &ltk_window_key_press_event,
       -        .key_release = &ltk_window_key_release_event,
       -        .mouse_press = &ltk_window_mouse_press_event,
       -        .mouse_release = &ltk_window_mouse_release_event,
       -        .release = NULL,
       -        .motion_notify = &ltk_window_motion_notify_event,
       -        .mouse_leave = NULL,
       -        .mouse_enter = NULL,
       -        .change_state = NULL,
       -        .get_child_at_pos = NULL,
       -        .resize = NULL,
       -        .hide = NULL,
       -        .draw = &ltk_window_redraw,
       -        .destroy = &ltk_window_destroy,
       -        .child_size_change = NULL,
       -        .remove_child = NULL,
       -        .type = LTK_WIDGET_WINDOW,
       -        .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS,
       -        .invalid_signal = LTK_WINDOW_SIGNAL_INVALID,
       -};
       -
       -static int cb_focus_active(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_unfocus_active(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_prev(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_next(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_left(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_right(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_up(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_move_down(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_set_pressed(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_unset_pressed(ltk_window *window, ltk_key_event *event, int handled);
       -static int cb_remove_popups(ltk_window *window, ltk_key_event *event, int handled);
       -
       -struct key_cb {
       -        char *func_name;
       -        int (*callback)(ltk_window *, ltk_key_event *, int handled);
       -};
       -
       -static struct key_cb cb_map[] = {
       -        {"focus-active", &cb_focus_active},
       -        {"move-down", &cb_move_down},
       -        {"move-left", &cb_move_left},
       -        {"move-next", &cb_move_next},
       -        {"move-prev", &cb_move_prev},
       -        {"move-right", &cb_move_right},
       -        {"move-up", &cb_move_up},
       -        {"remove-popups", &cb_remove_popups},
       -        {"set-pressed", &cb_set_pressed},
       -        {"unfocus-active", &cb_unfocus_active},
       -        {"unset-pressed", &cb_unset_pressed},
       -};
       -
       -struct keypress_cfg {
       -        ltk_keypress_binding b;
       -        struct key_cb cb;
       -};
       -
       -struct keyrelease_cfg {
       -        ltk_keyrelease_binding b;
       -        struct key_cb cb;
       -};
       -
       -LTK_ARRAY_INIT_DECL_STATIC(keypress, struct keypress_cfg)
       -LTK_ARRAY_INIT_IMPL_STATIC(keypress, struct keypress_cfg)
       -LTK_ARRAY_INIT_DECL_STATIC(keyrelease, struct keyrelease_cfg)
       -LTK_ARRAY_INIT_IMPL_STATIC(keyrelease, struct keyrelease_cfg)
       -
       -static ltk_array(keypress) *keypresses = NULL;
       -static ltk_array(keyrelease) *keyreleases = NULL;
       -
       -GEN_CB_MAP_HELPERS(cb_map, struct key_cb, func_name)
       -
       -/* 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 ltk_window_theme theme;
       -static ltk_theme_parseinfo theme_parseinfo[] = {
       -        {"bg", THEME_COLOR, {.color = &theme.bg}, {.color = "#000000"}, 0, 0, 0},
       -        {"fg", THEME_COLOR, {.color = &theme.fg}, {.color = "#FFFFFF"}, 0, 0, 0},
       -        {"font", THEME_STRING, {.str = &theme.font}, {.str = "Monospace"}, 0, 0, 0},
       -        {"font-size", THEME_INT, {.i = &theme.font_size}, {.i = 15}, 0, MAX_WINDOW_FONT_SIZE, 0},
       -};
       -static int theme_parseinfo_sorted = 0;
       -
       -int
       -ltk_window_fill_theme_defaults(ltk_renderdata *data) {
       -        return ltk_theme_fill_defaults(data, "window", theme_parseinfo, LENGTH(theme_parseinfo));
       -}
       -
       -int
       -ltk_window_ini_handler(ltk_renderdata *data, const char *prop, const char *value) {
       -        return ltk_theme_handle_value(data, "window", prop, value, theme_parseinfo, LENGTH(theme_parseinfo), &theme_parseinfo_sorted);
       -}
       -
       -void
       -ltk_window_uninitialize_theme(ltk_renderdata *data) {
       -        ltk_theme_uninitialize(data, theme_parseinfo, LENGTH(theme_parseinfo));
       -}
       -
       -/* FIXME: maybe ltk_fatal if ltk not initialized? */
       -ltk_window_theme *
       -ltk_window_get_theme(void) {
       -        return &theme;
       -}
       -
       -/* FIXME: most of this is duplicated code */
       -
       -int
       -ltk_window_register_keypress(const char *func_name, size_t func_len, ltk_keypress_binding b) {
       -        if (!keypresses)
       -                keypresses = ltk_array_create(keypress, 1);
       -        struct key_cb *cb = cb_map_get_entry(func_name, func_len);
       -        if (!cb)
       -                return 1;
       -        struct keypress_cfg cfg = {b, *cb};
       -        ltk_array_append(keypress, keypresses, cfg);
       -        return 0;
       -}
       -
       -int
       -ltk_window_register_keyrelease(const char *func_name, size_t func_len, ltk_keyrelease_binding b) {
       -        if (!keyreleases)
       -                keyreleases = ltk_array_create(keyrelease, 1);
       -        struct key_cb *cb = cb_map_get_entry(func_name, func_len);
       -        if (!cb)
       -                return 1;
       -        struct keyrelease_cfg cfg = {b, *cb};
       -        ltk_array_append(keyrelease, keyreleases, cfg);
       -        return 0;
       -}
       -
       -static void
       -destroy_keypress_cfg(struct keypress_cfg cfg) {
       -        ltk_keypress_binding_destroy(cfg.b);
       -}
       -
       -void
       -ltk_window_cleanup(void) {
       -        ltk_array_destroy_deep(keypress, keypresses, &destroy_keypress_cfg);
       -        ltk_array_destroy(keyrelease, keyreleases);
       -        free(widget_stack);
       -        keypresses = NULL;
       -        keyreleases = NULL;
       -        widget_stack = NULL;
       -}
       -
       -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: 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 */
       -static int
       -ltk_window_key_press_event(ltk_widget *self, ltk_key_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        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_press && widget_stack[i]->vtable->key_press(widget_stack[i], event)) {
       -                                handled = 1;
       -                                break;
       -                        }
       -                }
       -        }
       -        if (!keypresses)
       -                return 1;
       -        ltk_keypress_binding *b = NULL;
       -        for (size_t i = 0; i < ltk_array_len(keypresses); i++) {
       -                b = &ltk_array_get(keypresses, i).b;
       -                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 |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       -                } else if (b->rawtext) {
       -                        if (event->text && !strcmp(b->text, event->text))
       -                                handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       -                } else if (b->sym != LTK_KEY_NONE) {
       -                        if (event->sym == b->sym)
       -                                handled |= ltk_array_get(keypresses, i).cb.callback(window, event, handled);
       -                }
       -        }
       -        return 1;
       -}
       -
       -/* FIXME: need to actually check if any of parent widgets are focused and still pass to them even if bottom widget not focused? */
       -static int
       -ltk_window_key_release_event(ltk_widget *self, ltk_key_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        /* FIXME: emit event */
       -        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;
       -                        }
       -                }
       -        }
       -        if (!keyreleases)
       -                return 1;
       -        ltk_keyrelease_binding *b = NULL;
       -        for (size_t i = 0; i < ltk_array_len(keyreleases); i++) {
       -                b = &ltk_array_get(keyreleases, i).b;
       -                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 |= ltk_array_get(keyreleases, i).cb.callback(window, event, handled);
       -                }
       -        }
       -        return 1;
       -}
       -
       -/* FIXME: This is still weird. */
       -static int
       -ltk_window_mouse_press_event(ltk_widget *self, ltk_button_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        ltk_widget *widget = get_hover_popup(window, event->x, event->y);
       -        int check_hide = 0;
       -        if (!widget) {
       -                widget = window->root_widget;
       -                check_hide = 1;
       -        }
       -        if (!widget) {
       -                ltk_window_unregister_all_popups(window);
       -                return 1;
       -        }
       -        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);
       -        }
       -
       -        /* FIXME: this doesn't make much sense if the popups aren't a
       -           hierarchy (right now, they're just menus, so that's always
       -           a hierarchy */
       -        /* don't hide popups if they are children of the now pressed widget */
       -        if (check_hide && !(window->popups_num > 0 && is_parent(cur_widget, window->popups[0])))
       -                ltk_window_unregister_all_popups(window);
       -
       -        /* FIXME: popups don't always have their children geometrically contained within parents,
       -           so this won't work properly in all cases */
       -        int first = 1;
       -        while (cur_widget) {
       -                int handled = 0;
       -                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 */
       -                        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 && event->type == LTK_BUTTONPRESS_EVENT && (cur_widget->vtable->flags & LTK_ACTIVATABLE_ALWAYS)) {
       -                                ltk_window_set_pressed_widget(window, cur_widget, 0);
       -                                first = 0;
       -                        }
       -                }
       -                if (!handled)
       -                        cur_widget = cur_widget->parent;
       -                else
       -                        break;
       -        }
       -        return 1;
       -}
       -
       -static int
       -ltk_window_mouse_scroll_event(ltk_widget *self, ltk_scroll_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        /* FIXME: should it first be sent to pressed widget? */
       -        ltk_widget *widget = get_hover_popup(window, event->x, event->y);
       -        if (!widget)
       -                widget = window->root_widget;
       -        if (!widget)
       -                return 1;
       -        int orig_x = event->x, orig_y = event->y;
       -        ltk_widget *cur_widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
       -        /* FIXME: same issue with popups like in mouse_press above */
       -        while (cur_widget) {
       -                int handled = 0;
       -                ltk_point local = ltk_global_to_widget_pos(cur_widget, orig_x, orig_y);
       -                event->x = local.x;
       -                event->y = local.y;
       -                if (cur_widget->state != LTK_DISABLED) {
       -                        /* FIXME: see function above
       -                        if (queue_scroll_event(cur_widget, event->x, event->y, event->dx, event->dy))
       -                                handled = 1; */
       -                        if (cur_widget->vtable->mouse_scroll)
       -                                handled = cur_widget->vtable->mouse_scroll(cur_widget, event);
       -                }
       -                if (!handled)
       -                        cur_widget = cur_widget->parent;
       -                else
       -                        break;
       -        }
       -        return 1;
       -}
       -
       -void
       -ltk_window_fake_motion_event(ltk_window *window, int x, int y) {
       -        ltk_motion_event e = {.type = LTK_MOTION_EVENT, .x = x, .y = y};
       -        /* FIXME: call overwritten method */
       -        window->widget.vtable->motion_notify(LTK_CAST_WIDGET(window), &e);
       -}
       -
       -static int
       -ltk_window_mouse_release_event(ltk_widget *self, ltk_button_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        ltk_widget *widget = window->pressed_widget;
       -        int orig_x = event->x, orig_y = event->y;
       -        /* FIXME: why does this only take pressed widget and popups into account? */
       -        if (!widget) {
       -                widget = get_hover_popup(window, event->x, event->y);
       -                widget = get_widget_under_pointer(widget, event->x, event->y, &event->x, &event->y);
       -        }
       -        /* FIXME: loop up to top of hierarchy if not handled */
       -        /* FIXME: see functions above
       -        if (widget && queue_mouse_event(widget, event->type, event->x, event->y)) { */
       -                /* NOP */
       -        if (widget) {
       -                if (widget->vtable->mouse_release)
       -                        widget->vtable->mouse_release(widget, event);
       -        }
       -        if (event->button == LTK_BUTTONL && event->type == LTK_BUTTONRELEASE_EVENT) {
       -                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, orig_x, orig_y);
       -        }
       -        return 1;
       -}
       -
       -static int
       -ltk_window_motion_notify_event(ltk_widget *self, ltk_motion_event *event) {
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        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 1;
       -                }
       -                widget = window->root_widget;
       -        }
       -        if (!widget)
       -                return 1;
       -        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 1;
       -        }
       -        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) {
       -                        /* FIXME: see functions above
       -                        if (queue_mouse_event(cur_widget, LTK_MOTION_EVENT, event->x, event->y))
       -                                handled = 1; */
       -                        if (cur_widget->vtable->motion_notify)
       -                                handled = cur_widget->vtable->motion_notify(cur_widget, event);
       -                        /* set first non-disabled widget to hover widget */
       -                        /* FIXME: should enter/leave event be sent to parent
       -                           when moving from/to widget nested in parent? */
       -                        /* 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;
       -                        }
       -                }
       -                if (!handled)
       -                        cur_widget = cur_widget->parent;
       -                else
       -                        break;
       -        }
       -        if (first) {
       -                event->x = orig_x;
       -                event->y = orig_y;
       -                ltk_window_set_hover_widget(window, NULL, event);
       -        }
       -        return 1;
       -}
       -
       -void
       -ltk_window_set_root_widget(ltk_window *window, ltk_widget *widget) {
       -        window->root_widget = widget;
       -        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);
       -}
       -
       -void
       -ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect) {
       -        if (window->dirty_rect.w == 0 && window->dirty_rect.h == 0)
       -                window->dirty_rect = rect;
       -        else
       -                window->dirty_rect = ltk_rect_union(rect, window->dirty_rect);
       -}
       -
       -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});
       -}
       -
       -static void
       -ltk_window_redraw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
       -        (void)draw_surf;
       -        (void)x;
       -        (void)y;
       -        (void)clip;
       -        ltk_window *window = LTK_CAST_WINDOW(self);
       -        ltk_widget *ptr;
       -        if (!window) return;
       -        if (window->dirty_rect.x >= window->rect.w) return;
       -        if (window->dirty_rect.y >= window->rect.h) return;
       -        if (window->dirty_rect.x + window->dirty_rect.w > window->rect.w)
       -                window->dirty_rect.w -= window->dirty_rect.x + window->dirty_rect.w - window->rect.w;
       -        if (window->dirty_rect.y + window->dirty_rect.h > window->rect.h)
       -                window->dirty_rect.h -= window->dirty_rect.y + window->dirty_rect.h - window->rect.h;
       -        /* FIXME: this should use window->dirty_rect, but that doesn't work
       -           properly with double buffering */
       -        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->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->surface, ptr->lrect.x, ptr->lrect.y, ltk_rect_relative(ptr->lrect, window->rect));
       -        }
       -        renderer_swap_buffers(window->renderwindow);
       -        window->dirty_rect.w = 0;
       -        window->dirty_rect.h = 0;
       -}
       -
       -static void
       -ltk_window_other_event(ltk_window *window, ltk_event *event) {
       -        ltk_widget *ptr = window->root_widget;
       -        /* FIXME: decide whether this should be moved to separate resize function in window vtable */
       -        if (event->type == LTK_CONFIGURE_EVENT) {
       -                ltk_window_unregister_all_popups(window);
       -                int w, h;
       -                w = event->configure.w;
       -                h = event->configure.h;
       -                int orig_w = window->rect.w;
       -                int orig_h = window->rect.h;
       -                if (orig_w != w || orig_h != h) {
       -                        window->rect.w = w;
       -                        window->rect.h = h;
       -                        ltk_window_invalidate_rect(window, window->rect);
       -                        ltk_surface_update_size(window->surface, w, h);
       -                        if (ptr) {
       -                                ptr->lrect.w = w;
       -                                ptr->lrect.h = h;
       -                                ptr->crect = ptr->lrect;
       -                                ltk_widget_resize(ptr);
       -                        }
       -                }
       -        } else if (event->type == LTK_EXPOSE_EVENT) {
       -                ltk_rect r;
       -                r.x = event->expose.x;
       -                r.y = event->expose.y;
       -                r.w = event->expose.w;
       -                r.h = event->expose.h;
       -                ltk_window_invalidate_rect(window, r);
       -        } else if (event->type == LTK_WINDOWCLOSE_EVENT) {
       -                ltk_widget_emit_signal(LTK_CAST_WIDGET(window), LTK_WINDOW_SIGNAL_CLOSE, LTK_EMPTY_ARGLIST);
       -        }
       -}
       -
       -/* FIXME: check for duplicates? */
       -void
       -ltk_window_register_popup(ltk_window *window, ltk_widget *popup) {
       -        if (window->popups_num == window->popups_alloc) {
       -                window->popups_alloc = ideal_array_size(
       -                    window->popups_alloc, window->popups_num + 1
       -                );
       -                window->popups = ltk_reallocarray(
       -                    window->popups, window->popups_alloc, sizeof(ltk_widget *)
       -                );
       -        }
       -        window->popups[window->popups_num++] = popup;
       -        popup->popup = 1;
       -}
       -
       -void
       -ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup) {
       -        if (window->popups_locked)
       -                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,
       -                            sizeof(ltk_widget *) * (window->popups_num - i - 1)
       -                        );
       -                        window->popups_num--;
       -                        size_t sz = ideal_array_size(
       -                            window->popups_alloc, window->popups_num
       -                        );
       -                        if (sz != window->popups_alloc) {
       -                                window->popups_alloc = sz;
       -                                window->popups = ltk_reallocarray(
       -                                    window->popups, sz, sizeof(ltk_widget *)
       -                                );
       -                        }
       -                        return;
       -                }
       -        }
       -}
       -
       -/* FIXME: where should actual hiding happen? */
       -void
       -ltk_window_unregister_all_popups(ltk_window *window) {
       -        window->popups_locked = 1;
       -        for (size_t i = 0; i < window->popups_num; i++) {
       -                window->popups[i]->hidden = 1;
       -                window->popups[i]->popup = 0;
       -                ltk_widget_hide(window->popups[i]);
       -        }
       -        window->popups_num = 0;
       -        /* somewhat arbitrary, but should be enough for most cases */
       -        if (window->popups_num > 4) {
       -                window->popups = ltk_reallocarray(
       -                    window->popups, 4, sizeof(ltk_widget *)
       -                );
       -                window->popups_alloc = 4;
       -        }
       -        window->popups_locked = 0;
       -        /* I guess just invalidate everything instead of being smart */
       -        ltk_window_invalidate_rect(window, window->rect);
       -}
       -
       -/* FIXME: support more options like child windows */
       -ltk_window *
       -ltk_window_create_intern(ltk_renderdata *data, const char *title, int x, int y, unsigned int w, unsigned int h) {
       -        ltk_window *window = ltk_malloc(sizeof(ltk_window));
       -        /* this is a bit weird because the window entry points to itself */
       -        /* the ideal width isn't needed for a window */
       -        ltk_fill_widget_defaults(&window->widget, window, &vtable, 0, 0);
       -
       -        window->popups = NULL;
       -        window->popups_num = window->popups_alloc = 0;
       -        window->popups_locked = 0;
       -
       -        window->renderwindow = renderer_create_window(data, title, x, y, w, h);
       -        renderer_set_window_properties(window->renderwindow, theme.bg);
       -        window->theme = &theme;
       -
       -        window->root_widget = NULL;
       -        window->hover_widget = NULL;
       -        window->active_widget = NULL;
       -        window->pressed_widget = NULL;
       -
       -        //FIXME: use widget rect
       -        window->rect.w = w;
       -        window->rect.h = h;
       -        window->rect.x = 0;
       -        window->rect.y = 0;
       -        window->dirty_rect.w = 0;
       -        window->dirty_rect.h = 0;
       -        window->dirty_rect.x = 0;
       -        window->dirty_rect.y = 0;
       -
       -        window->surface_cache = ltk_surface_cache_create(window->renderwindow);
       -        window->surface = ltk_surface_from_window(window->renderwindow, w, h);
       -
       -        return window;
       -}
       -
       -/* FIXME: check if widget window matches in all public functions */
       -
       -void
       -ltk_window_destroy_intern(ltk_window *window) {
       -        if (window->root_widget) {
       -                ltk_widget_destroy(window->root_widget, 0);
       -        }
       -        if (window->popups)
       -                ltk_free(window->popups);
       -        ltk_surface_cache_destroy(window->surface_cache);
       -        ltk_surface_destroy(window->surface);
       -        renderer_destroy_window(window->renderwindow);
       -        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);
       -                ltk_point local = ltk_global_to_widget_pos(old, event->x, event->y);
       -                event->x = local.x;
       -                event->y = local.y;
       -                if (old->vtable->mouse_leave)
       -                        old->vtable->mouse_leave(old, event);
       -                event->x = orig_x;
       -                event->y = orig_y;
       -        }
       -        window->hover_widget = 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->mouse_enter)
       -                        widget->vtable->mouse_enter(widget, event);
       -                ltk_widget_state old_state = widget->state;
       -                widget->state |= LTK_HOVER;
       -                ltk_widget_change_state(widget, old_state);
       -                if ((widget->vtable->flags & LTK_HOVER_IS_ACTIVE) && widget != window->active_widget)
       -                        ltk_window_set_active_widget(window, widget);
       -        }
       -}
       -
       -void
       -ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget) {
       -        if (window->active_widget == widget) {
       -                return;
       -        }
       -        ltk_widget *old = window->active_widget;
       -        /* Note: this has to be set at the beginning to
       -           avoid infinite recursion in some cases */
       -        window->active_widget = widget;
       -        ltk_widget *common_parent = NULL;
       -        if (widget) {
       -                ltk_widget *cur = widget;
       -                while (cur) {
       -                        if (cur->state & LTK_ACTIVE) {
       -                                common_parent = cur;
       -                                break;
       -                        }
       -                        ltk_widget_state old_state = cur->state;
       -                        cur->state |= LTK_ACTIVE;
       -                        /* 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;
       -                }
       -        }
       -        /* FIXME: better variable names; generally make this nicer */
       -        /* special case if old is parent of new active widget */
       -        ltk_widget *tmp = common_parent;
       -        while (tmp) {
       -                if (tmp == old)
       -                        return;
       -                tmp = tmp->parent;
       -        }
       -        if (old) {
       -                old->state &= ~LTK_FOCUSED;
       -                ltk_widget *cur = old;
       -                while (cur) {
       -                        if (cur == common_parent)
       -                                break;
       -                        ltk_widget_state old_state = cur->state;
       -                        cur->state &= ~LTK_ACTIVE;
       -                        ltk_widget_change_state(cur, old_state);
       -                        cur = cur->parent;
       -                }
       -        }
       -}
       -
       -void
       -ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget, int release) {
       -        if (window->pressed_widget == widget)
       -                return;
       -        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 && (old_state & LTK_PRESSED)) {
       -                        if (window->pressed_widget->vtable->release)
       -                                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);
       -        }
       -}
       -
       -void
       -ltk_window_handle_event(ltk_window *window, ltk_event *event) {
       -        switch (event->type) {
       -        case LTK_KEYPRESS_EVENT:
       -                ltk_window_key_press_event(LTK_CAST_WIDGET(window), &event->key);
       -                break;
       -        case LTK_KEYRELEASE_EVENT:
       -                ltk_window_key_release_event(LTK_CAST_WIDGET(window), &event->key);
       -                break;
       -        case LTK_BUTTONPRESS_EVENT:
       -        case LTK_2BUTTONPRESS_EVENT:
       -        case LTK_3BUTTONPRESS_EVENT:
       -                ltk_window_mouse_press_event(LTK_CAST_WIDGET(window), &event->button);
       -                break;
       -        case LTK_SCROLL_EVENT:
       -                ltk_window_mouse_scroll_event(LTK_CAST_WIDGET(window), &event->scroll);
       -                break;
       -        case LTK_BUTTONRELEASE_EVENT:
       -        case LTK_2BUTTONRELEASE_EVENT:
       -        case LTK_3BUTTONRELEASE_EVENT:
       -                ltk_window_mouse_release_event(LTK_CAST_WIDGET(window), &event->button);
       -                break;
       -        case LTK_MOTION_EVENT:
       -                ltk_window_motion_notify_event(LTK_CAST_WIDGET(window), &event->motion);
       -                break;
       -        default:
       -                ltk_window_other_event(window, event);
       -        }
       -}
       -
       -/* x and y are global! */
       -static ltk_widget *
       -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, *local_x_ret, *local_y_ret);
       -                if (!next) {
       -                        break;
       -                } 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;
       -}
       -
       -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]->crect, x, y))
       -                        return window->popups[i];
       -        }
       -        return NULL;
       -}
       -
       -static int
       -is_parent(ltk_widget *parent, ltk_widget *child) {
       -        while (child && child != parent) {
       -                child = child->parent;
       -        }
       -        return child != NULL;
       -}
       -
       -/* 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);
       -        ltk_rect rect = {local.x, local.y, r.w, r.h};
       -        if (widget->vtable->nearest_child)
       -                return widget->vtable->nearest_child(widget, rect);
       -        return NULL;
       -}
       -
       -/* 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};
       -        ltk_widget *last_activatable = NULL;
       -        if (!cur) {
       -                cur = window->root_widget;
       -                if (cur->vtable->flags & act_flags)
       -                        last_activatable = cur;
       -                ltk_rect r = {cur->lrect.w, cur->lrect.h, 0, 0};
       -                while ((new = nearest_child(cur, r))) {
       -                        cur = new;
       -                        if (cur->vtable->flags & act_flags)
       -                                last_activatable = cur;
       -                }
       -        }
       -        if (last_activatable) {
       -                cur = last_activatable;
       -        } else 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};
       -                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 ((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;
       -                                }
       -                        }
       -                }
       -        }
       -        /* 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};
       -        int found_activatable = 0;
       -        if (!cur) {
       -                cur = window->root_widget;
       -                if (!(cur->vtable->flags & act_flags)) {
       -                        while ((new = nearest_child(cur, (ltk_rect){0, 0, 0, 0}))) {
       -                                cur = new;
       -                                if (cur->vtable->flags & act_flags) {
       -                                        found_activatable = 1;
       -                                        break;
       -                                }
       -                        }
       -                }
       -        }
       -        if (!found_activatable) {
       -                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 ((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 ((new = nearest_child(cur, old_rect))) {
       -                                                cur = new;
       -                                                if (cur->vtable->flags & act_flags) {
       -                                                        changed = 1;
       -                                                        break;
       -                                                }
       -                                        }
       -                                        if (changed)
       -                                                break;
       -                                } else {
       -                                        cur = cur->parent;
       -                                }
       -                        }
       -                }
       -        }
       -        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;
       -}