URI: 
       tmenu.c - 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
       ---
       tmenu.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)