ltk

Socket-based GUI for X11 (WIP)
git clone git://lumidify.org/ltk.git (fast, but not encrypted)
git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

menu.c (58157B)


      1 /*
      2  * Copyright (c) 2022-2023 lumidify <nobody@lumidify.org>
      3  *
      4  * Permission to use, copy, modify, and/or distribute this software for any
      5  * purpose with or without fee is hereby granted, provided that the above
      6  * copyright notice and this permission notice appear in all copies.
      7  *
      8  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      9  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     10  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     11  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     12  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     13  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     14  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     15  */
     16 
     17 /* NOTE: The implementation of menus and menu entries is a collection of ugly hacks. */
     18 
     19 /* FIXME: parent is pressed when scroll arrows pressed */
     20 /* -> this is because the pressed handling checks if the widget is activatable, then goes to the parent,
     21    but the child isn't geometrically in the parent here, so that's weird */
     22 
     23 #include <stdio.h>
     24 #include <stdlib.h>
     25 #include <stdint.h>
     26 #include <string.h>
     27 #include <stdarg.h>
     28 #include <math.h>
     29 
     30 #include "proto_types.h"
     31 #include "event.h"
     32 #include "memory.h"
     33 #include "color.h"
     34 #include "rect.h"
     35 #include "widget.h"
     36 #include "ltk.h"
     37 #include "util.h"
     38 #include "text.h"
     39 #include "menu.h"
     40 #include "graphics.h"
     41 #include "surface_cache.h"
     42 #include "theme.h"
     43 #include "cmd.h"
     44 
     45 #define MAX_MENU_BORDER_WIDTH 100
     46 #define MAX_MENU_PAD 500
     47 #define MAX_MENU_ARROW_SIZE 100
     48 
     49 #define MAX(a, b) ((a) > (b) ? (a) : (b))
     50 
     51 static struct theme {
     52 	int pad;
     53 	int arrow_pad;
     54 	int arrow_size;
     55 	int border_width;
     56 	int compress_borders;
     57 
     58 	ltk_color border;
     59 	ltk_color background;
     60 	ltk_color scroll_background;
     61 	ltk_color scroll_arrow_color;
     62 } menu_theme, submenu_theme;
     63 
     64 static struct entry_theme {
     65 	int text_pad;
     66 	int arrow_pad;
     67 	int arrow_size;
     68 	int border_width;
     69 	int compress_borders;
     70 	/* FIXME: should border_sides actually factor into
     71 	   size calculation? - probably useless and would
     72 	   just make it more complicated */
     73 	/* FIXME: allow different values for different states? */
     74 	ltk_border_sides border_sides;
     75 
     76 	ltk_color text;
     77 	ltk_color border;
     78 	ltk_color fill;
     79 
     80 	ltk_color text_pressed;
     81 	ltk_color border_pressed;
     82 	ltk_color fill_pressed;
     83 
     84 	ltk_color text_active;
     85 	ltk_color border_active;
     86 	ltk_color fill_active;
     87 
     88 	ltk_color text_disabled;
     89 	ltk_color border_disabled;
     90 	ltk_color fill_disabled;
     91 } menu_entry_theme, submenu_entry_theme;
     92 
     93 static void ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r);
     94 static void ltk_menu_resize(ltk_widget *self);
     95 static void ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
     96 static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret);
     97 static void ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step);
     98 static void ltk_menu_scroll_callback(void *data);
     99 static void stop_scrolling(ltk_menu *menu);
    100 static ltk_widget *ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y);
    101 static int set_scroll_timer(ltk_menu *menu, int x, int y);
    102 static int ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
    103 static void ltk_menu_hide(ltk_widget *self);
    104 static void popup_active_menu(ltk_menuentry *e);
    105 static void unpopup_active_entry(ltk_menuentry *e);
    106 static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event);
    107 static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event);
    108 static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event);
    109 static ltk_menu *ltk_menu_create(ltk_window *window, const char *id, int is_submenu);
    110 static void recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget);
    111 static void shrink_entries(ltk_menu *menu);
    112 static size_t get_entry_with_id(ltk_menu *menu, const char *id);
    113 static void ltk_menu_destroy(ltk_widget *self, int shallow);
    114 
    115 static ltk_widget *ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect);
    116 static ltk_widget *ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget);
    117 static ltk_widget *ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget *widget);
    118 static ltk_widget *ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget *widget);
    119 static ltk_widget *ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget *widget);
    120 
    121 static ltk_menuentry *ltk_menuentry_create(ltk_window *window, const char *id, const char *text);
    122 static void ltk_menuentry_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
    123 static void ltk_menuentry_destroy(ltk_widget *self, int shallow);
    124 static void ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state);
    125 static int ltk_menuentry_release(ltk_widget *self);
    126 static void ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry);
    127 static int ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, ltk_error *err);
    128 static void ltk_menuentry_detach_submenu(ltk_menuentry *e);
    129 
    130 static int ltk_menu_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err);
    131 static int ltk_menuentry_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err);
    132 
    133 static ltk_widget *ltk_menu_prev_child(ltk_widget *self, ltk_widget *child);
    134 static ltk_widget *ltk_menu_next_child(ltk_widget *self, ltk_widget *child);
    135 static ltk_widget *ltk_menu_first_child(ltk_widget *self);
    136 static ltk_widget *ltk_menu_last_child(ltk_widget *self);
    137 static ltk_widget *ltk_menuentry_get_child(ltk_widget *self);
    138 
    139 #define IN_SUBMENU(e) (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)e->widget.parent)->is_submenu)
    140 
    141 static struct ltk_widget_vtable vtable = {
    142 	.key_press = NULL,
    143 	.key_release = NULL,
    144 	.mouse_press = NULL,
    145 	.mouse_scroll = &ltk_menu_mouse_scroll,
    146 	.motion_notify = &ltk_menu_motion_notify,
    147 	.mouse_release = NULL,
    148 	.mouse_enter = &ltk_menu_mouse_enter,
    149 	.mouse_leave = &ltk_menu_mouse_leave,
    150 	.get_child_at_pos = &ltk_menu_get_child_at_pos,
    151 	.resize = &ltk_menu_resize,
    152 	.change_state = NULL,
    153 	.hide = &ltk_menu_hide,
    154 	.draw = &ltk_menu_draw,
    155 	.destroy = &ltk_menu_destroy,
    156 	.child_size_change = &recalc_ideal_menu_size,
    157 	.remove_child = &ltk_menu_remove_child,
    158 	.prev_child = &ltk_menu_prev_child,
    159 	.next_child = &ltk_menu_next_child,
    160 	.first_child = &ltk_menu_first_child,
    161 	.last_child = &ltk_menu_last_child,
    162 	.nearest_child = &ltk_menu_nearest_child,
    163 	.nearest_child_left = &ltk_menu_nearest_child_left,
    164 	.nearest_child_right = &ltk_menu_nearest_child_right,
    165 	.nearest_child_above = &ltk_menu_nearest_child_above,
    166 	.nearest_child_below = &ltk_menu_nearest_child_below,
    167 	.ensure_rect_shown = &ltk_menu_ensure_rect_shown,
    168 	.type = LTK_WIDGET_MENU,
    169 	.flags = LTK_NEEDS_REDRAW,
    170 };
    171 
    172 static struct ltk_widget_vtable entry_vtable = {
    173 	.key_press = NULL,
    174 	.key_release = NULL,
    175 	.mouse_press = NULL,
    176 	.motion_notify = NULL,
    177 	.mouse_release = NULL,
    178 	.release = &ltk_menuentry_release,
    179 	.mouse_enter = NULL,
    180 	.mouse_leave = NULL,
    181 	.get_child_at_pos = NULL,
    182 	.resize = NULL,
    183 	.change_state = &ltk_menuentry_change_state,
    184 	.hide = NULL,
    185 	.draw = &ltk_menuentry_draw,
    186 	.destroy = &ltk_menuentry_destroy,
    187 	.child_size_change = NULL,
    188 	.remove_child = &ltk_menuentry_remove_child,
    189 	.first_child = &ltk_menuentry_get_child,
    190 	.last_child = &ltk_menuentry_get_child,
    191 	.type = LTK_WIDGET_MENUENTRY,
    192 	.flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
    193 };
    194 
    195 /* FIXME: standardize menuentry vs. menu_entry */
    196 
    197 static ltk_theme_parseinfo menu_parseinfo[] = {
    198 	{"pad", THEME_INT, {.i = &menu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0},
    199 	{"arrow-pad", THEME_INT, {.i = &menu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    200 	{"arrow-size", THEME_INT, {.i = &menu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
    201 	{"border-width", THEME_INT, {.i = &menu_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
    202 	{"compress-borders", THEME_BOOL, {.b = &menu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
    203 	{"border", THEME_COLOR, {.color = &menu_theme.border}, {.color = "#000000"}, 0, 0, 0},
    204 	{"background", THEME_COLOR, {.color = &menu_theme.background}, {.color = "#000000"}, 0, 0, 0},
    205 	{"scroll-background", THEME_COLOR, {.color = &menu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
    206 	{"scroll-arrow-color", THEME_COLOR, {.color = &menu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
    207 };
    208 static int menu_parseinfo_sorted = 0;
    209 
    210 int
    211 ltk_menu_ini_handler(ltk_window *window, const char *prop, const char *value) {
    212 	return ltk_theme_handle_value(window, "menu", prop, value, menu_parseinfo, LENGTH(menu_parseinfo), &menu_parseinfo_sorted);
    213 }
    214 
    215 int
    216 ltk_menu_fill_theme_defaults(ltk_window *window) {
    217 	return ltk_theme_fill_defaults(window, "menu", menu_parseinfo, LENGTH(menu_parseinfo));
    218 }
    219 
    220 void
    221 ltk_menu_uninitialize_theme(ltk_window *window) {
    222 	ltk_theme_uninitialize(window, menu_parseinfo, LENGTH(menu_parseinfo));
    223 }
    224 
    225 static ltk_theme_parseinfo menu_entry_parseinfo[] = {
    226 	{"text-pad", THEME_INT, {.i = &menu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    227 	{"arrow-pad", THEME_INT, {.i = &menu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    228 	{"arrow-size", THEME_INT, {.i = &menu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
    229 	{"border-width", THEME_INT, {.i = &menu_entry_theme.border_width}, {.i = 2}, 0, MAX_MENU_BORDER_WIDTH, 0},
    230 	{"border-sides", THEME_BORDERSIDES, {.border = &menu_entry_theme.border_sides}, {.border = LTK_BORDER_ALL}, 0, 0, 0},
    231 	{"compress-borders", THEME_BOOL, {.b = &menu_entry_theme.compress_borders}, {.b = 1}, 0, 0, 0},
    232 	{"text", THEME_COLOR, {.color = &menu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
    233 	{"border", THEME_COLOR, {.color = &menu_entry_theme.border}, {.color = "#339999"}, 0, 0, 0},
    234 	{"fill", THEME_COLOR, {.color = &menu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
    235 	{"text-pressed", THEME_COLOR, {.color = &menu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
    236 	{"border-pressed", THEME_COLOR, {.color = &menu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
    237 	{"fill-pressed", THEME_COLOR, {.color = &menu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
    238 	{"text-active", THEME_COLOR, {.color = &menu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
    239 	{"border-active", THEME_COLOR, {.color = &menu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
    240 	{"fill-active", THEME_COLOR, {.color = &menu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
    241 	{"text-disabled", THEME_COLOR, {.color = &menu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
    242 	{"border-disabled", THEME_COLOR, {.color = &menu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
    243 	{"fill-disabled", THEME_COLOR, {.color = &menu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
    244 };
    245 static int menu_entry_parseinfo_sorted = 0;
    246 
    247 int
    248 ltk_menuentry_ini_handler(ltk_window *window, const char *prop, const char *value) {
    249 	return ltk_theme_handle_value(window, "menu-entry", prop, value, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo), &menu_entry_parseinfo_sorted);
    250 }
    251 
    252 int
    253 ltk_menuentry_fill_theme_defaults(ltk_window *window) {
    254 	return ltk_theme_fill_defaults(window, "menu-entry", menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
    255 }
    256 
    257 void
    258 ltk_menuentry_uninitialize_theme(ltk_window *window) {
    259 	ltk_theme_uninitialize(window, menu_entry_parseinfo, LENGTH(menu_entry_parseinfo));
    260 }
    261 
    262 static ltk_theme_parseinfo submenu_parseinfo[] = {
    263 	{"pad", THEME_INT, {.i = &submenu_theme.pad}, {.i = 0}, 0, MAX_MENU_PAD, 0},
    264 	{"arrow-pad", THEME_INT, {.i = &submenu_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    265 	{"arrow-size", THEME_INT, {.i = &submenu_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
    266 	{"border-width", THEME_INT, {.i = &submenu_theme.border_width}, {.i = 1}, 0, MAX_MENU_BORDER_WIDTH, 0},
    267 	{"compress-borders", THEME_BOOL, {.b = &submenu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
    268 	{"border", THEME_COLOR, {.color = &submenu_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
    269 	{"background", THEME_COLOR, {.color = &submenu_theme.background}, {.color = "#000000"}, 0, 0, 0},
    270 	{"scroll-background", THEME_COLOR, {.color = &submenu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
    271 	{"scroll-arrow-color", THEME_COLOR, {.color = &submenu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
    272 };
    273 static int submenu_parseinfo_sorted = 0;
    274 
    275 int
    276 ltk_submenu_ini_handler(ltk_window *window, const char *prop, const char *value) {
    277 	return ltk_theme_handle_value(window, "submenu", prop, value, submenu_parseinfo, LENGTH(submenu_parseinfo), &submenu_parseinfo_sorted);
    278 }
    279 
    280 int
    281 ltk_submenu_fill_theme_defaults(ltk_window *window) {
    282 	return ltk_theme_fill_defaults(window, "submenu", submenu_parseinfo, LENGTH(submenu_parseinfo));
    283 }
    284 
    285 void
    286 ltk_submenu_uninitialize_theme(ltk_window *window) {
    287 	ltk_theme_uninitialize(window, submenu_parseinfo, LENGTH(submenu_parseinfo));
    288 }
    289 
    290 static ltk_theme_parseinfo submenu_entry_parseinfo[] = {
    291 	{"text-pad", THEME_INT, {.i = &submenu_entry_theme.text_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    292 	{"arrow-pad", THEME_INT, {.i = &submenu_entry_theme.arrow_pad}, {.i = 5}, 0, MAX_MENU_PAD, 0},
    293 	{"arrow-size", THEME_INT, {.i = &submenu_entry_theme.arrow_size}, {.i = 10}, 0, MAX_MENU_ARROW_SIZE, 0},
    294 	{"border-width", THEME_INT, {.i = &submenu_entry_theme.border_width}, {.i = 0}, 0, MAX_MENU_BORDER_WIDTH, 0},
    295 	{"border-sides", THEME_BORDERSIDES, {.border = &submenu_entry_theme.border_sides}, {.border = LTK_BORDER_NONE}, 0, 0, 0},
    296 	{"compress-borders", THEME_BOOL, {.b = &submenu_entry_theme.compress_borders}, {.b = 0}, 0, 0, 0},
    297 	{"text", THEME_COLOR, {.color = &submenu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
    298 	{"border", THEME_COLOR, {.color = &submenu_entry_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
    299 	{"fill", THEME_COLOR, {.color = &submenu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
    300 	{"text-pressed", THEME_COLOR, {.color = &submenu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
    301 	{"border-pressed", THEME_COLOR, {.color = &submenu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
    302 	{"fill-pressed", THEME_COLOR, {.color = &submenu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
    303 	{"text-active", THEME_COLOR, {.color = &submenu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
    304 	{"border-active", THEME_COLOR, {.color = &submenu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
    305 	{"fill-active", THEME_COLOR, {.color = &submenu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
    306 	{"text-disabled", THEME_COLOR, {.color = &submenu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
    307 	{"border-disabled", THEME_COLOR, {.color = &submenu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
    308 	{"fill-disabled", THEME_COLOR, {.color = &submenu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
    309 };
    310 static int submenu_entry_parseinfo_sorted = 0;
    311 
    312 int
    313 ltk_submenuentry_ini_handler(ltk_window *window, const char *prop, const char *value) {
    314 	return ltk_theme_handle_value(window, "submenu-entry", prop, value, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo), &submenu_entry_parseinfo_sorted);
    315 }
    316 
    317 int
    318 ltk_submenuentry_fill_theme_defaults(ltk_window *window) {
    319 	return ltk_theme_fill_defaults(window, "submenu-entry", submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
    320 }
    321 
    322 void
    323 ltk_submenuentry_uninitialize_theme(ltk_window *window) {
    324 	ltk_theme_uninitialize(window, submenu_entry_parseinfo, LENGTH(submenu_entry_parseinfo));
    325 }
    326 
    327 static void
    328 ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
    329 	ltk_menuentry *e = (ltk_menuentry *)self;
    330 	int in_submenu = IN_SUBMENU(e);
    331 	int submenus_opened = self->parent && self->parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)self->parent)->popup_submenus;
    332 	if (!(self->state & (LTK_ACTIVE | LTK_PRESSED))) {
    333 		/* Note: This only has to take care of the submenu that is the direct child
    334 		   of e because ltk_window_set_active_widget already calls change_state for
    335 		   the whole hierarchy */
    336 		unpopup_active_entry(e);
    337 	} else if ((self->state & LTK_PRESSED) && !(old_state & LTK_PRESSED) && submenus_opened) {
    338 		((ltk_menu *)self->parent)->popup_submenus = 0;
    339 	} else if (((self->state & LTK_PRESSED) ||
    340 	           ((self->state & LTK_ACTIVE) && (in_submenu || submenus_opened))) &&
    341 		   e->submenu && e->submenu->widget.hidden) {
    342 		popup_active_menu(e);
    343 		if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENU)
    344 			((ltk_menu *)self->parent)->popup_submenus = 1;
    345 	}
    346 }
    347 
    348 static ltk_widget *
    349 ltk_menuentry_get_child(ltk_widget *self) {
    350 	ltk_menuentry *e = (ltk_menuentry *)self;
    351 	if (e->submenu && !e->submenu->widget.hidden)
    352 		return &e->submenu->widget;
    353 	return NULL;
    354 }
    355 
    356 static void
    357 ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
    358 	/* FIXME: figure out how hidden should work */
    359 	if (self->hidden)
    360 		return;
    361 	ltk_menuentry *entry = (ltk_menuentry *)self;
    362 	int in_submenu = IN_SUBMENU(entry);
    363 	struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
    364 	ltk_color *text, *border, *fill;
    365 	if (self->state & LTK_DISABLED) {
    366 		text = &t->text_disabled;
    367 		border = &t->border_disabled;
    368 		fill = &t->fill_disabled;
    369 	} else if (self->state & LTK_PRESSED) {
    370 		text = &t->text_pressed;
    371 		border = &t->border_pressed;
    372 		fill = &t->fill_pressed;
    373 	} else if (self->state & LTK_HOVERACTIVE) {
    374 		text = &t->text_active;
    375 		border = &t->border_active;
    376 		fill = &t->fill_active;
    377 	} else {
    378 		text = &t->text;
    379 		border = &t->border;
    380 		fill = &t->fill;
    381 	}
    382 	ltk_rect lrect = self->lrect;
    383 	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
    384 	if (clip_final.w <= 0 || clip_final.h <= 0)
    385 		return;
    386 	ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
    387 	ltk_surface_fill_rect(draw_surf, fill, surf_clip);
    388 
    389 	ltk_surface *s;
    390 	int text_w, text_h;
    391 	ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
    392 	if (!ltk_surface_cache_get_surface(entry->text_surface_key, &s) || self->dirty) {
    393 		ltk_surface_fill_rect(s, fill, (ltk_rect){0, 0, text_w, text_h});
    394 		ltk_text_line_draw(entry->text_line, s, text, 0, 0);
    395 		self->dirty = 0;
    396 	}
    397 	int text_x = t->text_pad + t->border_width;
    398 	int text_y = t->text_pad + t->border_width;
    399 	ltk_rect text_clip = ltk_rect_intersect(clip, (ltk_rect){text_x, text_y, text_w, text_h});
    400 	ltk_surface_copy(
    401 	    s, draw_surf,
    402 	    (ltk_rect){text_clip.x - text_x, text_clip.y - text_y, text_clip.w, text_clip.h}, x + text_clip.x, y + text_clip.y
    403 	);
    404 
    405 	if (in_submenu && entry->submenu) {
    406 		ltk_point arrow_points[] = {
    407 		    {x + lrect.w - t->arrow_pad - t->border_width, y + lrect.h / 2},
    408 		    {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 - t->arrow_size / 2},
    409 		    {x + lrect.w - t->arrow_pad - t->border_width - t->arrow_size, y + lrect.h / 2 + t->arrow_size / 2}
    410 		};
    411 		ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), surf_clip);
    412 	}
    413 	ltk_surface_draw_border_clipped(draw_surf, border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, t->border_width, t->border_sides);
    414 }
    415 
    416 static void
    417 ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
    418 	if (self->hidden)
    419 		return;
    420 	ltk_menu *menu = (ltk_menu *)self;
    421 	ltk_rect lrect = self->lrect;
    422 	ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
    423 	if (clip_final.w <= 0 || clip_final.h <= 0)
    424 		return;
    425 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
    426 	ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
    427 	ltk_surface_fill_rect(s, &t->background, surf_clip);
    428 	ltk_widget *ptr = NULL;
    429 	for (size_t i = 0; i < menu->num_entries; i++) {
    430 		/* FIXME: I guess it could be improved *slightly* by making the clip rect
    431 		   smaller when scrollarrows are shown */
    432 		/* draw active entry after others so it isn't hidden with compress_borders */
    433 		if ((menu->entries[i]->widget.state & (LTK_ACTIVE | LTK_PRESSED | LTK_HOVER)) && i < menu->num_entries - 1) {
    434 			ptr = &menu->entries[i + 1]->widget;
    435 			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
    436 			ptr = &menu->entries[i]->widget;
    437 			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
    438 			i++;
    439 		} else {
    440 			ptr = &menu->entries[i]->widget;
    441 			ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
    442 		}
    443 	}
    444 
    445 	/* FIXME: active, pressed states */
    446 	int sz = t->arrow_size + t->arrow_pad * 2;
    447 	int ww = self->lrect.w;
    448 	int wh = self->lrect.h;
    449 	int wx = x, wy = y;
    450 	int mbw = t->border_width;
    451 	/* FIXME: handle pathological case where rect is so small that this still draws outside */
    452 	/* -> this is currently a mess because some parts handle clipping properly, but the scroll arrow drawing doesn't */
    453 	if (lrect.w < (int)self->ideal_w) {
    454 		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, sz, wh - mbw * 2});
    455 		ltk_surface_fill_rect(s, &t->scroll_background, (ltk_rect){wx + ww - sz - mbw, wy + mbw, sz, wh - mbw * 2});
    456 		ltk_point arrow_points[3] = {
    457 		    {wx + t->arrow_pad + mbw, wy + wh / 2},
    458 		    {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 - t->arrow_size / 2},
    459 		    {wx + t->arrow_pad + mbw + t->arrow_size, wy + wh / 2 + t->arrow_size / 2}
    460 		};
    461 		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
    462 		arrow_points[0] = (ltk_point){wx + ww - t->arrow_pad - mbw, wy + wh / 2};
    463 		arrow_points[1] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 - t->arrow_size / 2};
    464 		arrow_points[2] = (ltk_point){wx + ww - t->arrow_pad - mbw - t->arrow_size, wy + wh / 2 + t->arrow_size / 2};
    465 		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
    466 	}
    467 	if (lrect.h < (int)self->ideal_h) {
    468 		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, ww - mbw * 2, sz});
    469 		ltk_surface_fill_rect(self->window->surface, &t->scroll_background, (ltk_rect){wx + mbw, wy + wh - sz - mbw, ww - mbw * 2, sz});
    470 		ltk_point arrow_points[3] = {
    471 		    {wx + ww / 2, wy + t->arrow_pad + mbw},
    472 		    {wx + ww / 2 - t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size},
    473 		    {wx + ww / 2 + t->arrow_size / 2, wy + t->arrow_pad + mbw + t->arrow_size}
    474 		};
    475 		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
    476 		arrow_points[0] = (ltk_point){wx + ww / 2, wy + wh - t->arrow_pad - mbw};
    477 		arrow_points[1] = (ltk_point){wx + ww / 2 - t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size};
    478 		arrow_points[2] = (ltk_point){wx + ww / 2 + t->arrow_size / 2, wy + wh - t->arrow_pad - mbw - t->arrow_size};
    479 		ltk_surface_fill_polygon(s, &t->scroll_arrow_color, arrow_points, 3);
    480 	}
    481 	ltk_surface_draw_border_clipped(s, &t->border, (ltk_rect){x, y, lrect.w, lrect.h}, surf_clip, mbw, LTK_BORDER_ALL);
    482 
    483 	self->dirty = 0;
    484 }
    485 
    486 
    487 static void
    488 ltk_menu_resize(ltk_widget *self) {
    489 	ltk_menu *menu = (ltk_menu *)self;
    490 	int max_x, max_y;
    491 	ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
    492 	if (menu->x_scroll_offset > max_x)
    493 		menu->x_scroll_offset = max_x;
    494 	if (menu->y_scroll_offset > max_y)
    495 		menu->y_scroll_offset = max_y;
    496 
    497 	ltk_rect lrect = self->lrect;
    498 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
    499 	struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
    500 
    501 	int ideal_w = self->ideal_w, ideal_h = self->ideal_h;
    502 	int arrow_size = t->arrow_pad * 2 + t->arrow_size;
    503 	int start_x = lrect.w < ideal_w ? arrow_size : 0;
    504 	int start_y = lrect.h < ideal_h ? arrow_size : 0;
    505 	start_x += t->border_width;
    506 	start_y += t->border_width;
    507 
    508 	int mbw = t->border_width;
    509 	int cur_abs_x = -(int)menu->x_scroll_offset + start_x + t->pad;
    510 	int cur_abs_y = -(int)menu->y_scroll_offset + start_y + t->pad;
    511 
    512 	for (size_t i = 0; i < menu->num_entries; i++) {
    513 		ltk_menuentry *e = menu->entries[i];
    514 		e->widget.lrect.x = cur_abs_x;
    515 		e->widget.lrect.y = cur_abs_y;
    516 		if (menu->is_submenu) {
    517 			e->widget.lrect.w = ideal_w - 2 * t->pad - 2 * mbw;
    518 			e->widget.lrect.h = e->widget.ideal_h;
    519 			cur_abs_y += e->widget.ideal_h + t->pad;
    520 			if (et->compress_borders)
    521 				cur_abs_y -= et->border_width;
    522 		} else {
    523 			e->widget.lrect.w = e->widget.ideal_w;
    524 			e->widget.lrect.h = ideal_h - 2 * t->pad - 2 * mbw;
    525 			cur_abs_x += e->widget.ideal_w + t->pad;
    526 			if (et->compress_borders)
    527 				cur_abs_x -= et->border_width;
    528 		}
    529 		e->widget.crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, e->widget.lrect);
    530 	}
    531 	self->dirty = 1;
    532 	ltk_window_invalidate_widget_rect(self->window, self);
    533 }
    534 
    535 static void
    536 ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
    537 	ltk_menu *menu = (ltk_menu *)self;
    538 	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
    539 	int extra_size = theme->arrow_size + theme->arrow_pad * 2 + theme->border_width;
    540 	int delta = 0;
    541 	if (self->lrect.w < (int)self->ideal_w && !menu->is_submenu) {
    542 		if (r.x + r.w > self->lrect.w - extra_size && r.w <= self->lrect.w - 2 * extra_size)
    543 			delta = r.x - (self->lrect.w - extra_size - r.w);
    544 		else if (r.x < extra_size || r.w > self->lrect.w - 2 * extra_size)
    545 			delta = r.x - extra_size;
    546 		if (delta)
    547 			ltk_menu_scroll(menu, 0, 0, 0, 1, delta);
    548 	} else if (self->lrect.h < (int)self->ideal_h && menu->is_submenu) {
    549 		if (r.y + r.h > self->lrect.h - extra_size && r.h <= self->lrect.h - 2 * extra_size)
    550 			delta = r.y - (self->lrect.h - extra_size - r.h);
    551 		else if (r.y < extra_size || r.h > self->lrect.h - 2 * extra_size)
    552 			delta = r.y - extra_size;
    553 		if (delta)
    554 			ltk_menu_scroll(menu, 0, 1, 0, 0, delta);
    555 	}
    556 }
    557 
    558 static void
    559 ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) {
    560 	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
    561 	int extra_size = theme->arrow_size * 2 + theme->arrow_pad * 4;
    562 	*x_ret = 0;
    563 	*y_ret = 0;
    564 	if (menu->widget.lrect.w < (int)menu->widget.ideal_w) {
    565 		*x_ret = menu->widget.ideal_w - (menu->widget.lrect.w - extra_size);
    566 	}
    567 	if (menu->widget.lrect.h < (int)menu->widget.ideal_h) {
    568 		*y_ret = menu->widget.ideal_h - (menu->widget.lrect.h - extra_size);
    569 	}
    570 }
    571 
    572 static void
    573 ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
    574 	int max_scroll_x, max_scroll_y;
    575 	ltk_menu_get_max_scroll_offset(menu, &max_scroll_x, &max_scroll_y);
    576 	double y_old = menu->y_scroll_offset;
    577 	double x_old = menu->x_scroll_offset;
    578 	if (t)
    579 		menu->y_scroll_offset -= step;
    580 	else if (b)
    581 		menu->y_scroll_offset += step;
    582 	else if (l)
    583 		menu->x_scroll_offset -= step;
    584 	else if (r)
    585 		menu->x_scroll_offset += step;
    586 	if (menu->x_scroll_offset < 0)
    587 		menu->x_scroll_offset = 0;
    588 	if (menu->y_scroll_offset < 0)
    589 		menu->y_scroll_offset = 0;
    590 	if (menu->x_scroll_offset > max_scroll_x)
    591 		menu->x_scroll_offset = max_scroll_x;
    592 	if (menu->y_scroll_offset > max_scroll_y)
    593 		menu->y_scroll_offset = max_scroll_y;
    594 	/* FIXME: sensible epsilon? */
    595 	if (fabs(x_old - menu->x_scroll_offset) > 0.01 ||
    596 	    fabs(y_old - menu->y_scroll_offset) > 0.01) {
    597 		ltk_menu_resize(&menu->widget);
    598 		menu->widget.dirty = 1;
    599 		ltk_window_invalidate_widget_rect(menu->widget.window, &menu->widget);
    600 	}
    601 }
    602 
    603 /* FIXME: show scroll arrow disabled when nothing further */
    604 static void
    605 ltk_menu_scroll_callback(void *data) {
    606 	ltk_menu *menu = (ltk_menu *)data;
    607 	ltk_menu_scroll(
    608 	    menu,
    609 	    menu->scroll_top_hover, menu->scroll_bottom_hover,
    610 	    menu->scroll_left_hover, menu->scroll_right_hover, 2
    611 	);
    612 }
    613 
    614 static void
    615 stop_scrolling(ltk_menu *menu) {
    616 	menu->scroll_top_hover = 0;
    617 	menu->scroll_bottom_hover = 0;
    618 	menu->scroll_left_hover = 0;
    619 	menu->scroll_right_hover = 0;
    620 	if (menu->scroll_timer_id >= 0)
    621 		ltk_unregister_timer(menu->scroll_timer_id);
    622 }
    623 
    624 /* FIXME: should ideal_w, ideal_h just be int? */
    625 static ltk_widget *
    626 ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
    627 	ltk_menu *menu = (ltk_menu *)self;
    628 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
    629 	int arrow_size = t->arrow_size + t->arrow_pad * 2;
    630 	int mbw = t->border_width;
    631 	int start_x = mbw, end_x = self->lrect.w - mbw;
    632 	int start_y = mbw, end_y = self->lrect.h - mbw;
    633 	if (self->lrect.w < (int)self->ideal_w) {
    634 		start_x += arrow_size;
    635 		end_x -= arrow_size;
    636 	}
    637 	if (self->lrect.h < (int)self->ideal_h) {
    638 		start_y += arrow_size;
    639 		end_y -= arrow_size;
    640 	}
    641 	/* FIXME: use crect for this */
    642 	if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
    643 		return NULL;
    644 
    645 	for (size_t i = 0; i < menu->num_entries; i++) {
    646 		if (ltk_collide_rect(menu->entries[i]->widget.crect, x, y))
    647 			return &menu->entries[i]->widget;
    648 	}
    649 	return NULL;
    650 }
    651 
    652 /* FIXME: make sure timers are always destroyed when widget is destroyed */
    653 static int
    654 set_scroll_timer(ltk_menu *menu, int x, int y) {
    655 	/* this check probably isn't necessary, but whatever */
    656 	if (x < 0 || y < 0 || x >= menu->widget.lrect.w || y >= menu->widget.lrect.h)
    657 		return 0;
    658 	int t = 0, b = 0, l = 0,r = 0;
    659 	struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
    660 	int arrow_size = theme->arrow_size + theme->arrow_pad * 2;
    661 	if (menu->widget.lrect.w < (int)menu->widget.ideal_w) {
    662 		if (x < arrow_size)
    663 			l = 1;
    664 		else if (x > menu->widget.lrect.w - arrow_size)
    665 			r = 1;
    666 	}
    667 	if (menu->widget.lrect.h < (int)menu->widget.ideal_h) {
    668 		if (y < arrow_size)
    669 			t = 1;
    670 		else if (y > menu->widget.lrect.h - arrow_size)
    671 			b = 1;
    672 	}
    673 	if (t == menu->scroll_top_hover &&
    674 	    b == menu->scroll_bottom_hover &&
    675 	    l == menu->scroll_left_hover &&
    676 	    r == menu->scroll_right_hover)
    677 		return 0;
    678 	stop_scrolling(menu);
    679 	menu->scroll_top_hover = t;
    680 	menu->scroll_bottom_hover = b;
    681 	menu->scroll_left_hover = l;
    682 	menu->scroll_right_hover = r;
    683 	ltk_menu_scroll_callback(menu);
    684 	menu->scroll_timer_id = ltk_register_timer(0, 300, &ltk_menu_scroll_callback, menu);
    685 	return 1;
    686 }
    687 
    688 /* FIXME: The mouse release handler checks if the mouse collides with the rect of the widget
    689    before calling this, but that doesn't work with menuentries because part of their rect may
    690    be hidden when scrolling in a menu. Maybe widgets also need a "visible rect"? */
    691 static int
    692 ltk_menuentry_release(ltk_widget *self) {
    693 	ltk_menuentry *e = (ltk_menuentry *)self;
    694 	int in_submenu = IN_SUBMENU(e);
    695 	int keep_popup = self->parent && self->parent->vtable->type == LTK_WIDGET_MENU && ((ltk_menu *)self->parent)->popup_submenus;
    696 	if (in_submenu || !keep_popup) {
    697 		ltk_window_unregister_all_popups(self->window);
    698 	}
    699 	ltk_queue_specific_event(self, "menu", LTK_PWEVENTMASK_MENUENTRY_PRESS, "press");
    700 	return 1;
    701 }
    702 
    703 static int
    704 ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event) {
    705 	ltk_menu *menu = (ltk_menu *)self;
    706 	ltk_point glob = ltk_widget_pos_to_global(self, event->x, event->y);
    707 	/* FIXME: configure scroll step */
    708 	/* FIXME: fix the interface for ltk_menu_scroll */
    709 	if (event->dx > 0)
    710 		ltk_menu_scroll(menu, 0, 0, 0, 1, event->dx * 10);
    711 	else if (event->dx < 0)
    712 		ltk_menu_scroll(menu, 0, 0, 1, 0, -event->dx * 10);
    713 	if (event->dy > 0)
    714 		ltk_menu_scroll(menu, 1, 0, 0, 0, event->dy * 10);
    715 	else if (event->dy < 0)
    716 		ltk_menu_scroll(menu, 0, 1, 0, 0, -event->dy * 10);
    717 	ltk_window_fake_motion_event(self->window, glob.x, glob.y);
    718 	return 1;
    719 }
    720 
    721 static void
    722 ltk_menu_hide(ltk_widget *self) {
    723 	ltk_menu *menu = (ltk_menu *)self;
    724 	if (menu->scroll_timer_id >= 0)
    725 		ltk_unregister_timer(menu->scroll_timer_id);
    726 	menu->scroll_bottom_hover = menu->scroll_top_hover = 0;
    727 	menu->scroll_left_hover = menu->scroll_right_hover = 0;
    728 	ltk_window_unregister_popup(self->window, self);
    729 	ltk_window_invalidate_widget_rect(self->window, self);
    730 	/* FIXME: this is really ugly/hacky */
    731 	if (menu->unpopup_submenus_on_hide && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY &&
    732 	    self->parent->parent && self->parent->parent->vtable->type == LTK_WIDGET_MENU) {
    733 		((ltk_menu *)self->parent->parent)->popup_submenus = 0;
    734 	}
    735 	menu->unpopup_submenus_on_hide = 1;
    736 }
    737 
    738 /* FIXME: hacky because entries need to know about their parents to be able to properly position the popup */
    739 static void
    740 popup_active_menu(ltk_menuentry *e) {
    741 	if (!e->submenu)
    742 		return;
    743 	int in_submenu = 0, was_opened_left = 0;
    744 	ltk_rect menu_rect = e->widget.lrect;
    745 	ltk_point entry_global = ltk_widget_pos_to_global(&e->widget, 0, 0);
    746 	ltk_point menu_global;
    747 	if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
    748 		ltk_menu *menu = (ltk_menu *)e->widget.parent;
    749 		in_submenu = menu->is_submenu;
    750 		was_opened_left = menu->was_opened_left;
    751 		menu_rect = menu->widget.lrect;
    752 		menu_global = ltk_widget_pos_to_global(e->widget.parent, 0, 0);
    753 	} else {
    754 		menu_global = ltk_widget_pos_to_global(&e->widget, 0, 0);
    755 	}
    756 	int win_w = e->widget.window->rect.w;
    757 	int win_h = e->widget.window->rect.h;
    758 	ltk_menu *submenu = e->submenu;
    759 	int ideal_w = submenu->widget.ideal_w;
    760 	int ideal_h = submenu->widget.ideal_h;
    761 	int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
    762 	if (in_submenu) {
    763 		int space_left = menu_global.x;
    764 		int space_right = win_w - (menu_global.x + menu_rect.w);
    765 		int x_right = menu_global.x + menu_rect.w;
    766 		int x_left = menu_global.x - ideal_w;
    767 		if (submenu_theme.compress_borders) {
    768 			x_right -= submenu_theme.border_width;
    769 			x_left += submenu_theme.border_width;
    770 		}
    771 		if (was_opened_left) {
    772 			if (x_left >= 0) {
    773 				x_final = x_left;
    774 				submenu->was_opened_left = 1;
    775 			} else if (space_right >= ideal_w) {
    776 				x_final = x_right;
    777 				submenu->was_opened_left = 0;
    778 			} else {
    779 				x_final = 0;
    780 				if (win_w < ideal_w)
    781 					w_final = win_w;
    782 				submenu->was_opened_left = 1;
    783 			}
    784 		} else {
    785 			if (space_right >= ideal_w) {
    786 				x_final = x_right;
    787 				submenu->was_opened_left = 0;
    788 			} else if (space_left >= ideal_w) {
    789 				x_final = x_left;
    790 				submenu->was_opened_left = 1;
    791 			} else {
    792 				x_final = win_w - ideal_w;
    793 				if (x_final < 0) {
    794 					x_final = 0;
    795 					w_final = win_w;
    796 				}
    797 				submenu->was_opened_left = 0;
    798 			}
    799 		}
    800 		/* subtract padding and border width so the actual entries are at the right position */
    801 		y_final = entry_global.y - submenu_theme.pad - submenu_theme.border_width;
    802 		if (y_final + ideal_h > win_h)
    803 			y_final = win_h - ideal_h;
    804 		if (y_final < 0) {
    805 			y_final = 0;
    806 			h_final = win_h;
    807 		}
    808 	} else {
    809 		int space_top = menu_global.y;
    810 		int space_bottom = win_h - (menu_global.y + menu_rect.h);
    811 		int y_top = menu_global.y - ideal_h;
    812 		int y_bottom = menu_global.y + menu_rect.h;
    813 		if (menu_theme.compress_borders) {
    814 			y_top += menu_theme.border_width;
    815 			y_bottom -= menu_theme.border_width;
    816 		}
    817 		if (space_top > space_bottom) {
    818 			y_final = y_top;
    819 			if (y_final < 0) {
    820 				y_final = 0;
    821 				h_final = menu_rect.y;
    822 			}
    823 			submenu->was_opened_above = 1;
    824 		} else {
    825 			y_final = y_bottom;
    826 			if (space_bottom < ideal_h)
    827 				h_final = space_bottom;
    828 			submenu->was_opened_above = 0;
    829 		}
    830 		/* FIXME: maybe threshold so there's always at least a part of
    831 		   the menu contents shown (instead of maybe just a few pixels) */
    832 		/* pathological case where window is way too small */
    833 		if (h_final <= 0) {
    834 			y_final = 0;
    835 			h_final = win_h;
    836 		}
    837 		x_final = entry_global.x;
    838 		if (x_final + ideal_w > win_w)
    839 			x_final = win_w - ideal_w;
    840 		if (x_final < 0) {
    841 			x_final = 0;
    842 			w_final = win_w;
    843 		}
    844 	}
    845 	/* reset everything just in case */
    846 	submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
    847 	submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
    848 	submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
    849 	submenu->widget.lrect.x = x_final;
    850 	submenu->widget.lrect.y = y_final;
    851 	submenu->widget.lrect.w = w_final;
    852 	submenu->widget.lrect.h = h_final;
    853 	submenu->widget.crect = submenu->widget.lrect;
    854 	submenu->widget.dirty = 1;
    855 	submenu->widget.hidden = 0;
    856 	submenu->popup_submenus = 0;
    857 	submenu->unpopup_submenus_on_hide = 1;
    858 	ltk_menu_resize(&submenu->widget);
    859 	ltk_window_register_popup(e->widget.window, (ltk_widget *)submenu);
    860 	ltk_window_invalidate_widget_rect(submenu->widget.window, &submenu->widget);
    861 }
    862 
    863 static void
    864 unpopup_active_entry(ltk_menuentry *e) {
    865 	if (e->submenu && !e->submenu->widget.hidden) {
    866 		e->submenu->unpopup_submenus_on_hide = 0;
    867 		ltk_widget_hide(&e->submenu->widget);
    868 	}
    869 }
    870 
    871 static int
    872 ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event) {
    873 	set_scroll_timer((ltk_menu *)self, event->x, event->y);
    874 	return 1;
    875 }
    876 
    877 static int
    878 ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
    879 	set_scroll_timer((ltk_menu *)self, event->x, event->y);
    880 	return 1;
    881 }
    882 
    883 static int
    884 ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
    885 	(void)event;
    886 	stop_scrolling((ltk_menu *)self);
    887 	return 1;
    888 }
    889 
    890 static ltk_menu *
    891 ltk_menu_create(ltk_window *window, const char *id, int is_submenu) {
    892 	ltk_menu *menu = ltk_malloc(sizeof(ltk_menu));
    893 	menu->widget.ideal_w = menu_theme.pad;
    894 	menu->widget.ideal_h = menu_theme.pad;
    895 	ltk_fill_widget_defaults(&menu->widget, id, window, &vtable, menu->widget.ideal_w, menu->widget.ideal_h);
    896 	menu->widget.dirty = 1;
    897 
    898 	menu->entries = NULL;
    899 	menu->num_entries = menu->num_alloc = 0;
    900 	menu->x_scroll_offset = menu->y_scroll_offset = 0;
    901 	menu->is_submenu = is_submenu;
    902 	menu->was_opened_left = 0;
    903 	menu->was_opened_above = 0;
    904 	menu->scroll_timer_id = -1;
    905 	menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
    906 	menu->scroll_left_hover = menu->scroll_right_hover = 0;
    907 	menu->popup_submenus = 0;
    908 	menu->unpopup_submenus_on_hide = 1;
    909 	/* FIXME: hide widget by default so recalc doesn't cause
    910 	   unnecessary redrawing */
    911 	recalc_ideal_menu_size(&menu->widget, NULL);
    912 
    913 	return menu;
    914 }
    915 
    916 static int
    917 insert_entry(ltk_menu *menu, ltk_menuentry *e, size_t idx) {
    918 	if (idx > menu->num_entries)
    919 		return 1;
    920 	if (menu->num_entries == menu->num_alloc) {
    921 		menu->num_alloc = ideal_array_size(menu->num_alloc, menu->num_entries + 1);
    922 		menu->entries = ltk_reallocarray(menu->entries, menu->num_alloc, sizeof(ltk_menuentry *));
    923 	}
    924 	memmove(
    925 	    menu->entries + idx + 1,
    926 	    menu->entries + idx,
    927 	    sizeof(ltk_menuentry *) * (menu->num_entries - idx)
    928 	);
    929 	menu->num_entries++;
    930 	menu->entries[idx] = e;
    931 	return 0;
    932 }
    933 
    934 static void
    935 recalc_ideal_menu_size(ltk_widget *self, ltk_widget *widget) {
    936 	ltk_menu *menu = (ltk_menu *)self;
    937 	/* If widget with size change is submenu, it doesn't affect this menu */
    938 	if (widget && widget->vtable->type == LTK_WIDGET_MENU) {
    939 		ltk_widget_resize(widget);
    940 		return;
    941 	}
    942 	struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
    943 	struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
    944 	unsigned int old_w = menu->widget.ideal_w, old_h = menu->widget.ideal_h;
    945 	menu->widget.ideal_w = menu->widget.ideal_h = t->pad + t->border_width * 2;
    946 	ltk_menuentry *e;
    947 	for (size_t i = 0; i < menu->num_entries; i++) {
    948 		e = menu->entries[i];
    949 		if (menu->is_submenu) {
    950 			menu->widget.ideal_w = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_w, (int)menu->widget.ideal_w);
    951 			menu->widget.ideal_h += e->widget.ideal_h + t->pad;
    952 			if (et->compress_borders && i != 0)
    953 				menu->widget.ideal_h -= et->border_width;
    954 		} else {
    955 			menu->widget.ideal_w += e->widget.ideal_w + t->pad;
    956 			if (et->compress_borders && i != 0)
    957 				menu->widget.ideal_w -= et->border_width;
    958 			menu->widget.ideal_h = MAX((t->pad + t->border_width) * 2 + (int)e->widget.ideal_h, (int)menu->widget.ideal_h);
    959 		}
    960 	}
    961 	if ((old_w != menu->widget.ideal_w || old_h != menu->widget.ideal_h) &&
    962 	    menu->widget.parent && menu->widget.parent->vtable->child_size_change) {
    963 		menu->widget.parent->vtable->child_size_change(menu->widget.parent, (ltk_widget *)menu);
    964 	} else {
    965 		ltk_menu_resize(self);
    966 	}
    967 	menu->widget.dirty = 1;
    968 	if (!menu->widget.hidden)
    969 		ltk_window_invalidate_widget_rect(menu->widget.window, &menu->widget);
    970 }
    971 
    972 static void
    973 ltk_menuentry_recalc_ideal_size(ltk_menuentry *entry) {
    974 	int in_submenu = IN_SUBMENU(entry);
    975 	struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
    976 	int extra_size = (in_submenu && entry->submenu) ? t->arrow_pad * 2 + t->arrow_size : 0;
    977 
    978 	int text_w, text_h;
    979 	ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
    980 	entry->widget.ideal_w = text_w + extra_size + (t->text_pad + t->border_width) * 2;
    981 	entry->widget.ideal_h = MAX(text_h + t->text_pad * 2, extra_size) + t->border_width * 2;
    982 	/* FIXME: only call if something changed */
    983 	if (entry->widget.parent && entry->widget.parent->vtable->child_size_change) {
    984 		entry->widget.parent->vtable->child_size_change(entry->widget.parent, (ltk_widget *)entry);
    985 	}
    986 }
    987 
    988 static ltk_menuentry *
    989 ltk_menuentry_create(ltk_window *window, const char *id, const char *text) {
    990 	ltk_menuentry *e = ltk_malloc(sizeof(ltk_menuentry));
    991 	e->text_line = ltk_text_line_create(window->text_context, window->theme->font_size, (char *)text, 0, -1);
    992 	e->submenu = NULL;
    993 	int w, h;
    994 	ltk_text_line_get_size(e->text_line, &w, &h);
    995 	e->text_surface_key = ltk_surface_cache_get_unnamed_key(window->surface_cache, w, h);
    996 	ltk_fill_widget_defaults(&e->widget, id, window, &entry_vtable, 5, 5);
    997 	/* Note: This is only set as a dummy value! The actual ideal size can't
    998 	   be set until it is part of a menu because it needs to know which
    999 	   theme it should use */
   1000 	ltk_menuentry_recalc_ideal_size(e);
   1001 	e->widget.dirty = 1;
   1002 	return e;
   1003 }
   1004 
   1005 static int
   1006 ltk_menuentry_remove_child(ltk_widget *widget, ltk_widget *self, ltk_error *err) {
   1007 	ltk_menuentry *e = (ltk_menuentry *)self;
   1008 	if (widget != &e->submenu->widget) {
   1009 		err->type = ERR_WIDGET_NOT_IN_CONTAINER;
   1010 		return 1;
   1011 	}
   1012 	widget->parent = NULL;
   1013 	e->submenu = NULL;
   1014 	ltk_menuentry_recalc_ideal_size(e);
   1015 	return 0;
   1016 }
   1017 
   1018 static void
   1019 ltk_menuentry_destroy(ltk_widget *self, int shallow) {
   1020 	ltk_menuentry *e = (ltk_menuentry *)self;
   1021 	ltk_text_line_destroy(e->text_line);
   1022 	ltk_surface_cache_release_key(e->text_surface_key);
   1023 	/* FIXME: function to call when parent is destroyed */
   1024 	/* also function to call when parent added */
   1025 	if (e->submenu) {
   1026 		e->submenu->widget.parent = NULL;
   1027 		if (!shallow) {
   1028 			ltk_error err;
   1029 			ltk_widget_destroy(&e->submenu->widget, shallow, &err);
   1030 		}
   1031 	}
   1032 	ltk_free(e);
   1033 }
   1034 
   1035 static int
   1036 ltk_menu_insert_entry(ltk_menu *menu, ltk_menuentry *entry, size_t idx, ltk_error *err) {
   1037 	if (entry->widget.parent) {
   1038 		err->type = ERR_WIDGET_IN_CONTAINER;
   1039 		return 1;
   1040 	}
   1041 	if (insert_entry(menu, entry, idx)) {
   1042 		err->type = ERR_INVALID_INDEX;
   1043 		return 1;
   1044 	}
   1045 	entry->widget.parent = &menu->widget;
   1046 	ltk_menuentry_recalc_ideal_size(entry);
   1047 	recalc_ideal_menu_size(&menu->widget, NULL);
   1048 	menu->widget.dirty = 1;
   1049 	return 0;
   1050 }
   1051 
   1052 static int
   1053 ltk_menu_add_entry(ltk_menu *menu, ltk_menuentry *entry, ltk_error *err) {
   1054 	return ltk_menu_insert_entry(menu, entry, menu->num_entries, err);
   1055 }
   1056 
   1057 /* FIXME: maybe allow any menu and just change is_submenu (also need to recalculate size then) */
   1058 static int
   1059 ltk_menuentry_attach_submenu(ltk_menuentry *e, ltk_menu *submenu, ltk_error *err) {
   1060 	if (!submenu->is_submenu) {
   1061 		err->type = ERR_MENU_NOT_SUBMENU;
   1062 		return 1;
   1063 	} else if (e->submenu) {
   1064 		err->type = ERR_MENU_ENTRY_CONTAINS_SUBMENU;
   1065 		return 1;
   1066 	}
   1067 	e->submenu = submenu;
   1068 	ltk_menuentry_recalc_ideal_size(e);
   1069 	e->widget.dirty = 1;
   1070 	if (submenu) {
   1071 		submenu->widget.hidden = 1;
   1072 		submenu->widget.parent = &e->widget;
   1073 	}
   1074 	if (!e->widget.hidden)
   1075 		ltk_window_invalidate_widget_rect(e->widget.window, &e->widget);
   1076 	return 0;
   1077 }
   1078 
   1079 /* FIXME: hide all entries when menu hidden? */
   1080 
   1081 static void
   1082 shrink_entries(ltk_menu *menu) {
   1083 	size_t new_alloc = ideal_array_size(menu->num_alloc, menu->num_entries);
   1084 	if (new_alloc != menu->num_alloc) {
   1085 		menu->entries = ltk_reallocarray(menu->entries, new_alloc, sizeof(ltk_menuentry *));
   1086 		menu->num_alloc = new_alloc;
   1087 	}
   1088 }
   1089 
   1090 static int
   1091 ltk_menu_remove_entry_index(ltk_menu *menu, size_t idx, ltk_error *err) {
   1092 	if (idx >= menu->num_entries) {
   1093 		err->type = ERR_INVALID_INDEX;
   1094 		return 1;
   1095 	}
   1096 	menu->entries[idx]->widget.parent = NULL;
   1097 	ltk_menuentry_recalc_ideal_size(menu->entries[idx]);
   1098 	memmove(
   1099 	    menu->entries + idx,
   1100 	    menu->entries + idx + 1,
   1101 	    sizeof(ltk_menuentry *) * (menu->num_entries - idx - 1)
   1102 	);
   1103 	menu->num_entries--;
   1104 	shrink_entries(menu);
   1105 	recalc_ideal_menu_size(&menu->widget, NULL);
   1106 	return 0;
   1107 }
   1108 
   1109 static size_t
   1110 get_entry_with_id(ltk_menu *menu, const char *id) {
   1111 	for (size_t i = 0; i < menu->num_entries; i++) {
   1112 		if (!strcmp(id, menu->entries[i]->widget.id))
   1113 			return i;
   1114 	}
   1115 	return SIZE_MAX;
   1116 }
   1117 
   1118 static int
   1119 ltk_menu_remove_entry_id(ltk_menu *menu, const char *id, ltk_error *err) {
   1120 	size_t idx = get_entry_with_id(menu, id);
   1121 	if (idx >= menu->num_entries) {
   1122 		err->type = ERR_INVALID_WIDGET_ID;
   1123 		return 1;
   1124 	}
   1125 	return ltk_menu_remove_entry_index(menu, idx, err);
   1126 }
   1127 
   1128 static int
   1129 ltk_menu_remove_child(ltk_widget *child, ltk_widget *self, ltk_error *err) {
   1130 	ltk_menu *menu = (ltk_menu *)self;
   1131 	for (size_t i = 0; i < menu->num_entries; i++) {
   1132 		if (&menu->entries[i]->widget == child) {
   1133 			return ltk_menu_remove_entry_index(menu, i, err);
   1134 		}
   1135 	}
   1136 	err->type = ERR_WIDGET_NOT_IN_CONTAINER;
   1137 	return 1;
   1138 }
   1139 
   1140 static void
   1141 ltk_menu_remove_all_entries(ltk_menu *menu) {
   1142 	for (size_t i = 0; i < menu->num_entries; i++) {
   1143 		menu->entries[i]->widget.parent = NULL;
   1144 		ltk_menuentry_recalc_ideal_size(menu->entries[i]);
   1145 	}
   1146 	menu->num_entries = menu->num_alloc = 0;
   1147 	ltk_free(menu->entries);
   1148 	menu->entries = NULL;
   1149 	recalc_ideal_menu_size(&menu->widget, NULL);
   1150 }
   1151 
   1152 static ltk_widget *
   1153 ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect) {
   1154 	ltk_menu *menu = (ltk_menu *)self;
   1155 	ltk_widget *minw = NULL;
   1156 	int min_dist = INT_MAX;
   1157 	int cx = rect.x + rect.w / 2;
   1158 	int cy = rect.y + rect.h / 2;
   1159 	ltk_rect r;
   1160 	int dist;
   1161 	for (size_t i = 0; i < menu->num_entries; i++) {
   1162 		r = menu->entries[i]->widget.lrect;
   1163 		dist = abs((r.x + r.w / 2) - cx) + abs((r.y + r.h / 2) - cy);
   1164 		if (dist < min_dist) {
   1165 			min_dist = dist;
   1166 			minw = &menu->entries[i]->widget;
   1167 		}
   1168 	}
   1169 	return minw;
   1170 }
   1171 
   1172 /* FIXME: These need to be updated if all menus are allowed to be horizontal or vertical! */
   1173 /* FIXME: this doesn't work properly when parent and child are both on the same side,
   1174    but I guess there's no good way to fix that */
   1175 /* FIXME: behavior is a bit weird when e.g. moving down when every active menu entry in hierarchy
   1176    is already at bottom of respective menu - the top-level menu will give the first submenu in
   1177    the current active hierarchy as child widget again, and nearest_child on that submenu will
   1178    (probably) give the bottom widget again, so nothing changes except that all submenus except
   1179    for the first and second one disappeare */
   1180 static ltk_widget *
   1181 ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget *widget) {
   1182 	ltk_menu *menu = (ltk_menu *)self;
   1183 	ltk_widget *left = NULL;
   1184 	if (!menu->is_submenu) {
   1185 		left = ltk_menu_prev_child(self, widget);
   1186 	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY &&
   1187 	           ((ltk_menuentry *)widget)->submenu &&
   1188 	           !((ltk_menuentry *)widget)->submenu->widget.hidden &&
   1189 	           ((ltk_menuentry *)widget)->submenu->was_opened_left) {
   1190 		left =  &((ltk_menuentry *)widget)->submenu->widget;
   1191 	} else if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
   1192 		ltk_menuentry *e = (ltk_menuentry *)self->parent;
   1193 		if (!menu->was_opened_left && IN_SUBMENU(e)) {
   1194 			left = self->parent;
   1195 		} else if (!IN_SUBMENU(e) && e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
   1196 			left = ltk_menu_prev_child(e->widget.parent, &e->widget);
   1197 		}
   1198 	}
   1199 	return left;
   1200 }
   1201 
   1202 static ltk_widget *
   1203 ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget *widget) {
   1204 	ltk_menu *menu = (ltk_menu *)self;
   1205 	ltk_widget *right = NULL;
   1206 	if (!menu->is_submenu) {
   1207 		right = ltk_menu_next_child(self, widget);
   1208 	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY &&
   1209 	           ((ltk_menuentry *)widget)->submenu &&
   1210 	           !((ltk_menuentry *)widget)->submenu->widget.hidden &&
   1211 	           !((ltk_menuentry *)widget)->submenu->was_opened_left) {
   1212 		right =  &((ltk_menuentry *)widget)->submenu->widget;
   1213 	} else if (self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
   1214 		ltk_menuentry *e = (ltk_menuentry *)self->parent;
   1215 		if (menu->was_opened_left && IN_SUBMENU(e)) {
   1216 			right = self->parent;
   1217 		} else if (!IN_SUBMENU(e) && e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
   1218 			right = ltk_menu_next_child(e->widget.parent, &e->widget);
   1219 		}
   1220 	}
   1221 	return right;
   1222 }
   1223 
   1224 static ltk_widget *
   1225 ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget *widget) {
   1226 	ltk_menu *menu = (ltk_menu *)self;
   1227 	ltk_widget *above = NULL;
   1228 	if (menu->is_submenu) {
   1229 		above = ltk_menu_prev_child(self, widget);
   1230 		if (!above && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
   1231 			ltk_menuentry *e = (ltk_menuentry *)self->parent;
   1232 			if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
   1233 				ltk_menu *pmenu = (ltk_menu *)e->widget.parent;
   1234 				if (!menu->was_opened_above && !pmenu->is_submenu) {
   1235 					above = self->parent;
   1236 				} else if (pmenu->is_submenu) {
   1237 					above = ltk_menu_prev_child(e->widget.parent, &e->widget);
   1238 				}
   1239 			}
   1240 		}
   1241 	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY) {
   1242 		ltk_menuentry *e = (ltk_menuentry *)widget;
   1243 		if (e->submenu && !e->submenu->widget.hidden && e->submenu->was_opened_above) {
   1244 			above =  &e->submenu->widget;
   1245 		}
   1246 	}
   1247 	return above;
   1248 }
   1249 
   1250 static ltk_widget *
   1251 ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget *widget) {
   1252 	ltk_menu *menu = (ltk_menu *)self;
   1253 	ltk_widget *below = NULL;
   1254 	if (menu->is_submenu) {
   1255 		below = ltk_menu_next_child(self, widget);
   1256 		if (!below && self->parent && self->parent->vtable->type == LTK_WIDGET_MENUENTRY) {
   1257 			ltk_menuentry *e = (ltk_menuentry *)self->parent;
   1258 			if (e->widget.parent && e->widget.parent->vtable->type == LTK_WIDGET_MENU) {
   1259 				ltk_menu *pmenu = (ltk_menu *)e->widget.parent;
   1260 				if (menu->was_opened_above && !pmenu->is_submenu) {
   1261 					below = self->parent;
   1262 				} else if (pmenu->is_submenu) {
   1263 					below = ltk_menu_next_child(e->widget.parent, &e->widget);
   1264 				}
   1265 			}
   1266 		}
   1267 	} else if (widget->vtable->type == LTK_WIDGET_MENUENTRY) {
   1268 		ltk_menuentry *e = (ltk_menuentry *)widget;
   1269 		if (e->submenu && !e->submenu->widget.hidden && !e->submenu->was_opened_above) {
   1270 			below = &e->submenu->widget;
   1271 		}
   1272 	}
   1273 	return below;
   1274 }
   1275 
   1276 static ltk_widget *
   1277 ltk_menu_prev_child(ltk_widget *self, ltk_widget *child) {
   1278 	ltk_menu *menu = (ltk_menu *)self;
   1279 	for (size_t i = menu->num_entries; i-- > 0;) {
   1280 		if (&menu->entries[i]->widget == child)
   1281 			return i > 0 ? &menu->entries[i-1]->widget : NULL;
   1282 	}
   1283 	return NULL;
   1284 }
   1285 
   1286 static ltk_widget *
   1287 ltk_menu_next_child(ltk_widget *self, ltk_widget *child) {
   1288 	ltk_menu *menu = (ltk_menu *)self;
   1289 	for (size_t i = 0; i < menu->num_entries; i++) {
   1290 		if (&menu->entries[i]->widget == child)
   1291 			return i < menu->num_entries - 1 ? &menu->entries[i+1]->widget : NULL;
   1292 	}
   1293 	return NULL;
   1294 }
   1295 
   1296 static ltk_widget *
   1297 ltk_menu_first_child(ltk_widget *self) {
   1298 	ltk_menu *menu = (ltk_menu *)self;
   1299 	return menu->num_entries > 0 ? &menu->entries[0]->widget : NULL;
   1300 }
   1301 
   1302 static ltk_widget *
   1303 ltk_menu_last_child(ltk_widget *self) {
   1304 	ltk_menu *menu = (ltk_menu *)self;
   1305 	return menu->num_entries > 0 ? &menu->entries[menu->num_entries-1]->widget : NULL;
   1306 }
   1307 
   1308 /* FIXME: unregister from window popups? */
   1309 static void
   1310 ltk_menuentry_detach_submenu(ltk_menuentry *e) {
   1311 	if (e->submenu)
   1312 		e->submenu->widget.parent = NULL;
   1313 	e->submenu = NULL;
   1314 	ltk_menuentry_recalc_ideal_size(e);
   1315 }
   1316 
   1317 static void
   1318 ltk_menu_destroy(ltk_widget *self, int shallow) {
   1319 	ltk_menu *menu = (ltk_menu *)self;
   1320 	if (!menu) {
   1321 		ltk_warn("Tried to destroy NULL menu.\n");
   1322 		return;
   1323 	}
   1324 	if (menu->scroll_timer_id >= 0)
   1325 		ltk_unregister_timer(menu->scroll_timer_id);
   1326 	ltk_window_unregister_popup(self->window, self);
   1327 	if (!shallow) {
   1328 		ltk_error err;
   1329 		for (size_t i = 0; i < menu->num_entries; i++) {
   1330 			/* for efficiency - to avoid ltk_widget_destroy calling
   1331 			   ltk_menu_remove_child for each of the entries */
   1332 			menu->entries[i]->widget.parent = NULL;
   1333 			ltk_widget_destroy(&menu->entries[i]->widget, shallow, &err);
   1334 		}
   1335 		ltk_free(menu->entries);
   1336 	} else {
   1337 		ltk_menu_remove_all_entries(menu);
   1338 	}
   1339 	ltk_free(menu);
   1340 }
   1341 
   1342 /* TODO: get-index-for-id */
   1343 
   1344 /* [sub]menu <menu id> create */
   1345 static int
   1346 ltk_menu_cmd_create(
   1347     ltk_window *window,
   1348     ltk_menu *menu_unneeded,
   1349     ltk_cmd_token *tokens,
   1350     size_t num_tokens,
   1351     ltk_error *err) {
   1352 	(void)menu_unneeded;
   1353 	ltk_cmdarg_parseinfo cmd[] = {
   1354 		{.type = CMDARG_STRING, .optional = 0},
   1355 		{.type = CMDARG_STRING, .optional = 0},
   1356 		{.type = CMDARG_IGNORE, .optional = 0},
   1357 	};
   1358 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1359 		return 1;
   1360 	if (!ltk_widget_id_free(cmd[1].val.str)) {
   1361 		err->type = ERR_WIDGET_ID_IN_USE;
   1362 		err->arg = 1;
   1363 		return 1;
   1364 	}
   1365 	ltk_menu *menu;
   1366 	if (!strcmp(cmd[0].val.str, "menu")) {
   1367 		menu = ltk_menu_create(window, cmd[1].val.str, 0);
   1368 	} else {
   1369 		menu = ltk_menu_create(window, cmd[1].val.str, 1);
   1370 	}
   1371 	ltk_set_widget((ltk_widget *)menu, cmd[1].val.str);
   1372 	return 0;
   1373 }
   1374 
   1375 /* menu <menu id> insert-entry <entry widget id> <index> */
   1376 static int
   1377 ltk_menu_cmd_insert_entry(
   1378     ltk_window *window,
   1379     ltk_menu *menu,
   1380     ltk_cmd_token *tokens,
   1381     size_t num_tokens,
   1382     ltk_error *err) {
   1383 	ltk_cmdarg_parseinfo cmd[] = {
   1384 		{.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
   1385 		{.type = CMDARG_INT, .min = 0, .max = menu->num_entries, .optional = 0},
   1386 	};
   1387 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1388 		return 1;
   1389 	if (ltk_menu_insert_entry(menu, (ltk_menuentry *)cmd[0].val.widget, cmd[1].val.i, err)) {
   1390 		err->arg = err->type == ERR_WIDGET_IN_CONTAINER ? 0 : 1;
   1391 		return 1;
   1392 	}
   1393 	return 0;
   1394 }
   1395 
   1396 /* menu <menu id> add-entry <entry widget id> */
   1397 static int
   1398 ltk_menu_cmd_add_entry(
   1399     ltk_window *window,
   1400     ltk_menu *menu,
   1401     ltk_cmd_token *tokens,
   1402     size_t num_tokens,
   1403     ltk_error *err) {
   1404 	ltk_cmdarg_parseinfo cmd[] = {
   1405 		{.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENUENTRY, .optional = 0},
   1406 	};
   1407 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1408 		return 1;
   1409 	if (ltk_menu_add_entry(menu, (ltk_menuentry *)cmd[0].val.widget, err)) {
   1410 		err->arg = 0;
   1411 		return 1;
   1412 	}
   1413 	return 0;
   1414 }
   1415 
   1416 /* menu <menu id> remove-entry-index <entry index> */
   1417 static int
   1418 ltk_menu_cmd_remove_entry_index(
   1419     ltk_window *window,
   1420     ltk_menu *menu,
   1421     ltk_cmd_token *tokens,
   1422     size_t num_tokens,
   1423     ltk_error *err) {
   1424 	ltk_cmdarg_parseinfo cmd[] = {
   1425 		{.type = CMDARG_INT, .min = 0, .max = menu->num_entries - 1, .optional = 0},
   1426 	};
   1427 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1428 		return 1;
   1429 	if (!ltk_menu_remove_entry_index(menu, cmd[0].val.i, err)) {
   1430 		err->arg = 0;
   1431 		return 1;
   1432 	}
   1433 	return 0;
   1434 }
   1435 
   1436 /* menu <menu id> remove-entry-id <entry id> */
   1437 static int
   1438 ltk_menu_cmd_remove_entry_id(
   1439     ltk_window *window,
   1440     ltk_menu *menu,
   1441     ltk_cmd_token *tokens,
   1442     size_t num_tokens,
   1443     ltk_error *err) {
   1444 	ltk_cmdarg_parseinfo cmd[] = {
   1445 		{.type = CMDARG_STRING, .optional = 0},
   1446 	};
   1447 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1448 		return 1;
   1449 	if (!ltk_menu_remove_entry_id(menu, cmd[0].val.str, err)) {
   1450 		err->arg = 0;
   1451 		return 1;
   1452 	}
   1453 	return 0;
   1454 }
   1455 
   1456 /* menu <menu id> remove-all-entries */
   1457 static int
   1458 ltk_menu_cmd_remove_all_entries(
   1459     ltk_window *window,
   1460     ltk_menu *menu,
   1461     ltk_cmd_token *tokens,
   1462     size_t num_tokens,
   1463     ltk_error *err) {
   1464 	(void)window;
   1465 	(void)tokens;
   1466 	(void)num_tokens;
   1467 	(void)err;
   1468 	ltk_menu_remove_all_entries(menu);
   1469 	return 0;
   1470 }
   1471 
   1472 /* menuentry <id> create <text> */
   1473 static int
   1474 ltk_menuentry_cmd_create(
   1475     ltk_window *window,
   1476     ltk_menuentry *e_unneeded,
   1477     ltk_cmd_token *tokens,
   1478     size_t num_tokens,
   1479     ltk_error *err) {
   1480 	(void)e_unneeded;
   1481 	ltk_cmdarg_parseinfo cmd[] = {
   1482 		{.type = CMDARG_STRING, .optional = 0},
   1483 		{.type = CMDARG_STRING, .optional = 0},
   1484 		{.type = CMDARG_IGNORE, .optional = 0},
   1485 		{.type = CMDARG_STRING, .optional = 0},
   1486 	};
   1487 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1488 		return 1;
   1489 	if (!ltk_widget_id_free(cmd[1].val.str)) {
   1490 		err->type = ERR_WIDGET_ID_IN_USE;
   1491 		err->arg = 1;
   1492 		return 1;
   1493 	}
   1494 	ltk_menuentry *e = ltk_menuentry_create(window, cmd[1].val.str, cmd[3].val.str);
   1495 	ltk_set_widget((ltk_widget *)e, cmd[1].val.str);
   1496 	return 0;
   1497 }
   1498 
   1499 /* menuentry <menuentry id> attach-submenu <submenu id> */
   1500 static int
   1501 ltk_menuentry_cmd_attach_submenu(
   1502     ltk_window *window,
   1503     ltk_menuentry *e,
   1504     ltk_cmd_token *tokens,
   1505     size_t num_tokens,
   1506     ltk_error *err) {
   1507 	ltk_cmdarg_parseinfo cmd[] = {
   1508 		{.type = CMDARG_WIDGET, .widget_type = LTK_WIDGET_MENU, .optional = 0},
   1509 	};
   1510 	if (ltk_parse_cmd(window, tokens, num_tokens, cmd, LENGTH(cmd), err))
   1511 		return 1;
   1512 	if (ltk_menuentry_attach_submenu(e, (ltk_menu *)cmd[0].val.widget, err)) {
   1513 		/* FIXME: allow setting err->arg to arg before the args given to function */
   1514 		/*err->arg = err->type == ERR_MENU_NOT_SUBMENU ? 0 : -2;*/
   1515 		err->arg = 0;
   1516 		return 1;
   1517 	}
   1518 	return 0;
   1519 }
   1520 
   1521 /* menuentry <menuentry id> detach-submenu */
   1522 static int
   1523 ltk_menuentry_cmd_detach_submenu(
   1524     ltk_window *window,
   1525     ltk_menuentry *e,
   1526     ltk_cmd_token *tokens,
   1527     size_t num_tokens,
   1528     ltk_error *err) {
   1529 	(void)window;
   1530 	(void)tokens;
   1531 	(void)num_tokens;
   1532 	(void)err;
   1533 	ltk_menuentry_detach_submenu(e);
   1534 	return 0;
   1535 }
   1536 
   1537 /* FIXME: sort out menu/submenu - it's weird right now */
   1538 /* FIXME: distinguish between menu/submenu in commands other than create? */
   1539 
   1540 static struct menu_cmd {
   1541 	char *name;
   1542 	int (*func)(ltk_window *, ltk_menu *, ltk_cmd_token *, size_t, ltk_error *);
   1543 	int needs_all;
   1544 } menu_cmds[] = {
   1545 	{"add-entry", &ltk_menu_cmd_add_entry, 0},
   1546 	{"create", &ltk_menu_cmd_create, 1},
   1547 	{"insert-entry", &ltk_menu_cmd_insert_entry, 0},
   1548 	{"remove-all-entries", &ltk_menu_cmd_remove_all_entries, 0},
   1549 	{"remove-entry-index", &ltk_menu_cmd_remove_entry_index, 0},
   1550 	{"remove-entry-id", &ltk_menu_cmd_remove_entry_id, 0},
   1551 };
   1552 
   1553 static struct menuentry_cmd {
   1554 	char *name;
   1555 	int (*func)(ltk_window *, ltk_menuentry *, ltk_cmd_token *, size_t, ltk_error *);
   1556 	int needs_all;
   1557 } menuentry_cmds[] = {
   1558 	{"attach-submenu", &ltk_menuentry_cmd_attach_submenu, 0},
   1559 	{"create", &ltk_menuentry_cmd_create, 1},
   1560 	{"detach-submenu", &ltk_menuentry_cmd_detach_submenu, 0},
   1561 };
   1562 
   1563 GEN_CMD_HELPERS(ltk_menu_cmd, LTK_WIDGET_MENU, ltk_menu, menu_cmds, struct menu_cmd)
   1564 GEN_CMD_HELPERS(ltk_menuentry_cmd, LTK_WIDGET_MENUENTRY, ltk_menuentry, menuentry_cmds, struct menuentry_cmd)