URI: 
       tAdd menus - ltk - Socket-based GUI for X11 (WIP)
  HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
  HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
   DIR Log
   DIR Files
   DIR Refs
   DIR README
   DIR LICENSE
       ---
   DIR commit d3b49ae1320664eeb8629e6c50be99642dc7f25e
   DIR parent 33cf30d1cfde0826ff45ce7b876ca2c1d8dbc9b9
  HTML Author: lumidify <nobody@lumidify.org>
       Date:   Sun, 22 May 2022 17:12:52 +0200
       
       Add menus
       
       Yes, I know a lot of other things were also changed.
       
       Diffstat:
         M Makefile                            |      42 ++++++++++++++++---------------
         M README.md                           |       8 +++++---
         M src/box.c                           |      19 +++++++++++++------
         M src/button.c                        |      85 ++++++++++++++++---------------
         M src/button.h                        |       2 +-
         M src/color.c                         |      23 ++++++++++++++---------
         M src/color.h                         |       3 ++-
         M src/compat.h                        |       2 +-
         M src/draw.c                          |      22 +++++++++++-----------
         M src/graphics.h                      |      17 +++++++++++++++++
         M src/graphics_xlib.c                 |      42 ++++++++++++++++++++++++-------
         M src/grid.c                          |      44 ++++++++++++++++++++-----------
         M src/label.c                         |      34 ++++++++++++++++++++-----------
         M src/ltk.h                           |      17 ++++++++++++++++-
         M src/ltkd.c                          |     255 ++++++++++++++++++++++++++++---
         A src/macros.h                        |      25 +++++++++++++++++++++++++
         M src/memory.c                        |       3 +++
         A src/menu.c                          |    1772 +++++++++++++++++++++++++++++++
         A src/menu.h                          |      64 +++++++++++++++++++++++++++++++
         M src/scrollbar.c                     |      63 +++++++++++++++++--------------
         M src/strtonum.c                      |      16 ++++++----------
         M src/text.h                          |       5 +++++
         M src/text_pango.c                    |       9 ++++++---
         M src/text_stb.c                      |      51 ++++++++++++++++++++++---------
         M src/util.h                          |       9 ++++-----
         M src/widget.c                        |      73 +++++++++++++++++++++++--------
         M src/widget.h                        |      10 +++++++---
         M test.sh                             |       2 +-
         A test2.gui                           |      25 +++++++++++++++++++++++++
         A test2.sh                            |      10 ++++++++++
       
       30 files changed, 2518 insertions(+), 234 deletions(-)
       ---
   DIR diff --git a/Makefile b/Makefile
       t@@ -4,34 +4,36 @@
        NAME = ltk
        VERSION = -999-prealpha0
        
       +# Note: The stb backend should not be used with untrusted font files.
        # FIXME: Using DEBUG here doesn't work because it somehow
        # interferes with a predefined macro, at least on OpenBSD.
       -DEV = 1
       +DEV = 0
        USE_PANGO = 0
        
       -# FIXME: When using _POSIX_C_SOURCE on OpenBSD, strtonum isn't defined anymore -
       -# should strtonum just only be used from the local copy?
       -
       -CFLAGS += -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -Wall -Wextra -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
       -LDFLAGS += -lm `pkg-config --libs x11 fontconfig xext`
       -
        # Note: this macro magic for debugging and pango rendering seems ugly; it should probably be changed
        
        # debug
       -DEV_1 = -g -Wall -Wextra -pedantic
       -#-Werror
       +DEV_CFLAGS_1 = -fsanitize=address -g -Wall -Wextra -pedantic
       +DEV_LDFLAGS_1 = -fsanitize=address
       +# don't include default flags when debugging so possible
       +# optimization flags don't interfere with it
       +DEV_CFLAGS_0 = $(CFLAGS)
       +DEV_LDFLAGS_0 = $(LDFLAGS)
        
        # stb rendering
        EXTRA_OBJ_0 = src/stb_truetype.o src/text_stb.o
        
        # pango rendering
        EXTRA_OBJ_1 = src/text_pango.o
       -EXTRA_CFLAGS_1 += -DUSE_PANGO `pkg-config --cflags pangoxft`
       -EXTRA_LDFLAGS_1 += `pkg-config --libs pangoxft`
       +EXTRA_CFLAGS_1 = `pkg-config --cflags pangoxft`
       +EXTRA_LDFLAGS_1 = `pkg-config --libs pangoxft`
        
        EXTRA_OBJ = $(EXTRA_OBJ_$(USE_PANGO))
       -EXTRA_CFLAGS = $(EXTRA_CFLAGS_$(USE_PANGO)) $(DEV_$(DEV))
       -EXTRA_LDFLAGS = $(EXTRA_LDFLAGS_$(USE_PANGO))
       +EXTRA_CFLAGS = $(DEV_CFLAGS_$(DEV)) $(EXTRA_CFLAGS_$(USE_PANGO))
       +EXTRA_LDFLAGS = $(DEV_LDFLAGS_$(DEV)) $(EXTRA_LDFLAGS_$(USE_PANGO))
       +
       +LTK_CFLAGS = $(EXTRA_CFLAGS) -DUSE_PANGO=$(USE_PANGO) -DDEV=$(DEV) -std=c99 `pkg-config --cflags x11 fontconfig xext` -D_POSIX_C_SOURCE=200809L
       +LTK_LDFLAGS = $(EXTRA_LDFLAGS) -lm `pkg-config --libs x11 fontconfig xext`
        
        OBJ = \
                src/strtonum.o \
       t@@ -47,6 +49,7 @@ OBJ = \
                src/scrollbar.o \
                src/button.o \
                src/label.o \
       +        src/menu.o \
                src/graphics_xlib.o \
                src/surface_cache.o \
                $(EXTRA_OBJ)
       t@@ -71,25 +74,24 @@ HDR = \
                src/stb_truetype.h \
                src/text.h \
                src/util.h \
       +        src/menu.h \
                src/graphics.h \
       -        src/surface_cache.h
       +        src/surface_cache.h \
       +        src/macros.h
        #        src/draw.h \
        
       -CFLAGS += $(EXTRA_CFLAGS)
       -LDFLAGS += $(EXTRA_LDFLAGS)
       -
        all: src/ltkd src/ltkc
        
        src/ltkd: $(OBJ)
       -        $(CC) -o $@ $(OBJ) $(LDFLAGS)
       +        $(CC) -o $@ $(OBJ) $(LTK_LDFLAGS)
        
        src/ltkc: src/ltkc.o src/util.o src/memory.o
       -        $(CC) -o $@ src/ltkc.o src/util.o src/memory.o
       +        $(CC) -o $@ src/ltkc.o src/util.o src/memory.o $(LTK_LDFLAGS)
        
        $(OBJ) : $(HDR)
        
        .c.o:
       -        $(CC) -c -o $@ $< $(CFLAGS)
       +        $(CC) -c -o $@ $< $(LTK_CFLAGS)
        
        .PHONY: clean
        
   DIR diff --git a/README.md b/README.md
       t@@ -5,8 +5,7 @@ WILDEST FANTASIES, NOT ACTUAL WORKING CODE.
        
        To build with or without pango: Follow instructions in config.mk.
        
       -Note: The basic (non-pango) text doesn't work properly on my i386 machine
       -because it's a bit of a hack.
       +Note: The basic (non-pango) text doesn't work properly on all systems.
        
        To test:
        
       t@@ -16,5 +15,8 @@ make
        If you click the top button, it should exit. That's all it does now.
        Also read the comment in './test.sh'.
        
       -New:
        ./testbox.sh shows my gopherhole, but most buttons don't actually do anything.
       +./test2.sh shows an example with menus.
       +
       +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/src/box.c b/src/box.c
       t@@ -117,15 +117,21 @@ static void
        ltk_box_destroy(ltk_widget *self, int shallow) {
                ltk_box *box = (ltk_box *)self;
                ltk_widget *ptr;
       -        if (!shallow) {
       -                for (size_t i = 0; i < box->num_widgets; i++) {
       -                        ptr = box->widgets[i];
       +        char *errstr;
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
       +        for (size_t i = 0; i < box->num_widgets; i++) {
       +                ptr = box->widgets[i];
       +                ptr->parent = NULL;
       +                if (!shallow)
                                ptr->vtable->destroy(ptr, shallow);
       -                }
                }
                ltk_free(box->widgets);
       -        ltk_remove_widget(box->widget.id);
       -        ltk_free(box->widget.id);
       +        ltk_remove_widget(self->id);
       +        ltk_free(self->id);
                box->sc->widget.vtable->destroy((ltk_widget *)box->sc, 0);
                ltk_free(box);
        }
       t@@ -341,6 +347,7 @@ ltk_box_mouse_press(ltk_widget *self, XEvent event) {
                                                default_handler = widget->vtable->mouse_press(widget, event);
                                }
                        }
       +                /* FIXME: configure scrollstep */
                        if (default_handler) {
                                int delta = event.xbutton.button == 4 ? -15 : 15;
                                ltk_scrollbar_scroll((ltk_widget *)box->sc, delta, 0);
   DIR diff --git a/src/button.c b/src/button.c
       t@@ -34,6 +34,9 @@
        #include "graphics.h"
        #include "surface_cache.h"
        
       +#define MAX_BUTTON_BORDER_WIDTH 100
       +#define MAX_BUTTON_PADDING 500
       +
        static void ltk_button_draw(ltk_widget *self, ltk_rect clip);
        static int ltk_button_mouse_release(ltk_widget *self, XEvent event);
        static ltk_button *ltk_button_create(ltk_window *window,
       t@@ -76,61 +79,57 @@ void
        ltk_button_setup_theme_defaults(ltk_window *window) {
                theme.border_width = 2;
                theme.pad = 5;
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#FFFFFF", &theme.text_color);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#339999", &theme.border);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#113355", &theme.fill);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#FFFFFF", &theme.border_pressed);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#113355", &theme.fill_pressed);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#FFFFFF", &theme.border_active);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#738194", &theme.fill_active);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#FFFFFF", &theme.border_disabled);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#292929", &theme.fill_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.text_color);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#339999", &theme.border);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fill);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fill_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &theme.fill_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.border_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &theme.fill_disabled);
        }
        
        void
        ltk_button_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        const char *errstr;
                if (strcmp(prop, "border_width") == 0) {
       -                theme.border_width = atoi(value);
       +                theme.border_width = ltk_strtonum(value, 0, MAX_BUTTON_BORDER_WIDTH, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid button border width '%s': %s.\n", value, errstr);
                } else if (strcmp(prop, "pad") == 0) {
       -                theme.pad = atoi(value);
       +                theme.pad = ltk_strtonum(value, 0, MAX_BUTTON_PADDING, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid button padding '%s': %s.\n", value, errstr);
                } else if (strcmp(prop, "border") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.border);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border))
       +                        ltk_warn("Error setting button border color to '%s'.\n", value);
                } else if (strcmp(prop, "fill") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fill);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill))
       +                        ltk_warn("Error setting button fill color to '%s'.\n", value);
                } else if (strcmp(prop, "border_pressed") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.border_pressed);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_pressed))
       +                        ltk_warn("Error setting button pressed border color to '%s'.\n", value);
                } else if (strcmp(prop, "fill_pressed") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fill_pressed);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_pressed))
       +                        ltk_warn("Error setting button pressed fill color to '%s'.\n", value);
                } else if (strcmp(prop, "border_active") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.border_active);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_active))
       +                        ltk_warn("Error setting button active border color to '%s'.\n", value);
                } else if (strcmp(prop, "fill_active") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fill_active);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_active))
       +                        ltk_warn("Error setting button active fill color to '%s'.\n", value);
                } else if (strcmp(prop, "border_disabled") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.border_disabled);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.border_disabled))
       +                        ltk_warn("Error setting button disabled border color to '%s'.\n", value);
                } else if (strcmp(prop, "fill_disabled") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fill_disabled);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fill_disabled))
       +                        ltk_warn("Error setting button disabled fill color to '%s'.\n", value);
                } else if (strcmp(prop, "text_color") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.text_color);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.text_color))
       +                        ltk_warn("Error setting button text color to '%s'.\n", value);
                } else {
       -                ltk_warn("Unknown property \"%s\" for button style.\n", prop);
       +                ltk_warn("Unknown property '%s' for button style.\n", prop);
                }
        }
        
       t@@ -192,7 +191,6 @@ ltk_button_change_state(ltk_widget *self) {
        /* FIXME: only when pressed button was actually this one */
        static int
        ltk_button_mouse_release(ltk_widget *self, XEvent event) {
       -        (void)event;
                ltk_button *button = (ltk_button *)self;
                if (event.xbutton.button == 1) {
                        ltk_queue_event(button->widget.window, LTK_EVENT_BUTTON, button->widget.id, "button_click");
       t@@ -220,6 +218,12 @@ ltk_button_create(ltk_window *window, const char *id, char *text) {
        static void
        ltk_button_destroy(ltk_widget *self, int shallow) {
                (void)shallow;
       +        char *errstr;
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
                ltk_button *button = (ltk_button *)self;
                if (!button) {
                        ltk_warn("Tried to destroy NULL button.\n");
       t@@ -228,6 +232,7 @@ ltk_button_destroy(ltk_widget *self, int shallow) {
                /* FIXME: this should be generic part of widget */
                ltk_surface_cache_release_key(self->surface_key);
                ltk_text_line_destroy(button->tl);
       +        ltk_remove_widget(self->id);
                ltk_remove_widget(button->widget.id);
                ltk_free(button->widget.id);
                ltk_free(button);
   DIR diff --git a/src/button.h b/src/button.h
       t@@ -1,5 +1,5 @@
        /*
       - * Copyright (c) 2016, 2017, 2018, 2020 lumidify <nobody@lumidify.org>
       + * Copyright (c) 2016, 2017, 2018, 2020, 2022 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
   DIR diff --git a/src/color.c b/src/color.c
       t@@ -20,18 +20,23 @@
        
        #include "util.h"
        #include "color.h"
       +#include "compat.h"
        
       -void
       +/* FIXME: avoid initializing part of the struct and then error returning */
       +/* FIXME: better error codes */
       +/* FIXME: I think xcolor is unneeded when xft is enabled */
       +int
        ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col) {
       -        if (!XParseColor(dpy, cm, hex, &col->xcolor)) {
       -                /* FIXME: better error reporting!!! */
       -                ltk_fatal("ltk_color_create");
       -        }
       -        XAllocColor(dpy, cm, &col->xcolor);
       -        /* FIXME: replace with XftColorAllocValue; error checking */
       -        #if USE_PANGO == 1
       -        XftColorAllocName(dpy, DefaultVisual(dpy, screen), cm, hex, &col->xftcolor);
       +        if (!XParseColor(dpy, cm, hex, &col->xcolor))
       +                return 1;
       +        if (!XAllocColor(dpy, cm, &col->xcolor))
       +                return 1;
       +        /* FIXME: replace with XftColorAllocValue */
       +        #if USE_XFT == 1
       +        if (!XftColorAllocName(dpy, DefaultVisual(dpy, screen), cm, hex, &col->xftcolor))
       +                return 1;
                #else
                (void)screen;
                #endif
       +        return 0;
        }
   DIR diff --git a/src/color.h b/src/color.h
       t@@ -30,6 +30,7 @@ typedef struct {
                #endif
        } ltk_color;
        
       -void ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col);
       +/* returns 1 on failure, 0 on success */
       +int ltk_color_create(Display *dpy, int screen, Colormap cm, const char *hex, ltk_color *col);
        
        #endif /* _LTK_COLOR_H_ */
   DIR diff --git a/src/compat.h b/src/compat.h
       t@@ -1,4 +1,4 @@
       -#ifdef _LTK_COMPAT_H_
       +#ifndef _LTK_COMPAT_H_
        #define _LTK_COMPAT_H_
        
        #if USE_PANGO == 1
   DIR diff --git a/src/draw.c b/src/draw.c
       t@@ -254,22 +254,22 @@ ltk_draw_cmd_line(
                }
                draw = (ltk_draw *)ltk_get_widget(tokens[1], LTK_DRAW, errstr);
                if (!draw) return 1;
       -        x1 = strtonum(tokens[3], 0, 100000, &errstr_num);
       +        x1 = ltk_strtonum(tokens[3], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid x1.\n";
                        return 1;
                }
       -        y1 = strtonum(tokens[4], 0, 100000, &errstr_num);
       +        y1 = ltk_strtonum(tokens[4], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid y1.\n";
                        return 1;
                }
       -        x2 = strtonum(tokens[5], 0, 100000, &errstr_num);
       +        x2 = ltk_strtonum(tokens[5], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid x2.\n";
                        return 1;
                }
       -        y2 = strtonum(tokens[6], 0, 100000, &errstr_num);
       +        y2 = ltk_strtonum(tokens[6], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid y2.\n";
                        return 1;
       t@@ -294,27 +294,27 @@ ltk_draw_cmd_rect(
                }
                draw = (ltk_draw *)ltk_get_widget(tokens[1], LTK_DRAW, errstr);
                if (!draw) return 1;
       -        x = strtonum(tokens[3], 0, 100000, &errstr_num);
       +        x = ltk_strtonum(tokens[3], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid x.\n";
                        return 1;
                }
       -        y = strtonum(tokens[4], 0, 100000, &errstr_num);
       +        y = ltk_strtonum(tokens[4], 0, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid y.\n";
                        return 1;
                }
       -        w = strtonum(tokens[5], 1, 100000, &errstr_num);
       +        w = ltk_strtonum(tokens[5], 1, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid width.\n";
                        return 1;
                }
       -        h = strtonum(tokens[6], 1, 100000, &errstr_num);
       +        h = ltk_strtonum(tokens[6], 1, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid height.\n";
                        return 1;
                }
       -        fill = strtonum(tokens[7], 0, 1, &errstr_num);
       +        fill = ltk_strtonum(tokens[7], 0, 1, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid fill bool.\n";
                        return 1;
       t@@ -342,12 +342,12 @@ ltk_draw_cmd_create(
                        *errstr = "Widget ID already taken.\n";
                        return 1;
                }
       -        w = strtonum(tokens[3], 1, 100000, &errstr_num);
       +        w = ltk_strtonum(tokens[3], 1, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid width.\n";
                        return 1;
                }
       -        h = strtonum(tokens[4], 1, 100000, &errstr_num);
       +        h = ltk_strtonum(tokens[4], 1, 100000, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid height.\n";
                        return 1;
   DIR diff --git a/src/graphics.h b/src/graphics.h
       t@@ -28,6 +28,20 @@
        #include "ltk.h"
        #include "compat.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;
       +
       +/* FIXME: X only supports 16-bit numbers */
       +typedef struct {
       +        int x, y;
       +} ltk_point;
       +
        /* typedef struct ltk_surface ltk_surface; */
        
        /* FIXME: graphics context */
       t@@ -42,6 +56,9 @@ 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, especiall difference to draw_rect with offsets and line_width */
       +void ltk_surface_draw_border(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_width, ltk_border_sides border_sides);
       +void ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints);
        
        /* TODO */
        /*
   DIR diff --git a/src/graphics_xlib.c b/src/graphics_xlib.c
       t@@ -107,22 +107,46 @@ ltk_surface_draw_rect(ltk_surface *s, ltk_color *c, ltk_rect rect, int line_widt
        }
        
        void
       -ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
       +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->dpy, s->window->gc, c->xcolor.pixel);
       -        XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
       +        if (border_sides & LTK_BORDER_TOP)
       +                XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, line_width);
       +        if (border_sides & LTK_BORDER_BOTTOM)
       +                XFillRectangle(s->window->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->dpy, s->d, s->window->gc, rect.x, rect.y, line_width, rect.h);
       +        if (border_sides & LTK_BORDER_RIGHT)
       +                XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x + rect.w - line_width, rect.y, line_width, rect.h);
        }
        
        void
       -ltk_window_draw_rect(ltk_window *window, ltk_color *c, ltk_rect rect, int line_width) {
       -        XSetForeground(window->dpy, window->gc, c->xcolor.pixel);
       -        XSetLineAttributes(window->dpy, window->gc, line_width, LineSolid, CapButt, JoinMiter);
       -        XDrawRectangle(window->dpy, window->drawable, window->gc, rect.x, rect.y, rect.w, rect.h);
       +ltk_surface_fill_rect(ltk_surface *s, ltk_color *c, ltk_rect rect) {
       +        XSetForeground(s->window->dpy, s->window->gc, c->xcolor.pixel);
       +        XFillRectangle(s->window->dpy, s->d, s->window->gc, rect.x, rect.y, rect.w, rect.h);
        }
        
        void
       -ltk_window_fill_rect(ltk_window *window, ltk_color *c, ltk_rect rect) {
       -        XSetForeground(window->dpy, window->gc, c->xcolor.pixel);
       -        XFillRectangle(window->dpy, window->drawable, window->gc, rect.x, rect.y, rect.w, rect.h);
       +ltk_surface_fill_polygon(ltk_surface *s, ltk_color *c, ltk_point *points, size_t npoints) {
       +        /* FIXME: maybe make this statis since this won't be threaded anyways? */
       +        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->dpy, s->window->gc, c->xcolor.pixel);
       +        XFillPolygon(s->window->dpy, s->d, s->window->gc, final_points, (int)npoints, Complex, CoordModeOrigin);
       +        if (npoints > 6)
       +                free(final_points);
        }
        
        void
   DIR diff --git a/src/grid.c b/src/grid.c
       t@@ -1,3 +1,4 @@
       +/* FIXME: sometimes, resizing doesn't work properly when running test.sh */
        /*
         * Copyright (c) 2016, 2017, 2018, 2020, 2021, 2022 lumidify <nobody@lumidify.org>
         *
       t@@ -160,11 +161,18 @@ ltk_grid_create(ltk_window *window, const char *id, int rows, int columns) {
        static void
        ltk_grid_destroy(ltk_widget *self, int shallow) {
                ltk_grid *grid = (ltk_grid *)self;
       +        char *errstr; /* FIXME: unused */
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
                ltk_widget *ptr;
       -        if (!shallow) {
       -                for (int i = 0; i < grid->rows * grid->columns; i++) {
       -                        if (grid->widget_grid[i]) {
       -                                ptr = grid->widget_grid[i];
       +        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++) {
       t@@ -183,8 +191,8 @@ ltk_grid_destroy(ltk_widget *self, int shallow) {
                ltk_free(grid->column_weights);
                ltk_free(grid->row_pos);
                ltk_free(grid->column_pos);
       -        ltk_remove_widget(grid->widget.id);
       -        ltk_free(grid->widget.id);
       +        ltk_remove_widget(self->id);
       +        ltk_free(self->id);
                ltk_free(grid);
        }
        
       t@@ -300,6 +308,10 @@ ltk_grid_child_size_change(ltk_widget *self, ltk_widget *widget) {
        static int
        ltk_grid_add(ltk_window *window, ltk_widget *widget, ltk_grid *grid,
            int row, int column, int row_span, int column_span, unsigned short sticky, char **errstr) {
       +        if (widget->parent) {
       +                *errstr = "Widget already inside a container.\n";
       +                return 1;
       +        }
                if (row + row_span > grid->rows || column + column_span > grid->columns) {
                        *errstr = "Invalid row or column.\n";
                        return 1;
       t@@ -439,22 +451,22 @@ ltk_grid_cmd_add(
                        *errstr = "Invalid widget ID.\n";
                        return 1;
                }
       -        row         = strtonum(tokens[4], 0, grid->rows - 1, &errstr_num);
       +        row         = ltk_strtonum(tokens[4], 0, grid->rows - 1, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid row number.\n";
                        return 1;
                }
       -        column      = strtonum(tokens[5], 0, grid->columns - 1, &errstr_num);
       +        column      = ltk_strtonum(tokens[5], 0, grid->columns - 1, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid row number.\n";
                        return 1;
                }
       -        row_span    = strtonum(tokens[6], 1, grid->rows, &errstr_num);
       +        row_span    = ltk_strtonum(tokens[6], 1, grid->rows, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid row span.\n";
                        return 1;
                }
       -        column_span = strtonum(tokens[7], 1, grid->columns, &errstr_num);
       +        column_span = ltk_strtonum(tokens[7], 1, grid->columns, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid column span.\n";
                        return 1;
       t@@ -517,12 +529,12 @@ ltk_grid_cmd_create(
                        *errstr = "Widget ID already taken.\n";
                        return 1;
                }
       -        rows    = strtonum(tokens[3], 1, 64, &errstr_num);
       +        rows    = ltk_strtonum(tokens[3], 1, 64, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid number of rows.\n";
                        return 1;
                }
       -        columns = strtonum(tokens[4], 1, 64, &errstr_num);
       +        columns = ltk_strtonum(tokens[4], 1, 64, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid number of columns.\n";
                        return 1;
       t@@ -550,12 +562,12 @@ ltk_grid_cmd_set_row_weight(
                }
                grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, errstr);
                if (!grid) return 1;
       -        row    = strtonum(tokens[3], 0, grid->rows, &errstr_num);
       +        row    = ltk_strtonum(tokens[3], 0, grid->rows, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid row number.\n";
                        return 1;
                }
       -        weight = strtonum(tokens[4], 0, 64, &errstr_num);
       +        weight = ltk_strtonum(tokens[4], 0, 64, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid row weight.\n";
                        return 1;
       t@@ -582,12 +594,12 @@ ltk_grid_cmd_set_column_weight(
                }
                grid = (ltk_grid *)ltk_get_widget(tokens[1], LTK_GRID, errstr);
                if (!grid) return 1;
       -        column = strtonum(tokens[3], 0, grid->columns, &errstr_num);
       +        column = ltk_strtonum(tokens[3], 0, grid->columns, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid column number.\n";
                        return 1;
                }
       -        weight = strtonum(tokens[4], 0, 64, &errstr_num);
       +        weight = ltk_strtonum(tokens[4], 0, 64, &errstr_num);
                if (errstr_num) {
                        *errstr = "Invalid column weight.\n";
                        return 1;
   DIR diff --git a/src/label.c b/src/label.c
       t@@ -34,6 +34,8 @@
        #include "graphics.h"
        #include "surface_cache.h"
        
       +#define MAX_LABEL_PADDING 500
       +
        static void ltk_label_draw(ltk_widget *self, ltk_rect clip);
        static ltk_label *ltk_label_create(ltk_window *window,
            const char *id, char *text);
       t@@ -57,24 +59,26 @@ static struct {
        void
        ltk_label_setup_theme_defaults(ltk_window *window) {
                theme.pad = 5;
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#FFFFFF", &theme.text_color);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#000000", &theme.bg_color);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &theme.text_color);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &theme.bg_color);
        }
        
        void
        ltk_label_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        const char *errstr;
       +        /* FIXME: store generic max padding somewhere for all widgets? */
                if (strcmp(prop, "pad") == 0) {
       -                theme.pad = atoi(value);
       +                theme.pad = ltk_strtonum(value, 0, MAX_LABEL_PADDING, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid label padding '%s': %s.\n", value, errstr);
                } else if (strcmp(prop, "text_color") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.text_color);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.text_color))
       +                        ltk_warn("Error setting label text color to '%s'.\n", value);
                } else if (strcmp(prop, "bg_color") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.bg_color);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_color))
       +                        ltk_warn("Error setting label background color to '%s'.\n", value);
                } else {
       -                ltk_warn("Unknown property \"%s\" for label style.\n", prop);
       +                ltk_warn("Unknown property '%s' for label style.\n", prop);
                }
        }
        
       t@@ -121,6 +125,12 @@ ltk_label_create(ltk_window *window, const char *id, char *text) {
        static void
        ltk_label_destroy(ltk_widget *self, int shallow) {
                (void)shallow;
       +        char *errstr;
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
                ltk_label *label = (ltk_label *)self;
                if (!label) {
                        ltk_warn("Tried to destroy NULL label.\n");
       t@@ -128,8 +138,8 @@ ltk_label_destroy(ltk_widget *self, int shallow) {
                }
                ltk_surface_cache_release_key(self->surface_key);
                ltk_text_line_destroy(label->tl);
       -        ltk_remove_widget(label->widget.id);
       -        ltk_free(label->widget.id);
       +        ltk_remove_widget(self->id);
       +        ltk_free(self->id);
                ltk_free(label);
        }
        
   DIR diff --git a/src/ltk.h b/src/ltk.h
       t@@ -28,7 +28,8 @@
        typedef enum {
                LTK_EVENT_RESIZE = 1 << 0,
                LTK_EVENT_BUTTON = 1 << 1,
       -        LTK_EVENT_KEY = 1 << 2
       +        LTK_EVENT_KEY = 1 << 2,
       +        LTK_EVENT_MENU = 1 << 3
        } ltk_event_type;
        
        typedef struct {
       t@@ -83,6 +84,14 @@ typedef struct ltk_window {
                ltk_rect dirty_rect;
                struct ltk_event_queue *first_event;
                struct ltk_event_queue *last_event;
       +        /* FIXME: generic array */
       +        ltk_widget **popups;
       +        size_t popups_num;
       +        size_t popups_alloc;
       +        /* This is a hack so ltk_window_unregister_all_popups can
       +           call hide for all popup widgets even if the hide function
       +           already calls ltk_window_unregister_popup */
       +        char popups_locked;
        } ltk_window;
        
        void ltk_window_invalidate_rect(ltk_window *window, ltk_rect rect);
       t@@ -92,4 +101,10 @@ void ltk_window_set_active_widget(ltk_window *window, ltk_widget *widget);
        void ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget);
        void ltk_quit(ltk_window *window);
        
       +void ltk_unregister_timer(int timer_id);
       +int ltk_register_timer(long first, long repeat, void (*callback)(void *), void *data);
       +void ltk_window_register_popup(ltk_window *window, ltk_widget *popup);
       +void ltk_window_unregister_popup(ltk_window *window, ltk_widget *popup);
       +void ltk_window_unregister_all_popups(ltk_window *window);
       +
        #endif
   DIR diff --git a/src/ltkd.c b/src/ltkd.c
       t@@ -55,6 +55,11 @@
        #include "label.h"
        #include "scrollbar.h"
        #include "box.h"
       +#include "menu.h"
       +#include "macros.h"
       +
       +#define MAX_WINDOW_BORDER_WIDTH 100
       +#define MAX_FONT_SIZE 200
        
        #define MAX_SOCK_CONNS 20
        #define READ_BLK_SIZE 128
       t@@ -85,6 +90,18 @@ static struct ltk_sock_info {
                struct token_list tokens;  /* current tokens */
        } sockets[MAX_SOCK_CONNS];
        
       +typedef struct {
       +        void (*callback)(void *);
       +        void *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 int ltk_mainloop(ltk_window *window);
        static char *get_sock_path(char *basedir, Window id);
        static FILE *open_log(char *dir);
       t@@ -179,9 +196,15 @@ ltk_mainloop(ltk_window *window) {
                maxfd = listenfd;
        
                printf("%lu", window->xwindow);
       -        /*fflush(stdout);*/
       +        fflush(stdout);
                daemonize();
        
       +        /* 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;
       +        clock_gettime(CLOCK_MONOTONIC, &last);
       +
                while (running) {
                        rfds = rallfds;
                        wfds = wallfds;
       t@@ -241,6 +264,29 @@ ltk_mainloop(ltk_window *window) {
                                }
                        }
        
       +                clock_gettime(CLOCK_MONOTONIC, &now);
       +                ltk_timespecsub(&now, &last, &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 timers 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++;
       +                        }
       +                }
       +                last = now;
       +
                        if (window->dirty_rect.w != 0 && window->dirty_rect.h != 0) {
                                ltk_redraw_window(window);
                                window->dirty_rect.w = 0;
       t@@ -378,6 +424,7 @@ ltk_cleanup(void) {
                ltk_widgets_cleanup();
                if (main_window)
                        ltk_destroy_window(main_window);
       +        main_window = NULL;
        }
        
        void
       t@@ -481,10 +528,15 @@ ltk_redraw_window(ltk_window *window) {
                        window->rect.x, window->rect.y,
                        window->rect.w, window->rect.h
                );
       -        if (!window->root_widget) return;
       -        ptr = window->root_widget;
       -        if (ptr)
       +        if (window->root_widget) {
       +                ptr = window->root_widget;
                        ptr->vtable->draw(ptr, window->rect);
       +        }
       +        /* last popup is the newest one, so draw that last */
       +        for (size_t i = 0; i < window->popups_num; i++) {
       +                ptr = window->popups[i];
       +                ptr->vtable->draw(ptr, window->rect);
       +        }
                XdbeSwapInfo swap_info;
                swap_info.swap_window = window->xwindow;
                swap_info.swap_action = XdbeBackground;
       t@@ -497,6 +549,7 @@ static void
        ltk_window_other_event(ltk_window *window, XEvent event) {
                ltk_widget *ptr = window->root_widget;
                if (event.type == ConfigureNotify) {
       +                ltk_window_unregister_all_popups(window);
                        int w, h;
                        w = event.xconfigure.width;
                        h = event.xconfigure.height;
       t@@ -526,6 +579,124 @@ ltk_window_other_event(ltk_window *window, XEvent event) {
                }
        }
        
       +/* 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)(void *), void *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: 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;
       +}
       +
       +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) {
       +                        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++) {
       +                if (window->popups[i]->vtable->hide) {
       +                        window->popups[i]->vtable->hide(window->popups[i]);
       +                }
       +                window->popups[i]->hidden = 1;
       +        }
       +        window->popups_num = 0;
       +        /* somewhat arbitrary, but should be enough for most cases */
       +        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);
       +}
       +
        static ltk_window *
        ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int h) {
                char *theme_path;
       t@@ -533,6 +704,10 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
        
                ltk_window *window = ltk_malloc(sizeof(ltk_window));
        
       +        window->popups = NULL;
       +        window->popups_num = window->popups_alloc = 0;
       +        window->popups_locked = 0;
       +
                window->dpy = XOpenDisplay(NULL);
                window->screen = DefaultScreen(window->dpy);
                /* based on http://wili.cc/blog/xdbe.html */
       t@@ -562,6 +737,9 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
                                ltk_fatal("Couldn't match a Visual with double buffering.\n");
                        }
                        window->vis = xvisinfo_match->visual;
       +                /* FIXME: is it legal to free this while keeping the visual? */
       +                XFree(xvisinfo_match);
       +                XdbeFreeVisualInfo(info);
                        found = 1;
                } else {
                        window->vis = DefaultVisual(window->dpy, window->screen);
       t@@ -574,6 +752,7 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
                if (!theme_path)
                        ltk_fatal_errno("Not enough memory for theme path.\n");
                ltk_load_theme(window, theme_path);
       +        ltk_free(theme_path);
                window->wm_delete_msg = XInternAtom(window->dpy, "WM_DELETE_WINDOW", False);
        
                memset(&attrs, 0, sizeof(attrs));
       t@@ -641,10 +820,13 @@ ltk_create_window(const char *title, int x, int y, unsigned int w, unsigned int 
        static void
        ltk_destroy_window(ltk_window *window) {
                ltk_text_context_destroy(window->text_context);
       +        if (window->popups)
       +                ltk_free(window->popups);
       +        XFreeGC(window->dpy, window->gc);
                XDestroyWindow(window->dpy, window->xwindow);
                XCloseDisplay(window->dpy);
       -        /* FIXME: This doesn't work because it can sometimes be a readonly
       -           string from ltk_window_setup_theme_defaults! */
       +        ltk_surface_destroy(window->surface);
       +        ltk_surface_cache_destroy(window->surface_cache);
                if (window->theme.font)
                        ltk_free(window->theme.font);
                ltk_free(window);
       t@@ -652,18 +834,23 @@ ltk_destroy_window(ltk_window *window) {
        
        void
        ltk_window_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        const char *errstr;
                if (strcmp(prop, "border_width") == 0) {
       -                window->theme.border_width = atoi(value);
       +                window->theme.border_width = ltk_strtonum(value, 0, MAX_WINDOW_BORDER_WIDTH, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid window border width '%s': %s.\n", value, errstr);
                } else if (strcmp(prop, "bg") == 0) {
       -                ltk_color_create(window->dpy, window->screen,
       -                    window->cm, value, &window->theme.bg);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &window->theme.bg))
       +                        ltk_warn("Error setting window background color to '%s'.\n", value);
                } else if (strcmp(prop, "fg") == 0) {
       -                ltk_color_create(window->dpy, window->screen,
       -                    window->cm, value, &window->theme.fg);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &window->theme.fg))
       +                        ltk_warn("Error setting window foreground color to '%s'.\n", value);
                } else if (strcmp(prop, "font") == 0) {
                        window->theme.font = ltk_strdup(value);
                } else if (strcmp(prop, "font_size") == 0) {
       -                window->theme.font_size = atoi(value);
       +                window->theme.font_size = ltk_strtonum(value, 0, MAX_FONT_SIZE, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid window font size '%s': %s.\n", value, errstr);
                }
        }
        
       t@@ -677,6 +864,10 @@ ltk_ini_handler(void *window, const char *widget, const char *prop, const char *
                        ltk_label_ini_handler(window, prop, value);
                } else if (strcmp(widget, "scrollbar") == 0) {
                        ltk_scrollbar_ini_handler(window, prop, value);
       +        } else if (strcmp(widget, "menu") == 0) {
       +                ltk_menu_ini_handler(window, prop, value);
       +        } else if (strcmp(widget, "submenu") == 0) {
       +                ltk_submenu_ini_handler(window, prop, value);
                } else {
                        return 0;
                }
       t@@ -688,10 +879,8 @@ ltk_window_setup_theme_defaults(ltk_window *window) {
                window->theme.border_width = 0;
                window->theme.font_size = 15;
                window->theme.font = ltk_strdup("Liberation Mono");
       -        ltk_color_create(window->dpy, window->screen,
       -            window->cm, "#000000", &window->theme.bg);
       -        ltk_color_create(window->dpy, window->screen,
       -            window->cm, "#FFFFFF", &window->theme.fg);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &window->theme.bg);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &window->theme.fg);
        }
        
        static void
       t@@ -701,6 +890,7 @@ ltk_load_theme(ltk_window *window, const char *path) {
                ltk_button_setup_theme_defaults(window);
                ltk_label_setup_theme_defaults(window);
                ltk_scrollbar_setup_theme_defaults(window);
       +        ltk_menu_setup_theme_defaults(window);
                if (ini_parse(path, ltk_ini_handler, window) < 0) {
                        ltk_warn("Can't load theme.\n");
                }
       t@@ -741,8 +931,18 @@ ltk_window_set_pressed_widget(ltk_window *window, ltk_widget *widget) {
                }
        }
        
       +static ltk_widget *
       +get_hover_popup(ltk_window *window, int x, int y) {
       +        for (size_t i = window->popups_num; i-- > 0;) {
       +                if (ltk_collide_rect(window->popups[i]->rect, x, y))
       +                        return window->popups[i];
       +        }
       +        return NULL;
       +}
       +
        static void
        ltk_handle_event(ltk_window *window, XEvent event) {
       +        ltk_widget *hover_popup;
                ltk_widget *root_widget = window->root_widget;
                switch (event.type) {
                case KeyPress:
       t@@ -750,15 +950,26 @@ ltk_handle_event(ltk_window *window, XEvent event) {
                case KeyRelease:
                        break;
                case ButtonPress:
       -                if (root_widget)
       +                hover_popup = get_hover_popup(window, event.xbutton.x, event.xbutton.y);
       +                if (hover_popup) {
       +                        ltk_widget_mouse_press_event(hover_popup, event);
       +                } else if (root_widget) {
       +                        ltk_window_unregister_all_popups(window);
                                ltk_widget_mouse_press_event(root_widget, event);
       +                }
                        break;
                case ButtonRelease:
       -                if (root_widget)
       +                hover_popup = get_hover_popup(window, event.xbutton.x, event.xbutton.y);
       +                if (hover_popup)
       +                        ltk_widget_mouse_release_event(hover_popup, event);
       +                else if (root_widget)
                                ltk_widget_mouse_release_event(root_widget, event);
                        break;
                case MotionNotify:
       -                if (root_widget)
       +                hover_popup = get_hover_popup(window, event.xmotion.x, event.xmotion.y);
       +                if (hover_popup)
       +                        ltk_widget_motion_notify_event(hover_popup, event);
       +                else if (root_widget)
                                ltk_widget_motion_notify_event(root_widget, event);
                        break;
                default:
       t@@ -1015,6 +1226,10 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
                                err = ltk_button_cmd(window, tokens, num_tokens, &errstr);
                        } else if (strcmp(tokens[0], "label") == 0) {
                                err = ltk_label_cmd(window, tokens, num_tokens, &errstr);
       +                } else if (strcmp(tokens[0], "menu") == 0) {
       +                        err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
       +                } else if (strcmp(tokens[0], "submenu") == 0) {
       +                        err = ltk_menu_cmd(window, tokens, num_tokens, &errstr);
                        } else if (strcmp(tokens[0], "set-root-widget") == 0) {
                                err = ltk_set_root_widget_cmd(window, tokens, num_tokens, &errstr);
        /*
       t@@ -1024,7 +1239,7 @@ process_commands(ltk_window *window, struct ltk_sock_info *sock) {
                        } else if (strcmp(tokens[0], "quit") == 0) {
                                ltk_quit(window);
                        } else if (strcmp(tokens[0], "destroy") == 0) {
       -                        err = ltk_widget_destroy(window, tokens, num_tokens, &errstr);
       +                        err = ltk_widget_destroy_cmd(window, tokens, num_tokens, &errstr);
                        } else {
                                errstr = "Invalid command.\n";
                                err = 1;
   DIR diff --git a/src/macros.h b/src/macros.h
       t@@ -0,0 +1,25 @@
       +#ifndef _MACROS_H_
       +#define _MACROS_H_
       +
       +/* stolen from OpenBSD */
       +#define ltk_timespecadd(tsp, usp, vsp)                                  \
       +        do {                                                            \
       +                (vsp)->tv_sec = (tsp)->tv_sec + (usp)->tv_sec;          \
       +                (vsp)->tv_nsec = (tsp)->tv_nsec + (usp)->tv_nsec;       \
       +                if ((vsp)->tv_nsec >= 1000000000L) {                    \
       +                        (vsp)->tv_sec++;                                \
       +                        (vsp)->tv_nsec -= 1000000000L;                  \
       +                }                                                       \
       +        } while (0)
       +
       +#define ltk_timespecsub(tsp, usp, vsp)                                  \
       +        do {                                                            \
       +                (vsp)->tv_sec = (tsp)->tv_sec - (usp)->tv_sec;          \
       +                (vsp)->tv_nsec = (tsp)->tv_nsec - (usp)->tv_nsec;       \
       +                if ((vsp)->tv_nsec < 0) {                               \
       +                        (vsp)->tv_sec--;                                \
       +                        (vsp)->tv_nsec += 1000000000L;                  \
       +                }                                                       \
       +        } while (0)
       +
       +#endif
   DIR diff --git a/src/memory.c b/src/memory.c
       t@@ -129,6 +129,9 @@ ltk_reallocarray(void *optr, size_t nmemb, size_t size)
        size_t
        ideal_array_size(size_t old, size_t needed) {
                size_t ret = old;
       +        /* FIXME: the shrinking here only makes sense if not
       +           many elements are removed at once - what would be
       +           more sensible here? */
                if (old < needed)
                        ret = old * 2 > needed ? old * 2 : needed;
                else if (needed * 4 < old)
   DIR diff --git a/src/menu.c b/src/menu.c
       t@@ -0,0 +1,1772 @@
       +/*
       + * Copyright (c) 2022 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 <stdlib.h>
       +#include <stdint.h>
       +#include <string.h>
       +#include <stdarg.h>
       +#include <math.h>
       +
       +#include <X11/Xlib.h>
       +#include <X11/Xutil.h>
       +
       +#include "memory.h"
       +#include "color.h"
       +#include "rect.h"
       +#include "widget.h"
       +#include "ltk.h"
       +#include "util.h"
       +#include "text.h"
       +#include "menu.h"
       +#include "graphics.h"
       +#include "surface_cache.h"
       +
       +#define MAX_MENU_BORDER_WIDTH 100
       +#define MAX_MENU_PAD 500
       +#define MAX_MENU_ARROW_SIZE 100
       +
       +#define MAX(a, b) ((a) > (b) ? (a) : (b))
       +
       +static struct theme {
       +        int border_width;
       +        int pad;
       +        int text_pad;
       +        int arrow_size;
       +        int arrow_pad;
       +        int compress_borders;
       +        int menu_border_width;
       +        /* FIXME: should border_sides actually factor into
       +           size calculation? - probably useless and would
       +           just make it more complicated */
       +        /* FIXME: allow different values for different states? */
       +        ltk_border_sides border_sides;
       +
       +        ltk_color background;
       +        ltk_color scroll_background;
       +        ltk_color scroll_arrow_color;
       +        ltk_color menu_border;
       +
       +        ltk_color text;
       +        ltk_color border;
       +        ltk_color fill;
       +
       +        ltk_color text_pressed;
       +        ltk_color border_pressed;
       +        ltk_color fill_pressed;
       +
       +        ltk_color text_active;
       +        ltk_color border_active;
       +        ltk_color fill_active;
       +
       +        ltk_color text_disabled;
       +        ltk_color border_disabled;
       +        ltk_color fill_disabled;
       +} menu_theme, submenu_theme;
       +
       +static void ini_handler(ltk_window *window, struct theme *t, const char *prop, const char *value);
       +static void ltk_menu_resize(ltk_widget *self);
       +static void ltk_menu_change_state(ltk_widget *self);
       +static void ltk_menu_draw(ltk_widget *self, ltk_rect clip);
       +static void ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s);
       +static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret);
       +static void ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step);
       +static void ltk_menu_scroll_callback(void *data);
       +static void stop_scrolling(ltk_menu *menu);
       +static size_t get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret);
       +static int set_scroll_timer(ltk_menu *menu, int x, int y);
       +static int ltk_menu_mouse_release(ltk_widget *self, XEvent event);
       +static int ltk_menu_mouse_press(ltk_widget *self, XEvent event);
       +static void ltk_menu_hide(ltk_widget *self);
       +static void popup_active_menu(ltk_menu *menu, ltk_rect r);
       +static void unpopup_active_entry(ltk_menu *menu);
       +static void handle_hover(ltk_menu *menu, int x, int y);
       +static int ltk_menu_motion_notify(ltk_widget *self, XEvent event);
       +static int ltk_menu_mouse_enter(ltk_widget *self, XEvent event);
       +static int ltk_menu_mouse_leave(ltk_widget *self, XEvent event);
       +static ltk_menu *ltk_menu_create(ltk_window *window, const char *id, int is_submenu);
       +static ltk_menuentry *insert_entry(ltk_menu *menu, size_t idx);
       +static void recalc_menu_size(ltk_menu *menu);
       +static void shrink_entries(ltk_menu *menu);
       +static size_t get_entry_with_id(ltk_menu *menu, const char *id);
       +static void ltk_menu_destroy(ltk_widget *self, int shallow);
       +
       +static ltk_menuentry *ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
       +static ltk_menuentry *ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
       +static ltk_menuentry *ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr);
       +static ltk_menuentry *ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr);
       +static int ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr);
       +static int ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr);
       +static int ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr);
       +static int ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr);
       +static int ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr);
       +static int ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
       +static int ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr);
       +static int ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr);
       +static int ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr);
       +static int ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr);
       +static int ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr);
       +
       +static struct ltk_widget_vtable vtable = {
       +    .mouse_press = &ltk_menu_mouse_press,
       +    .motion_notify = &ltk_menu_motion_notify,
       +    .mouse_release = &ltk_menu_mouse_release,
       +    .mouse_enter = &ltk_menu_mouse_enter,
       +    .mouse_leave = &ltk_menu_mouse_leave,
       +    .resize = &ltk_menu_resize,
       +    .change_state = &ltk_menu_change_state,
       +    .hide = &ltk_menu_hide,
       +    .draw = &ltk_menu_draw,
       +    .destroy = &ltk_menu_destroy,
       +    .type = LTK_MENU,
       +    .needs_redraw = 1,
       +    .needs_surface = 1
       +};
       +
       +/* FIXME: maybe just store colors as pointers and check after
       +   ini handling if any are null */
       +
       +void
       +ltk_menu_setup_theme_defaults(ltk_window *window) {
       +        menu_theme.border_width = 2;
       +        menu_theme.pad = 0;
       +        menu_theme.text_pad = 5;
       +        menu_theme.arrow_size = 10;
       +        menu_theme.arrow_pad = 5;
       +        menu_theme.compress_borders = 1;
       +        menu_theme.border_sides = LTK_BORDER_ALL;
       +        menu_theme.menu_border_width = 0;
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &menu_theme.background);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#333333", &menu_theme.scroll_background);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &menu_theme.scroll_arrow_color);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#339999", &menu_theme.border);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &menu_theme.fill);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &menu_theme.fill_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &menu_theme.fill_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.text_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &menu_theme.border_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &menu_theme.fill_disabled);
       +
       +        /* FIXME: actually unnecessary since border width is 0 */
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &menu_theme.menu_border);
       +
       +        submenu_theme.border_width = 0;
       +        submenu_theme.pad = 5;
       +        submenu_theme.text_pad = 5;
       +        submenu_theme.arrow_size = 10;
       +        submenu_theme.arrow_pad = 5;
       +        submenu_theme.compress_borders = 0;
       +        submenu_theme.border_sides = LTK_BORDER_NONE;
       +        submenu_theme.menu_border_width = 1;
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &submenu_theme.background);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#333333", &submenu_theme.scroll_background);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.scroll_arrow_color);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.menu_border);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.text);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.text_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &submenu_theme.text_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &submenu_theme.fill_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.text_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &submenu_theme.fill_disabled);
       +
       +        /* FIXME: make this unnecessary if border width is 0 */
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#FFFFFF", &submenu_theme.border_disabled);
       +}
       +
       +/* FIXME: use border-width, etc. */
       +/* FIXME: make theme parsing more convenient */
       +/* FIXME: DEALLOCATE COLORS INSTEAD OF OVERWRITING DEFAULTS! */
       +static void
       +ini_handler(ltk_window *window, struct theme *t, const char *prop, const char *value) {
       +        const char *errstr;
       +        if (strcmp(prop, "border_width") == 0) {
       +                t->border_width = ltk_strtonum(value, 0, MAX_MENU_BORDER_WIDTH, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid menu border width '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "menu_border_width") == 0) {
       +                t->menu_border_width = ltk_strtonum(value, 0, MAX_MENU_BORDER_WIDTH, &errstr);
       +                /* FIXME: clarify different types of border width in error message */
       +                if (errstr)
       +                        ltk_warn("Invalid menu border width '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "pad") == 0) {
       +                t->pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid menu pad '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "text_pad") == 0) {
       +                t->text_pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid menu text pad '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "arrow_size") == 0) {
       +                /* FIXME: should be error when used for menu instead of submenu */
       +                t->arrow_size = ltk_strtonum(value, 0, MAX_MENU_ARROW_SIZE, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid menu arrow size '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "arrow_pad") == 0) {
       +                /* FIXME: should be error when used for menu instead of submenu */
       +                t->arrow_pad = ltk_strtonum(value, 0, MAX_MENU_PAD, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid menu arrow pad '%s': %s.\n", value, errstr);
       +        } else if (strcmp(prop, "compress_borders") == 0) {
       +                if (strcmp(value, "true") == 0)
       +                        t->compress_borders = 1;
       +                else if (strcmp(value, "false") == 0)
       +                        t->compress_borders = 0;
       +                else
       +                        ltk_warn("Invalid menu compress_borders '%s'.\n", value);
       +        } else if (strcmp(prop, "border_sides") == 0) {
       +                t->border_sides = LTK_BORDER_NONE;
       +                for (const char *c = value; *c != '\0'; c++) {
       +                        switch (*c) {
       +                        case 't':
       +                                t->border_sides |= LTK_BORDER_TOP;
       +                                break;
       +                        case 'b':
       +                                t->border_sides |= LTK_BORDER_BOTTOM;
       +                                break;
       +                        case 'l':
       +                                t->border_sides |= LTK_BORDER_LEFT;
       +                                break;
       +                        case 'r':
       +                                t->border_sides |= LTK_BORDER_RIGHT;
       +                                break;
       +                        default:
       +                                ltk_warn("Invalid menu border_sides '%s'.\n", value);
       +                                return;
       +                        }
       +                }
       +        } else if (strcmp(prop, "background") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->background))
       +                        ltk_warn("Error setting menu background color to '%s'.\n", value);
       +        } else if (strcmp(prop, "menu_border") == 0) {
       +                /* FIXME: clarify different type of menu border color */
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->menu_border))
       +                        ltk_warn("Error setting menu border color to '%s'.\n", value);
       +        } else if (strcmp(prop, "scroll_background") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->scroll_background))
       +                        ltk_warn("Error setting menu scroll background color to '%s'.\n", value);
       +        } else if (strcmp(prop, "scroll_arrow_color") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->scroll_arrow_color))
       +                        ltk_warn("Error setting menu scroll arrow color to '%s'.\n", value);
       +        } else if (strcmp(prop, "text") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text))
       +                        ltk_warn("Error setting menu text color to '%s'.\n", value);
       +        } else if (strcmp(prop, "border") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border))
       +                        ltk_warn("Error setting menu border color to '%s'.\n", value);
       +        } else if (strcmp(prop, "fill") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill))
       +                        ltk_warn("Error setting menu fill color to '%s'.\n", value);
       +        } else if (strcmp(prop, "text_pressed") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_pressed))
       +                        ltk_warn("Error setting menu pressed text color to '%s'.\n", value);
       +        } else if (strcmp(prop, "border_pressed") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_pressed))
       +                        ltk_warn("Error setting menu pressed border color to '%s'.\n", value);
       +        } else if (strcmp(prop, "fill_pressed") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_pressed))
       +                        ltk_warn("Error setting menu pressed fill color to '%s'.\n", value);
       +        } else if (strcmp(prop, "text_active") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_active))
       +                        ltk_warn("Error setting menu active text color to '%s'.\n", value);
       +        } else if (strcmp(prop, "border_active") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_active))
       +                        ltk_warn("Error setting menu active border color to '%s'.\n", value);
       +        } else if (strcmp(prop, "fill_active") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_active))
       +                        ltk_warn("Error setting menu active fill color to '%s'.\n", value);
       +        } else if (strcmp(prop, "text_disabled") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->text_disabled))
       +                        ltk_warn("Error setting menu disabled text color to '%s'.\n", value);
       +        } else if (strcmp(prop, "border_disabled") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->border_disabled))
       +                        ltk_warn("Error setting menu disabled border color to '%s'.\n", value);
       +        } else if (strcmp(prop, "fill_disabled") == 0) {
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &t->fill_disabled))
       +                        ltk_warn("Error setting menu disabled fill color to '%s'.\n", value);
       +        } else {
       +                ltk_warn("Unknown property '%s' for button style.\n", prop);
       +        }
       +}
       +
       +void
       +ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        ini_handler(window, &menu_theme, prop, value);
       +}
       +
       +void
       +ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        ini_handler(window, &submenu_theme, prop, value);
       +}
       +
       +static void
       +ltk_menu_resize(ltk_widget *self) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        double x_old = menu->x_scroll_offset;
       +        double y_old = menu->y_scroll_offset;
       +        int max_x, max_y;
       +        ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
       +        if (menu->x_scroll_offset > max_x)
       +                menu->x_scroll_offset = max_x;
       +        if (menu->y_scroll_offset > max_y)
       +                menu->y_scroll_offset = max_y;
       +        if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
       +            fabs(y_old - menu->y_scroll_offset) < 0.01) {
       +                menu->widget.dirty = 1;
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        }
       +}
       +
       +static void
       +ltk_menu_change_state(ltk_widget *self) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        if (self->state != LTK_PRESSED && menu->pressed_entry < menu->num_entries) {
       +                menu->pressed_entry = SIZE_MAX;
       +                self->dirty = 1;
       +                ltk_window_invalidate_rect(self->window, self->rect);
       +        }
       +}
       +
       +static void
       +ltk_menu_draw(ltk_widget *self, ltk_rect clip) {
       +        if (self->hidden)
       +                return;
       +        ltk_menu *menu = (ltk_menu *)self;
       +        ltk_rect rect = self->rect;
       +        ltk_rect clip_final = ltk_rect_intersect(clip, rect);
       +        ltk_surface *s;
       +        if (!ltk_surface_cache_get_surface(self->surface_key, &s) || self->dirty)
       +                ltk_menu_redraw_surface(menu, s);
       +        ltk_surface_copy(s, self->window->surface, ltk_rect_relative(rect, clip_final), clip_final.x, clip_final.y);
       +}
       +
       +/* FIXME: glitches when drawing text with stb backend while scrolling */
       +static void
       +ltk_menu_redraw_surface(ltk_menu *menu, ltk_surface *s) {
       +        ltk_rect rect = menu->widget.rect;
       +        int ideal_w = menu->widget.ideal_w, ideal_h = menu->widget.ideal_h;
       +        struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
       +
       +        int arrow_size = t->arrow_pad * 2 + t->arrow_size;
       +        int start_x = rect.w < ideal_w ? arrow_size : 0;
       +        int start_y = rect.h < ideal_h ? arrow_size : 0;
       +        start_x += t->menu_border_width;
       +        start_y += t->menu_border_width;
       +        int real_w = rect.w - start_x * 2;
       +        int real_h = rect.h - start_y * 2;
       +
       +        int offset_x = (int)menu->x_scroll_offset;
       +        int offset_y = (int)menu->y_scroll_offset;
       +
       +        ltk_surface_fill_rect(s, &t->background, (ltk_rect){0, 0, rect.w, rect.h});
       +        int text_w, text_h;
       +        ltk_color *text, *border, *fill;
       +        int cur_abs_x = 0, cur_abs_y = 0;
       +        if (menu->is_submenu)
       +                cur_abs_y = t->pad;
       +        else
       +                cur_abs_x = t->pad;
       +        int overlap = t->compress_borders ? t->border_width - t->pad : 0;
       +        int bw_advance = t->compress_borders ? t->border_width : t->border_width * 2;
       +        int mbw = t->menu_border_width;
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                ltk_menuentry *e = &menu->entries[i];
       +                ltk_text_line_get_size(e->text, &text_w, &text_h);
       +                if (menu->is_submenu) {
       +                        if (cur_abs_y + t->border_width * 2 + t->text_pad * 2 + text_h <= offset_y) {
       +                                /* FIXME: ugly because repeated further down */
       +                                cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
       +                                continue;
       +                        } else if (cur_abs_y >= offset_y + real_h) {
       +                                break;
       +                        }
       +                } else {
       +                        if (cur_abs_x + t->border_width * 2 + t->text_pad * 2 + text_w <= offset_x) {
       +                                cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
       +                                continue;
       +                        } else if (cur_abs_x >= offset_x + real_w) {
       +                                break;
       +                        }
       +                }
       +                /* FIXME: allow different border_sides for different states */
       +                if (e->disabled) {
       +                        text = &t->text_disabled;
       +                        border = &t->border_disabled;
       +                        fill = &t->fill_disabled;
       +                } else if (menu->pressed_entry == i) {
       +                        text = &t->text_pressed;
       +                        border = &t->border_pressed;
       +                        fill = &t->fill_pressed;
       +                } else if (menu->active_entry == i) {
       +                        text = &t->text_active;
       +                        border = &t->border_active;
       +                        fill = &t->fill_active;
       +                } else {
       +                        text = &t->text;
       +                        border = &t->border;
       +                        fill = &t->fill;
       +                }
       +                /* FIXME: how well-defined is it to give X drawing commands
       +                   with parts outside of the actual pixmap? */
       +                /* FIXME: optimize drawing (avoid drawing pixels multiple times) */
       +                int draw_x = cur_abs_x - offset_x + start_x;
       +                int draw_y = cur_abs_y - offset_y + start_y;
       +                int last_special = i > 0 && (menu->active_entry == i - 1 || menu->pressed_entry == i - 1);
       +                if (menu->is_submenu) {
       +                        int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
       +                        int height = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
       +                        ltk_rect r;
       +                        if (last_special && overlap > 0) {
       +                                r = (ltk_rect){
       +                                    draw_x + overlap,
       +                                    draw_y + t->pad, /* t->pad is the same as t->border_width - overlap */
       +                                    ideal_w - t->pad * 2 - mbw * 2,
       +                                    height - overlap
       +                                };
       +                        } else {
       +                                r = (ltk_rect){draw_x + t->pad, draw_y, ideal_w - t->pad * 2, height};
       +                        }
       +                        ltk_surface_fill_rect(s, fill, r);
       +                        ltk_text_line_draw(
       +                            e->text, s, text,
       +                            draw_x + t->pad + t->border_width + t->text_pad,
       +                            draw_y + height / 2 - text_h / 2
       +                        );
       +                        if (e->submenu) {
       +                                ltk_point arrow_points[3] = {
       +                                    {draw_x + ideal_w - t->pad - t->arrow_pad, draw_y + height / 2},
       +                                    {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 - t->arrow_size / 2},
       +                                    {draw_x + ideal_w - t->pad - t->arrow_pad - t->arrow_size, draw_y + height / 2 + t->arrow_size / 2}
       +                                };
       +                                ltk_surface_fill_polygon(s, text, arrow_points, 3);
       +                        }
       +                        if (last_special && overlap > 0) {
       +                                ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_TOP);
       +                                if (t->border_sides & LTK_BORDER_TOP)
       +                                        ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_TOP);
       +                        } else {
       +                                ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
       +                        }
       +                        cur_abs_y += bw_advance + t->text_pad * 2 + text_h + t->pad;
       +                } else {
       +                        ltk_rect r;
       +                        if (last_special && overlap > 0) {
       +                                r = (ltk_rect){
       +                                    draw_x + overlap,
       +                                    draw_y + t->pad,
       +                                    t->text_pad * 2 + t->border_width * 2 - overlap + text_w,
       +                                    ideal_h - t->pad * 2 - mbw * 2
       +                                };
       +                        } else {
       +                                r = (ltk_rect){draw_x, draw_y + t->pad, t->text_pad * 2 + t->border_width * 2 + text_w, ideal_h - t->pad * 2};
       +                        }
       +                        ltk_surface_fill_rect(s, fill, r);
       +                        /* FIXME: should the text be bottom-aligned in case different
       +                           entries have different text height? */
       +                        ltk_text_line_draw(
       +                            e->text, s, text,
       +                            draw_x + t->border_width + t->text_pad,
       +                            draw_y + t->pad + t->border_width + t->text_pad
       +                        );
       +                        if (last_special && overlap > 0) {
       +                                ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides & ~LTK_BORDER_LEFT);
       +                                if (t->border_sides & LTK_BORDER_LEFT)
       +                                        ltk_surface_draw_border(s, border, r, t->pad, LTK_BORDER_LEFT);
       +                        } else {
       +                                ltk_surface_draw_border(s, border, r, t->border_width, t->border_sides);
       +                        }
       +                        cur_abs_x += bw_advance + t->text_pad * 2 + text_w + t->pad;
       +                }
       +        }
       +        /* FIXME: active, pressed states */
       +        int sz = t->arrow_size + t->arrow_pad * 2;
       +        int ww = menu->widget.rect.w;
       +        int wh = menu->widget.rect.h;
       +        if (rect.w < ideal_w) {
       +                ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, sz, wh - mbw * 2});
       +                ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){ww - sz - mbw, mbw, sz, wh - mbw * 2});
       +                ltk_point arrow_points[3] = {
       +                    {t->arrow_pad + mbw, wh / 2},
       +                    {t->arrow_pad + mbw + t->arrow_size, wh / 2 - t->arrow_size / 2},
       +                    {t->arrow_pad + mbw + t->arrow_size, wh / 2 + t->arrow_size / 2}
       +                };
       +                ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
       +                arrow_points[0] = (ltk_point){ww - t->arrow_pad - mbw, wh / 2};
       +                arrow_points[1] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 - t->arrow_size / 2};
       +                arrow_points[2] = (ltk_point){ww - t->arrow_pad - mbw - t->arrow_size, wh / 2 + t->arrow_size / 2};
       +                ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
       +        }
       +        if (rect.h < ideal_h) {
       +                ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, mbw, ww - mbw * 2, sz});
       +                ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){mbw, wh - sz - mbw, ww - mbw * 2, sz});
       +                ltk_point arrow_points[3] = {
       +                    {ww / 2, t->arrow_pad + mbw},
       +                    {ww / 2 - t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size},
       +                    {ww / 2 + t->arrow_size / 2, t->arrow_pad + mbw + t->arrow_size}
       +                };
       +                ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
       +                arrow_points[0] = (ltk_point){ww / 2, wh - t->arrow_pad - mbw};
       +                arrow_points[1] = (ltk_point){ww / 2 - t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
       +                arrow_points[2] = (ltk_point){ww / 2 + t->arrow_size / 2, wh - t->arrow_pad - mbw - t->arrow_size};
       +                ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
       +        }
       +        ltk_surface_draw_border(s, &t->menu_border, (ltk_rect){0, 0, ww, wh}, mbw, LTK_BORDER_ALL);
       +
       +        menu->widget.dirty = 0;
       +}
       +
       +static void
       +ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) {
       +        struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
       +        int extra_size = theme->arrow_size * 2 + theme->arrow_pad * 4;
       +        *x_ret = 0;
       +        *y_ret = 0;
       +        if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
       +                *x_ret = menu->widget.ideal_w - (menu->widget.rect.w - extra_size);
       +        }
       +        if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
       +                *y_ret = menu->widget.ideal_h - (menu->widget.rect.h - extra_size);
       +        }
       +}
       +
       +static void
       +ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
       +        int max_scroll_x, max_scroll_y;
       +        ltk_menu_get_max_scroll_offset(menu, &max_scroll_x, &max_scroll_y);
       +        double y_old = menu->y_scroll_offset;
       +        double x_old = menu->x_scroll_offset;
       +        if (t)
       +                menu->y_scroll_offset -= step;
       +        else if (b)
       +                menu->y_scroll_offset += step;
       +        else if (l)
       +                menu->x_scroll_offset -= step;
       +        else if (r)
       +                menu->x_scroll_offset += step;
       +        if (menu->x_scroll_offset < 0)
       +                menu->x_scroll_offset = 0;
       +        if (menu->y_scroll_offset < 0)
       +                menu->y_scroll_offset = 0;
       +        if (menu->x_scroll_offset > max_scroll_x)
       +                menu->x_scroll_offset = max_scroll_x;
       +        if (menu->y_scroll_offset > max_scroll_y)
       +                menu->y_scroll_offset = max_scroll_y;
       +        /* FIXME: sensible epsilon? */
       +        if (fabs(x_old - menu->x_scroll_offset) < 0.01 ||
       +            fabs(y_old - menu->y_scroll_offset) < 0.01) {
       +                menu->widget.dirty = 1;
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        }
       +}
       +
       +/* FIXME: show scroll arrow disabled when nothing further */
       +static void
       +ltk_menu_scroll_callback(void *data) {
       +        ltk_menu *menu = (ltk_menu *)data;
       +        ltk_menu_scroll(
       +            menu,
       +            menu->scroll_top_hover, menu->scroll_bottom_hover,
       +            menu->scroll_left_hover, menu->scroll_right_hover, 2
       +        );
       +}
       +
       +/* FIXME: HANDLE mouse scroll wheel! */
       +
       +static void
       +stop_scrolling(ltk_menu *menu) {
       +        menu->scroll_top_hover = 0;
       +        menu->scroll_bottom_hover = 0;
       +        menu->scroll_left_hover = 0;
       +        menu->scroll_right_hover = 0;
       +        if (menu->scroll_timer_id >= 0)
       +                ltk_unregister_timer(menu->scroll_timer_id);
       +}
       +
       +/* FIXME: should ideal_w, ideal_h just be int? */
       +static size_t
       +get_entry_at_point(ltk_menu *menu, int x, int y, ltk_rect *entry_rect_ret) {
       +        struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
       +        int arrow_size = t->arrow_size + t->arrow_pad * 2;
       +        int mbw = t->menu_border_width;
       +        int start_x = menu->widget.rect.x + mbw, end_x = menu->widget.rect.x + menu->widget.rect.w - mbw;
       +        int start_y = menu->widget.rect.y + mbw, end_y = menu->widget.rect.y + menu->widget.rect.h - mbw;
       +        if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
       +                start_x += arrow_size;
       +                end_x -= arrow_size;
       +        }
       +        if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
       +                start_y += arrow_size;
       +                end_y -= arrow_size;
       +        }
       +        if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
       +                return SIZE_MAX;
       +
       +        int bw_sub = t->compress_borders ? t->border_width : 0;
       +        int cur_x = start_x - (int)menu->x_scroll_offset + t->pad;
       +        int cur_y = start_y - (int)menu->y_scroll_offset + t->pad;
       +        /* FIXME: could be optimized a bit */
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                ltk_menuentry *e = &menu->entries[i];
       +                int text_w, text_h;
       +                ltk_text_line_get_size(e->text, &text_w, &text_h);
       +                if (menu->is_submenu) {
       +                        int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
       +                        int w = (int)menu->widget.ideal_w - t->pad * 2;
       +                        int h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
       +                        if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
       +                                if (entry_rect_ret) {
       +                                        entry_rect_ret->x = cur_x;
       +                                        entry_rect_ret->y = cur_y;
       +                                        entry_rect_ret->w = w;
       +                                        entry_rect_ret->h = h;
       +                                }
       +                                return i;
       +                        }
       +                        cur_y += h - bw_sub + t->pad;
       +                } else {
       +                        int w = text_w + t->text_pad * 2 + t->border_width * 2;
       +                        int h = (int)menu->widget.ideal_h - t->pad * 2;
       +                        if (x >= cur_x && x <= cur_x + w && y >= cur_y && y <= cur_y + h) {
       +                                if (entry_rect_ret) {
       +                                        entry_rect_ret->x = cur_x;
       +                                        entry_rect_ret->y = cur_y;
       +                                        entry_rect_ret->w = w;
       +                                        entry_rect_ret->h = h;
       +                                }
       +                                return i;
       +                        }
       +                        cur_x += w - bw_sub + t->pad;
       +                }
       +        }
       +        return SIZE_MAX;
       +}
       +
       +/* FIXME: make sure timers are always destroyed when widget is destroyed */
       +static int
       +set_scroll_timer(ltk_menu *menu, int x, int y) {
       +        if (!ltk_collide_rect(menu->widget.rect, x, y))
       +                return 0;
       +        int t = 0, b = 0, l = 0,r = 0;
       +        struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
       +        int arrow_size = theme->arrow_size + theme->arrow_pad * 2;
       +        if (menu->widget.rect.w < (int)menu->widget.ideal_w) {
       +                if (x < menu->widget.rect.x + arrow_size)
       +                        l = 1;
       +                else if (x > menu->widget.rect.x + menu->widget.rect.w - arrow_size)
       +                        r = 1;
       +        }
       +        if (menu->widget.rect.h < (int)menu->widget.ideal_h) {
       +                if (y < menu->widget.rect.y + arrow_size)
       +                        t = 1;
       +                else if (y > menu->widget.rect.y + menu->widget.rect.h - arrow_size)
       +                        b = 1;
       +        }
       +        if (t == menu->scroll_top_hover &&
       +            b == menu->scroll_bottom_hover &&
       +            l == menu->scroll_left_hover &&
       +            r == menu->scroll_right_hover)
       +                return 0;
       +        stop_scrolling(menu);
       +        menu->scroll_top_hover = t;
       +        menu->scroll_bottom_hover = b;
       +        menu->scroll_left_hover = l;
       +        menu->scroll_right_hover = r;
       +        ltk_menu_scroll_callback(menu);
       +        menu->scroll_timer_id = ltk_register_timer(0, 300, &ltk_menu_scroll_callback, menu);
       +        return 1;
       +}
       +
       +static int
       +ltk_menu_mouse_release(ltk_widget *self, XEvent event) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        size_t idx = get_entry_at_point(menu, event.xbutton.x, event.xbutton.y, NULL);
       +        if (idx < menu->num_entries && idx == menu->pressed_entry) {
       +                ltk_window_unregister_all_popups(self->window);
       +                /* FIXME: give menu id and entry id */
       +                ltk_queue_event(self->window, LTK_EVENT_MENU, menu->entries[idx].id, "menu_entry_click");
       +        }
       +        if (menu->pressed_entry < menu->num_entries && idx < menu->num_entries)
       +                menu->active_entry = menu->pressed_entry;
       +        else if (idx < menu->num_entries)
       +                menu->active_entry = idx;
       +        menu->pressed_entry = SIZE_MAX;
       +        self->dirty = 1;
       +        return 1;
       +}
       +
       +static int
       +ltk_menu_mouse_press(ltk_widget *self, XEvent event) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        size_t idx;
       +        /* FIXME: configure scroll step */
       +        switch (event.xbutton.button) {
       +        case 1:
       +                idx = get_entry_at_point(menu, event.xbutton.x, event.xbutton.y, NULL);
       +                if (idx < menu->num_entries) {
       +                        menu->pressed_entry = idx;
       +                        self->dirty = 1;
       +                }
       +                break;
       +        case 4:
       +                ltk_menu_scroll(menu, 1, 0, 0, 0, 10);
       +                handle_hover(menu, event.xbutton.x, event.xbutton.y);
       +                break;
       +        case 5:
       +                ltk_menu_scroll(menu, 0, 1, 0, 0, 10);
       +                handle_hover(menu, event.xbutton.x, event.xbutton.y);
       +                break;
       +        case 6:
       +                ltk_menu_scroll(menu, 0, 0, 1, 0, 10);
       +                handle_hover(menu, event.xbutton.x, event.xbutton.y);
       +                break;
       +        case 7:
       +                ltk_menu_scroll(menu, 0, 0, 0, 1, 10);
       +                handle_hover(menu, event.xbutton.x, event.xbutton.y);
       +                break;
       +        default:
       +                break;
       +        }
       +        return 1;
       +}
       +
       +static void
       +ltk_menu_hide(ltk_widget *self) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        menu->active_entry = menu->pressed_entry = SIZE_MAX;
       +        if (menu->scroll_timer_id >= 0)
       +                ltk_unregister_timer(menu->scroll_timer_id);
       +        menu->scroll_bottom_hover = menu->scroll_top_hover = 0;
       +        menu->scroll_left_hover = menu->scroll_right_hover = 0;
       +        ltk_window_unregister_popup(self->window, self);
       +        ltk_window_invalidate_rect(self->window, self->rect);
       +}
       +
       +/* FIXME: don't require passing rect */
       +static void
       +popup_active_menu(ltk_menu *menu, ltk_rect r) {
       +        size_t idx = menu->active_entry;
       +        if (idx >= menu->num_entries)
       +                return;
       +        int win_w = menu->widget.window->rect.w;
       +        int win_h = menu->widget.window->rect.h;
       +        if (menu->entries[idx].submenu) {
       +                ltk_menu *submenu = menu->entries[idx].submenu;
       +                int ideal_w = submenu->widget.ideal_w + 2;
       +                int ideal_h = submenu->widget.ideal_h;
       +                int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
       +                if (menu->is_submenu) {
       +                        int space_left = menu->widget.rect.x;
       +                        int space_right = win_w - (menu->widget.rect.x + menu->widget.rect.w);
       +                        int x_right = menu->widget.rect.x + menu->widget.rect.w;
       +                        int x_left = menu->widget.rect.x - ideal_w;
       +                        if (menu->was_opened_left) {
       +                                if (x_left >= 0) {
       +                                        x_final = x_left;
       +                                        submenu->was_opened_left = 1;
       +                                } else if (space_right >= ideal_w) {
       +                                        x_final = x_right;
       +                                        submenu->was_opened_left = 0;
       +                                } else {
       +                                        x_final = 0;
       +                                        if (win_w < ideal_w)
       +                                                w_final = win_w;
       +                                        submenu->was_opened_left = 1;
       +                                }
       +                        } else {
       +                                if (space_right >= ideal_w) {
       +                                        x_final = x_right;
       +                                        submenu->was_opened_left = 0;
       +                                } else if (space_left >= ideal_w) {
       +                                        x_final = x_left;
       +                                        submenu->was_opened_left = 1;
       +                                } else {
       +                                        x_final = win_w - ideal_w;
       +                                        if (x_final < 0) {
       +                                                x_final = 0;
       +                                                w_final = win_w;
       +                                        }
       +                                        submenu->was_opened_left = 0;
       +                                }
       +                        }
       +                        /* subtract padding and border width so the actual entries are at the right position */
       +                        y_final = r.y - submenu_theme.pad - submenu_theme.menu_border_width;
       +                        if (y_final + ideal_h > win_h)
       +                                y_final = win_h - ideal_h;
       +                        if (y_final < 0) {
       +                                y_final = 0;
       +                                h_final = win_h;
       +                        }
       +                } else {
       +                        int space_top = menu->widget.rect.y;
       +                        int space_bottom = win_h - (menu->widget.rect.y + menu->widget.rect.h);
       +                        int y_top = menu->widget.rect.y - ideal_h;
       +                        int y_bottom = menu->widget.rect.y + menu->widget.rect.h;
       +                        if (space_top > space_bottom) {
       +                                y_final = y_top;
       +                                if (y_final < 0) {
       +                                        y_final = 0;
       +                                        h_final = menu->widget.rect.y;
       +                                }
       +                        } else {
       +                                y_final = y_bottom;
       +                                if (space_bottom < ideal_h)
       +                                        h_final = space_bottom;
       +                        }
       +                        /* FIXME: maybe threshold so there's always at least a part of
       +                           the menu contents shown (instead of maybe just a few pixels) */
       +                        /* pathological case where window is way too small */
       +                        if (h_final <= 0) {
       +                                y_final = 0;
       +                                h_final = win_h;
       +                        }
       +                        x_final = r.x;
       +                        if (x_final + ideal_w > win_w)
       +                                x_final = win_w - ideal_w;
       +                        if (x_final < 0) {
       +                                x_final = 0;
       +                                w_final = win_w;
       +                        }
       +                }
       +                /* reset everything just in case */
       +                submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
       +                submenu->active_entry = submenu->pressed_entry = SIZE_MAX;
       +                submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
       +                submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
       +                submenu->widget.rect.x = x_final;
       +                submenu->widget.rect.y = y_final;
       +                submenu->widget.rect.w = w_final;
       +                submenu->widget.rect.h = h_final;
       +                ltk_surface_cache_request_surface_size(submenu->widget.surface_key, w_final, h_final);
       +                submenu->widget.dirty = 1;
       +                submenu->widget.hidden = 0;
       +                ltk_window_register_popup(menu->widget.window, (ltk_widget *)submenu);
       +                ltk_window_invalidate_rect(submenu->widget.window, submenu->widget.rect);
       +        }
       +}
       +
       +static void
       +unpopup_active_entry(ltk_menu *menu) {
       +        if (menu->active_entry >= menu->num_entries)
       +                return;
       +        ltk_menu *cur_menu = menu->entries[menu->active_entry].submenu;
       +        menu->active_entry = SIZE_MAX;
       +        while (cur_menu) {
       +                ltk_menu *tmp = NULL;
       +                if (cur_menu->active_entry < cur_menu->num_entries)
       +                        tmp = cur_menu->entries[cur_menu->active_entry].submenu;
       +                ltk_menu_hide((ltk_widget *)cur_menu);
       +                cur_menu = tmp;
       +        }
       +}
       +
       +static void
       +handle_hover(ltk_menu *menu, int x, int y) {
       +        if (set_scroll_timer(menu, x, y) || menu->pressed_entry < menu->num_entries)
       +                return;
       +        ltk_rect r;
       +        size_t idx = get_entry_at_point(menu, x, y, &r);
       +        if (idx >= menu->num_entries)
       +                return;
       +        ltk_menu *cur_submenu = menu->active_entry < menu->num_entries ? menu->entries[menu->active_entry].submenu : NULL;
       +        if (idx != menu->active_entry) {
       +                unpopup_active_entry(menu);
       +                menu->active_entry = idx;
       +                menu->widget.dirty = 1;
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +                popup_active_menu(menu, r);
       +        } else if (cur_submenu && cur_submenu->widget.hidden) {
       +                popup_active_menu(menu, r);
       +        }
       +}
       +
       +static int
       +ltk_menu_motion_notify(ltk_widget *self, XEvent event) {
       +        handle_hover((ltk_menu *)self, event.xmotion.x, event.xmotion.y);
       +        return 1;
       +}
       +
       +static int
       +ltk_menu_mouse_enter(ltk_widget *self, XEvent event) {
       +        handle_hover((ltk_menu *)self, event.xbutton.x, event.xbutton.y);
       +        return 1;
       +}
       +
       +static int
       +ltk_menu_mouse_leave(ltk_widget *self, XEvent event) {
       +        (void)event;
       +        stop_scrolling((ltk_menu *)self);
       +        return 1;
       +}
       +
       +static ltk_menu *
       +ltk_menu_create(ltk_window *window, const char *id, int is_submenu) {
       +        ltk_menu *menu = ltk_malloc(sizeof(ltk_menu));
       +        menu->widget.ideal_w = menu_theme.pad;
       +        menu->widget.ideal_h = menu_theme.pad;
       +        ltk_fill_widget_defaults(&menu->widget, id, window, &vtable, menu->widget.ideal_w, menu->widget.ideal_h);
       +        menu->widget.dirty = 1;
       +
       +        menu->entries = NULL;
       +        menu->num_entries = menu->num_alloc = 0;
       +        menu->pressed_entry = menu->active_entry = SIZE_MAX;
       +        menu->x_scroll_offset = menu->y_scroll_offset = 0;
       +        menu->is_submenu = is_submenu;
       +        menu->was_opened_left = 0;
       +        menu->scroll_timer_id = -1;
       +        menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
       +        menu->scroll_left_hover = menu->scroll_right_hover = 0;
       +        /* FIXME: hide widget by default so recalc doesn't cause
       +           unnecessary redrawing */
       +        recalc_menu_size(menu);
       +
       +        return menu;
       +}
       +
       +static ltk_menuentry *
       +insert_entry(ltk_menu *menu, size_t idx) {
       +        if (idx > menu->num_entries)
       +                return NULL;
       +        if (menu->num_entries == menu->num_alloc) {
       +                menu->num_alloc = ideal_array_size(menu->num_alloc, menu->num_entries + 1);
       +                menu->entries = ltk_reallocarray(menu->entries, menu->num_alloc, sizeof(ltk_menuentry));
       +        }
       +        memmove(
       +            menu->entries + idx + 1,
       +            menu->entries + idx,
       +            sizeof(ltk_menuentry) * (menu->num_entries - idx)
       +        );
       +        if (menu->active_entry >= idx && menu->active_entry < menu->num_entries)
       +                menu->active_entry++;
       +        if (menu->pressed_entry >= idx && menu->pressed_entry < menu->num_entries)
       +                menu->pressed_entry++;
       +        menu->num_entries++;
       +        return &menu->entries[idx];
       +}
       +
       +/* FIXME: handle child_size_change - what if something added while menu shown?
       +   -> I guess the scroll arrows will just be added when that's the case - it's
       +      kind of pointless to spend time implementing the logic for recalculating
       +      all submenu positions and sizes when it's such a corner case */
       +static void
       +recalc_menu_size(ltk_menu *menu) {
       +        struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
       +        menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->menu_border_width * 2;
       +        ltk_menuentry *e;
       +        int text_w, text_h, bw;
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                e = &menu->entries[i];
       +                ltk_text_line_get_size(e->text, &text_w, &text_h);
       +                bw = t->border_width * 2;
       +                if (t->compress_borders && i != 0)
       +                        bw = t->border_width;
       +                if (menu->is_submenu) {
       +                        int extra_size = e->submenu ? t->arrow_pad * 2 + t->arrow_size : 0;
       +                        menu->widget.ideal_w =
       +                            MAX(text_w + extra_size + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_w);
       +                        menu->widget.ideal_h += MAX(text_h + t->text_pad * 2, extra_size) + bw + t->pad;
       +                } else {
       +                        menu->widget.ideal_h =
       +                            MAX(text_h + (t->pad + t->text_pad + t->border_width + t->menu_border_width) * 2, (int)menu->widget.ideal_h);
       +                        menu->widget.ideal_w += text_w + t->text_pad * 2 + bw + t->pad;
       +                }
       +        }
       +        if (!menu->widget.hidden && menu->widget.parent && menu->widget.parent->vtable->child_size_change) {
       +                menu->widget.parent->vtable->child_size_change(menu->widget.parent, (ltk_widget *)menu);
       +        }
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +}
       +
       +static ltk_menuentry *
       +ltk_menu_insert_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
       +        if (submenu && submenu->widget.parent) {
       +                *errstr = "Submenu already part of other menu.\n";
       +                return NULL;
       +        }
       +        ltk_menuentry *e = insert_entry(menu, idx);
       +        if (!e) {
       +                *errstr = "Unable to insert menu entry at given index.\n";
       +                return NULL;
       +        }
       +        e->id = ltk_strdup(id);
       +        ltk_window *w = menu->widget.window;
       +        /* FIXME: pass const text */
       +        e->text = ltk_text_line_create(w->text_context, w->theme.font_size, (char *)text, 0, -1);
       +        e->submenu = submenu;
       +        if (submenu)
       +                submenu->widget.parent = (ltk_widget *)menu;
       +        e->disabled = 0;
       +        recalc_menu_size(menu);
       +        menu->widget.dirty = 1;
       +        return e;
       +}
       +
       +static ltk_menuentry *
       +ltk_menu_add_entry(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
       +        return ltk_menu_insert_entry(menu, id, text, submenu, menu->num_entries, errstr);
       +}
       +
       +/* FIXME: maybe allow any menu and just change is_submenu (also need to recalculate size then) */
       +static ltk_menuentry *
       +ltk_menu_insert_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, size_t idx, char **errstr) {
       +        if (!submenu->is_submenu) {
       +                *errstr = "Not a submenu.\n";
       +                return NULL;
       +        }
       +        return ltk_menu_insert_entry(menu, id, text, submenu, idx, errstr);
       +}
       +
       +static ltk_menuentry *
       +ltk_menu_add_submenu(ltk_menu *menu, const char *id, const char *text, ltk_menu *submenu, char **errstr) {
       +        return ltk_menu_insert_submenu(menu, id, text, submenu, menu->num_entries, errstr);
       +}
       +
       +static void
       +shrink_entries(ltk_menu *menu) {
       +        size_t new_alloc = ideal_array_size(menu->num_alloc, menu->num_entries);
       +        if (new_alloc != menu->num_alloc) {
       +                menu->entries = ltk_reallocarray(menu->entries, new_alloc, sizeof(ltk_menuentry));
       +                menu->num_alloc = new_alloc;
       +        }
       +}
       +
       +static int
       +ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, int shallow, char **errstr) {
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry index.\n";
       +                return 1;
       +        }
       +        ltk_menuentry *e = &menu->entries[idx];
       +        ltk_free(e->id);
       +        ltk_text_line_destroy(e->text);
       +        if (e->submenu) {
       +                e->submenu->widget.parent = NULL;
       +                if (!shallow)
       +                        ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
       +        }
       +        memmove(
       +            menu->entries + idx,
       +            menu->entries + idx + 1,
       +            sizeof(ltk_menuentry) * (menu->num_entries - idx - 1)
       +        );
       +        menu->num_entries--;
       +        shrink_entries(menu);
       +        recalc_menu_size(menu);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, int shallow, char **errstr) {
       +        size_t idx = get_entry_with_id(menu, id);
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry id.\n";
       +                return 1;
       +        }
       +        ltk_menu_remove_entry_index(menu, idx, shallow, errstr);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_remove_all_entries(ltk_menu *menu, int shallow, char **errstr) {
       +        (void)errstr; /* FIXME: why is errstr given at all? */
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                ltk_menuentry *e = &menu->entries[i];
       +                ltk_free(e->id);
       +                ltk_text_line_destroy(e->text);
       +                if (e->submenu) {
       +                        e->submenu->widget.parent = NULL;
       +                        if (!shallow)
       +                                ltk_menu_destroy((ltk_widget *)e->submenu, shallow);
       +                }
       +        }
       +        menu->num_entries = menu->num_alloc = 0;
       +        ltk_free(menu->entries);
       +        menu->entries = NULL;
       +        recalc_menu_size(menu);
       +        return 0;
       +}
       +
       +/* FIXME: how to get rid of duplicate IDs? */
       +
       +static size_t
       +get_entry_with_id(ltk_menu *menu, const char *id) {
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                if (!strcmp(id, menu->entries[i].id))
       +                        return i;
       +        }
       +        return SIZE_MAX;
       +}
       +
       +/* FIXME: unregister from window popups? */
       +static int
       +ltk_menu_detach_submenu_from_entry_id(ltk_menu *menu, const char *id, char **errstr) {
       +        size_t idx = get_entry_with_id(menu, id);
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry id.\n";
       +                return 1;
       +        }
       +        /* FIXME: error if submenu already NULL? */
       +        menu->entries[idx].submenu = NULL;
       +        recalc_menu_size(menu);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_detach_submenu_from_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry index.\n";
       +                return 1;
       +        }
       +        menu->entries[idx].submenu = NULL;
       +        recalc_menu_size(menu);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_disable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry index.\n";
       +                return 1;
       +        }
       +        menu->entries[idx].disabled = 1;
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_disable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
       +        size_t idx = get_entry_with_id(menu, id);
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry id.\n";
       +                return 1;
       +        }
       +        menu->entries[idx].disabled = 1;
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_disable_all_entries(ltk_menu *menu, char **errstr) {
       +        (void)errstr;
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                menu->entries[i].disabled = 1;
       +        }
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_enable_entry_index(ltk_menu *menu, size_t idx, char **errstr) {
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry index.\n";
       +                return 1;
       +        }
       +        menu->entries[idx].disabled = 0;
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_enable_entry_id(ltk_menu *menu, const char *id, char **errstr) {
       +        size_t idx = get_entry_with_id(menu, id);
       +        if (idx >= menu->num_entries) {
       +                *errstr = "Invalid menu entry id.\n";
       +                return 1;
       +        }
       +        menu->entries[idx].disabled = 0;
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static int
       +ltk_menu_enable_all_entries(ltk_menu *menu, char **errstr) {
       +        (void)errstr;
       +        for (size_t i = 0; i < menu->num_entries; i++) {
       +                menu->entries[i].disabled = 0;
       +        }
       +        menu->widget.dirty = 1;
       +        if (!menu->widget.hidden)
       +                ltk_window_invalidate_rect(menu->widget.window, menu->widget.rect);
       +        return 0;
       +}
       +
       +static void
       +ltk_menu_destroy(ltk_widget *self, int shallow) {
       +        ltk_menu *menu = (ltk_menu *)self;
       +        char *errstr;
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
       +        if (!menu) {
       +                ltk_warn("Tried to destroy NULL menu.\n");
       +                return;
       +        }
       +        /* FIXME: this should be generic part of widget */
       +        ltk_surface_cache_release_key(self->surface_key);
       +        if (menu->scroll_timer_id >= 0)
       +                ltk_unregister_timer(menu->scroll_timer_id);
       +        ltk_menu_remove_all_entries(menu, shallow, NULL);
       +        ltk_window_unregister_popup(self->window, self);
       +        /* FIXME: what to do on error here? */
       +        /* FIXME: maybe unregister popup in ltk_remove_widget? */
       +        ltk_remove_widget(self->id);
       +        ltk_free(self->id);
       +        ltk_free(menu);
       +}
       +
       +/* FIXME: simplify command handling to avoid all this boilerplate */
       +/* TODO: get-index-for-id */
       +
       +/* [sub]menu <menu id> create */
       +static int
       +ltk_menu_cmd_create(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        ltk_menu *menu;
       +        if (num_tokens != 3) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        if (!ltk_widget_id_free(tokens[1])) {
       +                *errstr = "Widget ID already taken.\n";
       +                return 1;
       +        }
       +        if (!strcmp(tokens[0], "menu")) {
       +                menu = ltk_menu_create(window, tokens[1], 0);
       +        } else {
       +                menu = ltk_menu_create(window, tokens[1], 1);
       +        }
       +        ltk_set_widget((ltk_widget *)menu, tokens[1]);
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> insert-entry <entry id> <entry text> <index> */
       +static int
       +ltk_menu_cmd_insert_entry(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        const char *errstr_num;
       +        if (num_tokens != 6) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        /* FIXME: actually use this errstr */
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[5], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +        if (!ltk_menu_insert_entry(menu, tokens[3], tokens[4], NULL, idx, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> add-entry <entry id> <entry text> */
       +static int
       +ltk_menu_cmd_add_entry(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 5) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        if (!ltk_menu_add_entry(menu, tokens[3], tokens[4], NULL, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> insert-submenu <entry id> <entry text> <submenu id> <index> */
       +static int
       +ltk_menu_cmd_insert_submenu(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu, *submenu;
       +        const char *errstr_num;
       +        if (num_tokens != 7) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
       +        if (!menu || !submenu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[6], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +        if (!ltk_menu_insert_submenu(menu, tokens[3], tokens[4], submenu, idx, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> add-submenu <entry id> <entry text> <submenu id> */
       +static int
       +ltk_menu_cmd_add_submenu(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu, *submenu;
       +        if (num_tokens != 6) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        submenu = (ltk_menu *)ltk_get_widget(tokens[5], LTK_MENU, errstr);
       +        if (!menu || !submenu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        if (!ltk_menu_add_submenu(menu, tokens[3], tokens[4], submenu, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-entry-index <entry index> [shallow|deep] */
       +static int
       +ltk_menu_cmd_remove_entry_index(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        const char *errstr_num;
       +        if (num_tokens != 4 && num_tokens != 5) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +        int shallow = 1;
       +        if (num_tokens == 5) {
       +                if (!strcmp(tokens[4], "shallow")) {
       +                        /* NOP */
       +                } else if (!strcmp(tokens[4], "deep")) {
       +                        shallow = 0;
       +                } else {
       +                        *errstr = "Invalid shallow specifier.\n";
       +                        return 1;
       +                }
       +        }
       +        if (!ltk_menu_remove_entry_index(menu, idx, shallow, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-entry-id <entry id> [shallow|deep] */
       +static int
       +ltk_menu_cmd_remove_entry_id(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 4 && num_tokens != 5) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        int shallow = 1;
       +        if (num_tokens == 5) {
       +                if (!strcmp(tokens[4], "shallow")) {
       +                        /* NOP */
       +                } else if (!strcmp(tokens[4], "deep")) {
       +                        shallow = 0;
       +                } else {
       +                        *errstr = "Invalid shallow specifier.\n";
       +                        return 1;
       +                }
       +        }
       +        if (!ltk_menu_remove_entry_id(menu, tokens[3], shallow, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> remove-all-entries [shallow|deep] */
       +static int
       +ltk_menu_cmd_remove_all_entries(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 3 && num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        int shallow = 1;
       +        if (num_tokens == 4) {
       +                if (!strcmp(tokens[3], "shallow")) {
       +                        /* NOP */
       +                } else if (!strcmp(tokens[3], "deep")) {
       +                        shallow = 0;
       +                } else {
       +                        *errstr = "Invalid shallow specifier.\n";
       +                        return 1;
       +                }
       +        }
       +        if (!ltk_menu_remove_all_entries(menu, shallow, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> detach-submenu-from-entry-id <entry id> */
       +static int
       +ltk_menu_cmd_detach_submenu_from_entry_id(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_detach_submenu_from_entry_id(menu, tokens[3], errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> detach-submenu-from-entry-index <entry index> */
       +static int
       +ltk_menu_cmd_detach_submenu_from_entry_index(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        const char *errstr_num;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_detach_submenu_from_entry_index(menu, idx, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> enable-entry-index <entry index> */
       +static int
       +ltk_menu_cmd_enable_entry_index(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        const char *errstr_num;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_enable_entry_index(menu, idx, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> enable-entry-id <entry id> */
       +static int
       +ltk_menu_cmd_enable_entry_id(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_enable_entry_id(menu, tokens[3], errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> enable-all-entries */
       +static int
       +ltk_menu_cmd_enable_all_entries(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 3) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_enable_all_entries(menu, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> disable-entry-index <entry index> */
       +static int
       +ltk_menu_cmd_disable_entry_index(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        const char *errstr_num;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +        size_t idx = (size_t)ltk_strtonum(tokens[3], 0, (long long)menu->num_entries, &errstr_num);
       +        if (errstr_num) {
       +                *errstr = "Invalid index.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_disable_entry_index(menu, idx, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> disable-entry-id <entry id> */
       +static int
       +ltk_menu_cmd_disable_entry_id(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 4) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_disable_entry_id(menu, tokens[3], errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* menu <menu id> disable-all-entries */
       +static int
       +ltk_menu_cmd_disable_all_entries(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        (void)window;
       +        ltk_menu *menu;
       +        if (num_tokens != 3) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        menu = (ltk_menu *)ltk_get_widget(tokens[1], LTK_MENU, errstr);
       +        if (!menu) {
       +                *errstr = "Invalid widget ID.\n";
       +                return 1;
       +        }
       +
       +        if (!ltk_menu_disable_all_entries(menu, errstr))
       +                return 1;
       +
       +        return 0;
       +}
       +
       +/* FIXME: binary search for command handler */
       +/* FIXME: distinguish between menu/submenu in commands other than create? */
       +/* menu <menu id> <command> ... */
       +int
       +ltk_menu_cmd(
       +    ltk_window *window,
       +    char **tokens,
       +    size_t num_tokens,
       +    char **errstr) {
       +        if (num_tokens < 3) {
       +                *errstr = "Invalid number of arguments.\n";
       +                return 1;
       +        }
       +        if (strcmp(tokens[2], "create") == 0) {
       +                return ltk_menu_cmd_create(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "insert-entry") == 0) {
       +                return ltk_menu_cmd_insert_entry(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "add-entry") == 0) {
       +                return ltk_menu_cmd_add_entry(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "insert-submenu") == 0) {
       +                return ltk_menu_cmd_insert_submenu(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "add-submenu") == 0) {
       +                return ltk_menu_cmd_add_submenu(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "remove-entry-index") == 0) {
       +                return ltk_menu_cmd_remove_entry_index(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "remove-entry-id") == 0) {
       +                return ltk_menu_cmd_remove_entry_id(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "remove-all-entries") == 0) {
       +                return ltk_menu_cmd_remove_all_entries(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "detach-submenu-from-entry-id") == 0) {
       +                return ltk_menu_cmd_detach_submenu_from_entry_id(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "detach-submenu-from-entry-index") == 0) {
       +                return ltk_menu_cmd_detach_submenu_from_entry_index(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "disable-entry-index") == 0) {
       +                return ltk_menu_cmd_disable_entry_index(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "disable-entry-id") == 0) {
       +                return ltk_menu_cmd_disable_entry_id(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "disable-all-entries") == 0) {
       +                return ltk_menu_cmd_disable_all_entries(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "enable-entry-index") == 0) {
       +                return ltk_menu_cmd_enable_entry_index(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "enable-entry-id") == 0) {
       +                return ltk_menu_cmd_enable_entry_id(window, tokens, num_tokens, errstr);
       +        } else if (strcmp(tokens[2], "enable-all-entries") == 0) {
       +                return ltk_menu_cmd_enable_all_entries(window, tokens, num_tokens, errstr);
       +        } else {
       +                *errstr = "Invalid command.\n";
       +                return 1;
       +        }
       +
       +        return 0;
       +}
   DIR diff --git a/src/menu.h b/src/menu.h
       t@@ -0,0 +1,64 @@
       +/*
       + * Copyright (c) 2022 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_MENU_H_
       +#define _LTK_MENU_H_
       +
       +#include "ltk.h"
       +#include "text.h"
       +#include "widget.h"
       +
       +/* TODO: implement scrolling */
       +
       +typedef struct ltk_menuentry ltk_menuentry;
       +
       +typedef struct {
       +        ltk_widget widget;
       +        ltk_menuentry *entries;
       +        size_t num_entries;
       +        size_t num_alloc;
       +        size_t pressed_entry;
       +        size_t active_entry;
       +        double x_scroll_offset;
       +        double y_scroll_offset;
       +        int scroll_timer_id;
       +        char is_submenu;
       +        char was_opened_left;
       +        char scroll_top_hover;
       +        char scroll_bottom_hover;
       +        char scroll_left_hover;
       +        char scroll_right_hover;
       +} ltk_menu;
       +
       +struct ltk_menuentry {
       +        char *id;
       +        ltk_text_line *text;
       +        ltk_menu *submenu;
       +        int disabled;
       +};
       +
       +void ltk_menu_setup_theme_defaults(ltk_window *window);
       +void ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value);
       +void ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *value);
       +
       +int ltk_menu_cmd(
       +        ltk_window *window,
       +        char **tokens,
       +        size_t num_tokens,
       +        char **errstr
       +);
       +
       +#endif /* _LTK_MENU_H_ */
   DIR diff --git a/src/scrollbar.c b/src/scrollbar.c
       t@@ -1,3 +1,4 @@
       +/* FIXME: make scrollbar a "real" widget that is also in widget hash */
        /*
         * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
         *
       t@@ -31,6 +32,8 @@
        #include "util.h"
        #include "scrollbar.h"
        
       +#define MAX_SCROLLBAR_WIDTH 100 /* completely arbitrary */
       +
        static void ltk_scrollbar_draw(ltk_widget *self, ltk_rect clip);
        static int ltk_scrollbar_mouse_press(ltk_widget *self, XEvent event);
        static int ltk_scrollbar_motion_notify(ltk_widget *self, XEvent event);
       t@@ -59,44 +62,43 @@ static struct {
        void
        ltk_scrollbar_setup_theme_defaults(ltk_window *window) {
                theme.size = 15;
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#000000", &theme.bg_normal);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#555555", &theme.bg_disabled);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#113355", &theme.fg_normal);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#738194", &theme.fg_active);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#113355", &theme.fg_pressed);
       -        ltk_color_create(window->dpy, window->screen, window->cm,
       -            "#292929", &theme.fg_disabled);
       +        /* FIXME: error checking - but if these fail, there is probably a bigger
       +           problem, so it might be best to just die completely */
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#000000", &theme.bg_normal);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#555555", &theme.bg_disabled);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fg_normal);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#738194", &theme.fg_active);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#113355", &theme.fg_pressed);
       +        ltk_color_create(window->dpy, window->screen, window->cm, "#292929", &theme.fg_disabled);
        }
        
        void
        ltk_scrollbar_ini_handler(ltk_window *window, const char *prop, const char *value) {
       +        const char *errstr;
                if (strcmp(prop, "size") == 0) {
       -                theme.size = atoi(value); /* FIXME: proper strtonum */
       +                theme.size = ltk_strtonum(value, 1, MAX_SCROLLBAR_WIDTH, &errstr);
       +                if (errstr)
       +                        ltk_warn("Invalid scrollbar size '%s': %s.\n", value, errstr);
                } else if (strcmp(prop, "bg") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.bg_normal);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_normal))
       +                        ltk_warn("Error setting scrollbar background color to '%s'.\n", value);
                } else if (strcmp(prop, "bg_disabled") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.bg_disabled);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.bg_disabled))
       +                        ltk_warn("Error setting scrollbar disabled background color to '%s'.\n", value);
                } else if (strcmp(prop, "fg") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fg_normal);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_normal))
       +                        ltk_warn("Error setting scrollbar foreground color to '%s'.\n", value);
                } else if (strcmp(prop, "fg_active") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fg_active);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_active))
       +                        ltk_warn("Error setting scrollbar active foreground color to '%s'.\n", value);
                } else if (strcmp(prop, "fg_pressed") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fg_pressed);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_pressed))
       +                        ltk_warn("Error setting scrollbar pressed foreground color to '%s'.\n", value);
                } else if (strcmp(prop, "fg_disabled") == 0) {
       -                ltk_color_create(window->dpy, window->screen, window->cm,
       -                    value, &theme.fg_disabled);
       +                if (ltk_color_create(window->dpy, window->screen, window->cm, value, &theme.fg_disabled))
       +                        ltk_warn("Error setting scrollbar disabled foreground color to '%s'.\n", value);
                } else {
       -                ltk_warn("Unknown property \"%s\" for scrollbar style.\n", prop);
       +                ltk_warn("Unknown property '%s' for scrollbar style.\n", prop);
                }
        }
        
       t@@ -262,7 +264,12 @@ ltk_scrollbar_create(ltk_window *window, ltk_orientation orient, void (*callback
        static void
        ltk_scrollbar_destroy(ltk_widget *self, int shallow) {
                (void)shallow;
       +        char *errstr;
       +        if (self->parent && self->parent->vtable->remove_child) {
       +                self->parent->vtable->remove_child(
       +                    self->window, self, self->parent, &errstr
       +                );
       +        }
                ltk_surface_cache_release_key(self->surface_key);
       -        ltk_scrollbar *scrollbar = (ltk_scrollbar *)self;
       -        ltk_free(scrollbar);
       +        ltk_free(self);
        }
   DIR diff --git a/src/strtonum.c b/src/strtonum.c
       t@@ -1,4 +1,6 @@
       -/*        $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $        */
       +/* Note: Taken from OpenBSD:
       + * $OpenBSD: strtonum.c,v 1.8 2015/09/13 08:31:48 guenther Exp $
       + */
        
        /*
         * Copyright (c) 2004 Ted Unangst and Todd Miller
       t@@ -17,8 +19,6 @@
         * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
         */
        
       -/* #ifndef __OpenBSD__ */
       -
        #include <errno.h>
        #include <limits.h>
        #include <stdlib.h>
       t@@ -28,9 +28,9 @@
        #define        TOOLARGE        3
        
        long long
       -strtonum(const char *numstr, long long minval, long long maxval,
       -    const char **errstrp)
       -{
       +ltk_strtonum(
       +    const char *numstr, long long minval,
       +    long long maxval, const char **errstrp) {
                long long ll = 0;
                int error = 0;
                char *ep;
       t@@ -65,7 +65,3 @@ strtonum(const char *numstr, long long minval, long long maxval,
        
                return (ll);
        }
       -/* FIXME: What does this do? - lumidify */
       -/* DEF_WEAK(strtonum); */
       -
       -/* #endif */
   DIR diff --git a/src/text.h b/src/text.h
       t@@ -36,6 +36,11 @@ void ltk_text_line_get_size(ltk_text_line *tl, int *w, int *h);
        void ltk_text_line_destroy(ltk_text_line *tl);
        
        /* Draw the entire line to a surface. */
       +/* FIXME: Some widgets rely on this to not fail when negative coordinates are given or
       +   the text goes outside of the surface boundaries - in the stb backend, this is taken
       +   into account and the pango-xft backend doesn't *seem* to have any problems with it,
       +   but I don't know if that's guaranteed. Proper clipping would be better, but Pango
       +   can't do that. */
        void ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y);
        
        /* Get the smallest rectangle of the line that can be drawn while covering 'clip'.
   DIR diff --git a/src/text_pango.c b/src/text_pango.c
       t@@ -48,20 +48,23 @@ struct ltk_text_context {
                char *default_font;
        };
        
       -void
       -ltk_text_context_create(ltk_window *window, const char *default_font) {
       +ltk_text_context *
       +ltk_text_context_create(ltk_window *window, char *default_font) {
                ltk_text_context *ctx = ltk_malloc(sizeof(ltk_text_context));
                ctx->window = window;
                ctx->fontmap = pango_xft_get_font_map(window->dpy, window->screen);
                ctx->context = pango_font_map_create_context(ctx->fontmap);
                ctx->default_font = ltk_strdup(default_font);
       +        return ctx;
        }
        
        void
        ltk_text_context_destroy(ltk_text_context *ctx) {
                ltk_free(ctx->default_font);
       +        /* FIXME: if both are unref'd, there is a segfault - what is
       +           the normal thing to do here? */
                g_object_unref(ctx->fontmap);
       -        g_object_unref(ctx->context);
       +        /*g_object_unref(ctx->context);*/
                ltk_free(ctx);
        }
        
   DIR diff --git a/src/text_stb.c b/src/text_stb.c
       t@@ -1,3 +1,4 @@
       +/* FIXME: max cache size for glyphs */
        /*
         * Copyright (c) 2017, 2018, 2020, 2022 lumidify <nobody@lumidify.org>
         *
       t@@ -99,7 +100,6 @@ struct ltk_text_context {
                ltk_font **fonts;
                int num_fonts;
                int fonts_bufsize;
       -        FcPattern *fcpattern;
                ltk_font *default_font;
                uint16_t font_id_cur;
        };
       t@@ -208,7 +208,6 @@ ltk_text_context_create(ltk_window *window, char *default_font) {
                ctx->fonts = ltk_malloc(sizeof(ltk_font *));
                ctx->num_fonts = 0;
                ctx->fonts_bufsize = 1;
       -        ctx->fcpattern = NULL;
                ltk_load_default_font(ctx, default_font);
                ctx->font_id_cur = 1;
                return ctx;
       t@@ -216,10 +215,10 @@ ltk_text_context_create(ltk_window *window, char *default_font) {
        
        void
        ltk_text_context_destroy(ltk_text_context *ctx) {
       -        /* FIXME: destroy fcpattern */
                for (int i = 0; i < ctx->num_fonts; i++) {
                        ltk_destroy_font(ctx->fonts[i]);
                }
       +        ltk_free(ctx->fonts);
                if (!ctx->glyph_cache) return;
                for (khint_t k = kh_begin(ctx->glyph_cache); k != kh_end(ctx->glyph_cache); k++) {
                        if (kh_exist(ctx->glyph_cache, k)) {
       t@@ -227,6 +226,7 @@ ltk_text_context_destroy(ltk_text_context *ctx) {
                        }
                }
                kh_destroy(glyphcache, ctx->glyph_cache);
       +        ltk_free(ctx);
        }
        
        static ltk_glyph_info *
       t@@ -302,18 +302,18 @@ ltk_destroy_glyph_cache(khash_t(glyphinfo) *cache) {
        
        static void
        ltk_load_default_font(ltk_text_context *ctx, char *name) {
       -        FcPattern *match;
       +        FcPattern *match, *pat;
                FcResult result;
                char *file;
                int index;
        
                /* FIXME: Get rid of this stupid cast somehow */
       -        ctx->fcpattern = FcNameParse((const FcChar8 *)name);
       -        /*ctx->fcpattern = FcPatternCreate();*/
       -        FcPatternAddString(ctx->fcpattern, FC_FONTFORMAT, (const FcChar8 *)"truetype");
       -        FcConfigSubstitute(NULL, ctx->fcpattern, FcMatchPattern);
       -        FcDefaultSubstitute(ctx->fcpattern);
       -        match = FcFontMatch(NULL, ctx->fcpattern, &result);
       +        pat = FcNameParse((const FcChar8 *)name);
       +        FcPatternAddString(pat, FC_FONTFORMAT, (const FcChar8 *)"truetype");
       +        FcConfigSubstitute(NULL, pat, FcMatchPattern);
       +        FcDefaultSubstitute(pat);
       +        /* FIXME look at result */
       +        match = FcFontMatch(NULL, pat, &result);
        
                FcPatternGetString(match, FC_FILE, 0, (FcChar8 **) &file);
                FcPatternGetInteger(match, FC_INDEX, 0, &index);
       t@@ -321,6 +321,7 @@ ltk_load_default_font(ltk_text_context *ctx, char *name) {
                ctx->default_font = ltk_get_font(ctx, file, index);
        
                FcPatternDestroy(match);
       +        FcPatternDestroy(pat);
        }
        
        static ltk_font *
       t@@ -331,6 +332,7 @@ ltk_create_font(char *path, uint16_t id, int index) {
                if (!contents)
                        ltk_fatal_errno("Unable to read font file %s\n", path);
                int offset = stbtt_GetFontOffsetForIndex((unsigned char *)contents, index);
       +        font->info.data = NULL;
                if (!stbtt_InitFont(&font->info, (unsigned char *)contents, offset))
                        ltk_fatal("Failed to load font %s\n", path);
                font->id = id;
       t@@ -343,6 +345,7 @@ ltk_create_font(char *path, uint16_t id, int index) {
        
        static void
        ltk_destroy_font(ltk_font *font) {
       +        ltk_free(font->path);
                ltk_free(font->info.data);
                ltk_free(font);
        }
       t@@ -405,6 +408,7 @@ ltk_text_to_glyphs(ltk_text_context *ctx, ltk_glyph *glyphs, int num_glyphs, cha
                                /* Question: Why does this not work with FcPatternDuplicate? */
                                FcPattern *pat = FcPatternCreate();
                                FcPattern *match;
       +                        /* FIXME: use result */
                                FcResult result;
                                FcPatternAddBool(pat, FC_SCALABLE, 1);
                                FcConfigSubstitute(NULL, pat, FcMatchPattern);
       t@@ -506,6 +510,8 @@ ltk_text_line_draw_glyph(ltk_glyph *glyph, int x, int y, XImage *img, XColor fg)
                int b;
                for (int i = 0; i < glyph->info->h; i++) {
                        for (int j = 0; j < glyph->info->w; j++) {
       +                        /* FIXME: this check could be moved to the for loop condition and initialization */
       +                        /* -> not sure it that would *possibly* be a tiny bit faster */
                                if (y + i >= img->height || x + j >= img->width ||
                                    y + i < 0 || x + i < 0)
                                        continue;
       t@@ -569,8 +575,23 @@ void
        ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, int y) {
                if (tl->dirty)
                        ltk_text_line_break_lines(tl);
       +        int xoff = 0, yoff = 0;
       +        if (x < 0) {
       +                xoff = x;
       +                x = 0;
       +        }
       +        if (y < 0) {
       +                yoff = y;
       +                y = 0;
       +        }
       +        int s_w, s_h;
       +        ltk_surface_get_size(s, &s_w, &s_h);
       +        int w = x + xoff + tl->w > s_w ? s_w - x : xoff + tl->w;
       +        int h = y + yoff + tl->h > s_h ? s_h - y : yoff + tl->h;
       +        if (w <= 0 || h <= 0)
       +                return;
                Drawable d = ltk_surface_get_drawable(s);
       -        XImage *img = XGetImage(tl->ctx->window->dpy, d, x, y, tl->w, tl->h, 0xFFFFFF, ZPixmap);
       +        XImage *img = XGetImage(tl->ctx->window->dpy, d, x, y, w, h, 0xFFFFFF, ZPixmap);
        
                int last_break = 0;
                for (int i = 0; i < tl->lines; i++) {
       t@@ -580,13 +601,13 @@ ltk_text_line_draw(ltk_text_line *tl, ltk_surface *s, ltk_color *color, int x, i
                        else
                                next_break = tl->glyph_len;
                        for (int j = last_break; j < next_break; j++) {
       -                        int x = tl->glyphs[j].x - tl->glyphs[last_break].x;
       -                        int y = tl->glyphs[j].y - tl->y_min + tl->line_h * i;
       -                        ltk_text_line_draw_glyph(&tl->glyphs[j], x, y, img, color->xcolor);
       +                        int g_x = tl->glyphs[j].x - tl->glyphs[last_break].x + xoff;
       +                        int g_y = tl->glyphs[j].y - tl->y_min + tl->line_h * i + yoff;
       +                        ltk_text_line_draw_glyph(&tl->glyphs[j], g_x, g_y, img, color->xcolor);
                        }
                        last_break = next_break;
                }
       -        XPutImage(tl->ctx->window->dpy, d, tl->ctx->window->gc, img, 0, 0, x, y, tl->w, tl->h);
       +        XPutImage(tl->ctx->window->dpy, d, tl->ctx->window->gc, img, 0, 0, x, y, w, h);
                XDestroyImage(img);
        }
        
   DIR diff --git a/src/util.h b/src/util.h
       t@@ -16,11 +16,10 @@
        
        /* Requires: <stdarg.h> */
        
       -/* #ifndef __OpenBSD__ */
       -long long
       -strtonum(const char *numstr, long long minval, long long maxval,
       -    const char **errstrp);
       -/* #endif */
       +long long ltk_strtonum(
       +    const char *numstr, long long minval,
       +    long long maxval, const char **errstrp
       +);
        
        char *ltk_read_file(const char *path, unsigned long *len);
        int ltk_grow_string(char **str, int *alloc_size, int needed);
   DIR diff --git a/src/widget.c b/src/widget.c
       t@@ -1,5 +1,10 @@
        /* FIXME: store coordinates relative to parent widget */
        /* FIXME: Destroy function for widget to destroy pixmap! */
       +/* FIXME/NOTE: maybe it would be better to do some sort of
       +   inheritance where the generic widget destroy function is
       +   called before the specific function for each widget type
       +   so each widget doesn't have to manually remove itself from
       +   its parent */
        /*
         * Copyright (c) 2021, 2022 lumidify <nobody@lumidify.org>
         *
       t@@ -35,18 +40,25 @@ static void ltk_destroy_widget_hash(void);
        
        KHASH_MAP_INIT_STR(widget, ltk_widget *)
        static khash_t(widget) *widget_hash = NULL;
       +/* Hack to make ltk_destroy_widget_hash work */
       +/* FIXME: any better way to do this? */
       +static int hash_locked = 0;
        
        static void
        ltk_destroy_widget_hash(void) {
       +        hash_locked = 1;
                khint_t k;
                ltk_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));
                                ptr->vtable->destroy(ptr, 1);
                        }
                }
                kh_destroy(widget, widget_hash);
       +        widget_hash = NULL;
       +        hash_locked = 0;
        }
        
        void
       t@@ -90,6 +102,7 @@ ltk_fill_widget_defaults(ltk_widget *widget, const char *id, ltk_window *window,
                widget->column_span = 0;
                widget->sticky = 0;
                widget->dirty = 1;
       +        widget->hidden = 0;
        }
        
        /* FIXME: Maybe pass the new width as arg here?
       t@@ -137,15 +150,28 @@ ltk_widget_mouse_release_event(ltk_widget *widget, XEvent event) {
        
        void
        ltk_widget_motion_notify_event(ltk_widget *widget, XEvent event) {
       -        if (!widget || widget->state == LTK_DISABLED)
       -                return;
                /* FIXME: THIS WHOLE STATE HANDLING IS STILL PARTIALLY BROKEN */
       +        /* FIXME: need to bring back hover state to make enter/leave work properly */
       +        /* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ */
       +        /* (especially once keyboard navigation is added) */
       +        /* Also, enter/leave should probably be called for all in hierarchy */
                int set_active = 1;
                if (widget->window->pressed_widget && widget->window->pressed_widget->vtable->motion_notify) {
                        widget->window->pressed_widget->vtable->motion_notify(widget->window->pressed_widget, event);
                        set_active = 0;
       -        } else if (widget->vtable->motion_notify) {
       -                set_active = widget->vtable->motion_notify(widget, event);
       +        } else if (widget && widget->state != LTK_DISABLED) {
       +                /* FIXME: because only the bottom widget of the hierarchy is stored,
       +                   this *really* does not work properly! */
       +                if (widget != widget->window->active_widget) {
       +                        if (widget->window->active_widget && widget->window->active_widget->vtable->mouse_leave) {
       +                                widget->window->active_widget->vtable->mouse_leave(widget->window->active_widget, event);
       +                        }
       +                        if (widget->vtable->mouse_enter) {
       +                                widget->vtable->mouse_enter(widget, event);
       +                        }
       +                }
       +                if (widget->vtable->motion_notify)
       +                        set_active = widget->vtable->motion_notify(widget, event);
                }
                if (set_active)
                        ltk_window_set_active_widget(widget->window, widget);
       t@@ -182,8 +208,7 @@ void
        ltk_set_widget(ltk_widget *widget, const char *id) {
                int ret;
                khint_t k;
       -        /* apparently, khash requires the string to stay accessible */
       -        /* FIXME: How is this freed? */
       +        /* 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;
       t@@ -191,20 +216,40 @@ ltk_set_widget(ltk_widget *widget, const char *id) {
        
        void
        ltk_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);
                }
        }
        
        int
       -ltk_widget_destroy(
       +ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr) {
       +        /* widget->parent->remove_child should never be NULL because of the fact that
       +           the widget is set as parent, but let's just check anyways... */
       +        int err = 0;
       +        /* FIXME: why is window passed here? */
       +        if (widget->parent && widget->parent->vtable->remove_child) {
       +                err = widget->parent->vtable->remove_child(
       +                    widget->window, widget, widget->parent, errstr
       +                );
       +        }
       +        widget->vtable->destroy(widget, shallow);
       +
       +        return err;
       +}
       +
       +int
       +ltk_widget_destroy_cmd(
            ltk_window *window,
            char **tokens,
            size_t num_tokens,
            char **errstr) {
       -        int err = 0, shallow = 1;
       +        (void)window;
       +        int shallow = 1;
                if (num_tokens != 2 && num_tokens != 3) {
                        *errstr = "Invalid number of arguments.\n";
                        return 1;
       t@@ -224,15 +269,5 @@ ltk_widget_destroy(
                        *errstr = "Invalid widget ID.\n";
                        return 1;
                }
       -        ltk_remove_widget(tokens[1]);
       -        /* 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... */
       -        if (widget->parent && widget->parent->vtable->remove_child) {
       -                err = widget->parent->vtable->remove_child(
       -                    window, widget, widget->parent, errstr
       -                );
       -        }
       -        widget->vtable->destroy(widget, shallow);
       -
       -        return err;
       +        return ltk_widget_destroy(widget, shallow, errstr);
        }
   DIR diff --git a/src/widget.h b/src/widget.h
       t@@ -53,6 +53,7 @@ typedef enum {
                LTK_LABEL,
                LTK_WIDGET,
                LTK_BOX,
       +        LTK_MENU,
                LTK_NUM_WIDGETS
        } ltk_widget_type;
        
       t@@ -81,6 +82,7 @@ struct ltk_widget {
                unsigned short row_span;
                unsigned short column_span;
                char dirty;
       +        char hidden;
        };
        
        struct ltk_widget_vtable {
       t@@ -90,10 +92,11 @@ struct ltk_widget_vtable {
                int (*mouse_release) (struct ltk_widget *, XEvent);
                int (*mouse_wheel) (struct ltk_widget *, XEvent);
                int (*motion_notify) (struct ltk_widget *, XEvent);
       -        void (*mouse_leave) (struct ltk_widget *, XEvent);
       -        void (*mouse_enter) (struct ltk_widget *, XEvent);
       +        int (*mouse_leave) (struct ltk_widget *, XEvent);
       +        int (*mouse_enter) (struct ltk_widget *, XEvent);
        
                void (*resize) (struct ltk_widget *);
       +        void (*hide) (struct ltk_widget *);
                void (*draw) (struct ltk_widget *, ltk_rect);
                void (*change_state) (struct ltk_widget *);
                void (*destroy) (struct ltk_widget *, int);
       t@@ -106,7 +109,8 @@ struct ltk_widget_vtable {
                char needs_surface;
        };
        
       -int ltk_widget_destroy(struct ltk_window *window, char **tokens, size_t num_tokens, char **errstr);
       +int ltk_widget_destroy(ltk_widget *widget, int shallow, char **errstr);
       +int ltk_widget_destroy_cmd(struct ltk_window *window, char **tokens, size_t num_tokens, char **errstr);
        void ltk_fill_widget_defaults(ltk_widget *widget, const char *id, struct ltk_window *window,
            struct ltk_widget_vtable *vtable, int w, int h);
        void ltk_widget_change_state(ltk_widget *widget);
   DIR diff --git a/test.sh b/test.sh
       t@@ -2,7 +2,7 @@
        
        # This is very hacky.
        #
       -# All events are still printed to the terminal curerntly because
       +# All events are still printed to the terminal currently because
        # the second './ltkc' still prints everything - event masks aren't
        # supported yet.
        
   DIR diff --git a/test2.gui b/test2.gui
       t@@ -0,0 +1,25 @@
       +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
       +menu menu1 add-entry entry1 "Menu Entry"
       +menu menu1 add-entry entrya1 "Menu Entry 2"
       +submenu submenu1 create
       +menu submenu1 add-entry entry2 "Submenu Entry 1"
       +menu submenu1 add-entry entry6 "Submenu Entry 2"
       +menu submenu1 add-entry entry7 "Submenu Entry 3"
       +menu submenu1 add-entry entry8 "Submenu Entry 4"
       +menu submenu1 add-entry entry9 "Submenu Entry 5"
       +menu submenu1 add-entry entry10 "Submenu Entry 6"
       +menu submenu1 add-entry entry11 "Submenu Entry 7"
       +menu submenu1 add-entry entry12 "Submenu Entry 8"
       +menu submenu1 add-entry entry13 "Submenu Entry 9"
       +menu menu1 add-submenu entry3 "Submenu" submenu1
       +submenu submenu2 create
       +menu submenu2 add-entry entry4 "Submenu Entry"
       +menu submenu1 add-submenu entry5 "Submenu" submenu2
       +submenu submenu3 create
       +menu submenu3 add-entry entrya3 "Submenu Entry"
       +menu submenu2 add-submenu entrya2 "Submenu" submenu3
       +grid grd1 add menu1 0 0 1 1 ew
   DIR diff --git a/test2.sh b/test2.sh
       t@@ -0,0 +1,10 @@
       +#!/bin/sh
       +
       +export LTKDIR="`pwd`/.ltk"
       +ltk_id=`./src/ltkd -t "Cool Window"`
       +if [ $? -ne 0 ]; then
       +        echo "Unable to start ltkd." >&2
       +        exit 1
       +fi
       +
       +cat test2.gui | ./src/ltkc $ltk_id