menu.c - ltk - GUI toolkit for X11 (WIP)
HTML git clone git://lumidify.org/ltk.git (fast, but not encrypted)
HTML git clone https://lumidify.org/git/ltk.git (encrypted, but very slow)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ltk.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
menu.c (58875B)
---
1 /*
2 * Copyright (c) 2022-2026 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 <stdint.h>
24 #include <string.h>
25 #include <math.h>
26 #include <limits.h>
27
28 #include "event.h"
29 #include "config.h"
30 #include "memory.h"
31 #include "color.h"
32 #include "rect.h"
33 #include "widget.h"
34 #include "ltk.h"
35 #include "util.h"
36 #include "text.h"
37 #include "menu.h"
38 #include "graphics.h"
39
40 #define MAX_MENU_BORDER_WIDTH 10000
41 #define MAX_MENU_PAD 50000
42 #define MAX_MENU_ARROW_SIZE 10000
43
44 #define MAX(a, b) ((a) > (b) ? (a) : (b))
45
46 static struct theme {
47 ltk_color *border;
48 ltk_color *background;
49 ltk_color *scroll_background;
50 ltk_color *scroll_arrow_color;
51
52 ltk_size pad;
53 ltk_size arrow_pad;
54 ltk_size arrow_size;
55 ltk_size border_width;
56 int compress_borders;
57 } menu_theme, submenu_theme;
58
59 static struct entry_theme {
60 int compress_borders;
61 /* FIXME: should border_sides actually factor into
62 size calculation? - probably useless and would
63 just make it more complicated */
64 /* FIXME: allow different values for different states? */
65 ltk_border_sides border_sides;
66
67 ltk_color *text;
68 ltk_color *border;
69 ltk_color *fill;
70
71 ltk_color *text_pressed;
72 ltk_color *border_pressed;
73 ltk_color *fill_pressed;
74
75 ltk_color *text_active;
76 ltk_color *border_active;
77 ltk_color *fill_active;
78
79 ltk_color *text_disabled;
80 ltk_color *border_disabled;
81 ltk_color *fill_disabled;
82
83 char *font;
84 ltk_size text_pad;
85 ltk_size arrow_pad;
86 ltk_size arrow_size;
87 ltk_size border_width;
88 ltk_size font_size;
89 } menu_entry_theme, submenu_entry_theme;
90
91 static void ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r);
92 static void ltk_menu_resize(ltk_widget *self);
93 static void ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
94 static void ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret);
95 static void ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step);
96 static void ltk_menu_scroll_callback(ltk_callback_arg data);
97 static void stop_scrolling(ltk_menu *menu);
98 static ltk_widget_id ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y);
99 static int set_scroll_timer(ltk_menu *menu, int x, int y);
100 static int ltk_menu_mouse_scroll(ltk_widget *self, ltk_scroll_event *event);
101 static void ltk_menu_hide(ltk_widget *self);
102 static void popup_active_menu(ltk_menuentry *e);
103 static void unpopup_active_entry(ltk_menuentry *e);
104 static int ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event);
105 static int ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event);
106 static int ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event);
107 static void ltk_menu_destroy(ltk_widget *self, int shallow);
108
109 static ltk_widget_id ltk_menu_create_base(ltk_widget_id windowid, int is_submenu);
110
111 static int ltk_menu_remove_child(ltk_widget *self, ltk_widget_id childid);
112 static int ltk_menuentry_remove_child(ltk_widget *self, ltk_widget_id childid);
113
114 static ltk_widget_id ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect);
115 static ltk_widget_id ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget_id childid);
116 static ltk_widget_id ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget_id childid);
117 static ltk_widget_id ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget_id childid);
118 static ltk_widget_id ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget_id childid);
119
120 static void ltk_menuentry_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip);
121 static void ltk_menuentry_destroy(ltk_widget *self, int shallow);
122 static void ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state);
123 static int ltk_menuentry_release(ltk_widget *self);
124
125 static ltk_widget_id ltk_menu_prev_child(ltk_widget *self, ltk_widget_id childid);
126 static ltk_widget_id ltk_menu_next_child(ltk_widget *self, ltk_widget_id childid);
127 static ltk_widget_id ltk_menu_first_child(ltk_widget *self);
128 static ltk_widget_id ltk_menu_last_child(ltk_widget *self);
129 static ltk_widget_id ltk_menuentry_get_child(ltk_widget *self);
130
131 /* FIXME: these functions are named really badly */
132 static void recalc_ideal_menu_size_with_notification(ltk_widget *self, ltk_widget_id childid);
133 static void recalc_ideal_menu_size(ltk_menu *menu);
134 static void ltk_menuentry_set_font(ltk_menuentry *entry);
135 static void ltk_menu_recalc_ideal_size(ltk_widget *self);
136 static void ltk_menuentry_recalc_ideal_size(ltk_widget *self);
137 static void ltk_menuentry_recalc_ideal_size_with_notification(ltk_menuentry *entry);
138
139 #define IS_SUBMENU(parent) (parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENU && (LTK_CAST_MENU(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 = <k_menu_mouse_scroll,
146 .motion_notify = <k_menu_motion_notify,
147 .mouse_release = NULL,
148 .mouse_enter = <k_menu_mouse_enter,
149 .mouse_leave = <k_menu_mouse_leave,
150 .get_child_at_pos = <k_menu_get_child_at_pos,
151 .resize = <k_menu_resize,
152 .change_state = NULL,
153 .hide = <k_menu_hide,
154 .draw = <k_menu_draw,
155 .destroy = <k_menu_destroy,
156 .child_size_change = &recalc_ideal_menu_size_with_notification,
157 .remove_child = <k_menu_remove_child,
158 .prev_child = <k_menu_prev_child,
159 .next_child = <k_menu_next_child,
160 .first_child = <k_menu_first_child,
161 .last_child = <k_menu_last_child,
162 .nearest_child = <k_menu_nearest_child,
163 .nearest_child_left = <k_menu_nearest_child_left,
164 .nearest_child_right = <k_menu_nearest_child_right,
165 .nearest_child_above = <k_menu_nearest_child_above,
166 .nearest_child_below = <k_menu_nearest_child_below,
167 .ensure_rect_shown = <k_menu_ensure_rect_shown,
168 .recalc_ideal_size = <k_menu_recalc_ideal_size,
169 .type = LTK_WIDGET_MENU,
170 .flags = LTK_NEEDS_REDRAW,
171 .invalid_signal = LTK_MENU_SIGNAL_INVALID,
172 };
173
174 static struct ltk_widget_vtable entry_vtable = {
175 .key_press = NULL,
176 .key_release = NULL,
177 .mouse_press = NULL,
178 .motion_notify = NULL,
179 .mouse_release = NULL,
180 .release = <k_menuentry_release,
181 .mouse_enter = NULL,
182 .mouse_leave = NULL,
183 .get_child_at_pos = NULL,
184 .resize = NULL,
185 .change_state = <k_menuentry_change_state,
186 .hide = NULL,
187 .draw = <k_menuentry_draw,
188 .destroy = <k_menuentry_destroy,
189 .child_size_change = NULL,
190 .remove_child = <k_menuentry_remove_child,
191 .first_child = <k_menuentry_get_child,
192 .last_child = <k_menuentry_get_child,
193 .recalc_ideal_size = <k_menuentry_recalc_ideal_size,
194 .type = LTK_WIDGET_MENUENTRY,
195 .flags = LTK_NEEDS_REDRAW | LTK_ACTIVATABLE_ALWAYS | LTK_HOVER_IS_ACTIVE,
196 .invalid_signal = LTK_MENUENTRY_SIGNAL_INVALID,
197 };
198
199 /* FIXME: standardize menuentry vs. menu_entry */
200
201 static ltk_theme_parseinfo menu_parseinfo[] = {
202 {"pad", THEME_SIZE, {.size = &menu_theme.pad}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_PAD, 0},
203 {"arrow-pad", THEME_SIZE, {.size = &menu_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
204 {"arrow-size", THEME_SIZE, {.size = &menu_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0},
205 {"border-width", THEME_SIZE, {.size = &menu_theme.border_width}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_BORDER_WIDTH, 0},
206 {"compress-borders", THEME_BOOL, {.b = &menu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
207 {"border", THEME_COLOR, {.color = &menu_theme.border}, {.color = "#000000"}, 0, 0, 0},
208 {"background", THEME_COLOR, {.color = &menu_theme.background}, {.color = "#000000"}, 0, 0, 0},
209 {"scroll-background", THEME_COLOR, {.color = &menu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
210 {"scroll-arrow-color", THEME_COLOR, {.color = &menu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
211 };
212
213 static ltk_theme_parseinfo menu_entry_parseinfo[] = {
214 {"text-pad", THEME_SIZE, {.size = &menu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
215 {"arrow-pad", THEME_SIZE, {.size = &menu_entry_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
216 {"arrow-size", THEME_SIZE, {.size = &menu_entry_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0},
217 {"border-width", THEME_SIZE, {.size = &menu_entry_theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_BORDER_WIDTH, 0},
218 {"border-sides", THEME_BORDERSIDES, {.border = &menu_entry_theme.border_sides}, {.border = LTK_BORDER_ALL}, 0, 0, 0},
219 {"compress-borders", THEME_BOOL, {.b = &menu_entry_theme.compress_borders}, {.b = 1}, 0, 0, 0},
220 {"text", THEME_COLOR, {.color = &menu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
221 {"border", THEME_COLOR, {.color = &menu_entry_theme.border}, {.color = "#339999"}, 0, 0, 0},
222 {"fill", THEME_COLOR, {.color = &menu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
223 {"text-pressed", THEME_COLOR, {.color = &menu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
224 {"border-pressed", THEME_COLOR, {.color = &menu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
225 {"fill-pressed", THEME_COLOR, {.color = &menu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
226 {"text-active", THEME_COLOR, {.color = &menu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
227 {"border-active", THEME_COLOR, {.color = &menu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
228 {"fill-active", THEME_COLOR, {.color = &menu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
229 {"text-disabled", THEME_COLOR, {.color = &menu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
230 {"border-disabled", THEME_COLOR, {.color = &menu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
231 {"fill-disabled", THEME_COLOR, {.color = &menu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
232 {"font-size", THEME_SIZE, {.size = &menu_entry_theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
233 {"font", THEME_STRING, {.str = &menu_entry_theme.font}, {.str = "Monospace"}, 0, 0, 0},
234 };
235
236 static ltk_theme_parseinfo submenu_parseinfo[] = {
237 {"pad", THEME_SIZE, {.size = &submenu_theme.pad}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_PAD, 0},
238 {"arrow-pad", THEME_SIZE, {.size = &submenu_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
239 {"arrow-size", THEME_SIZE, {.size = &submenu_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0},
240 {"border-width", THEME_SIZE, {.size = &submenu_theme.border_width}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_BORDER_WIDTH, 0},
241 {"compress-borders", THEME_BOOL, {.b = &submenu_theme.compress_borders}, {.b = 1}, 0, 0, 0},
242 {"border", THEME_COLOR, {.color = &submenu_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
243 {"background", THEME_COLOR, {.color = &submenu_theme.background}, {.color = "#000000"}, 0, 0, 0},
244 {"scroll-background", THEME_COLOR, {.color = &submenu_theme.scroll_background}, {.color = "#333333"}, 0, 0, 0},
245 {"scroll-arrow-color", THEME_COLOR, {.color = &submenu_theme.scroll_arrow_color}, {.color = "#000000"}, 0, 0, 0},
246 };
247
248 static ltk_theme_parseinfo submenu_entry_parseinfo[] = {
249 {"text-pad", THEME_SIZE, {.size = &submenu_entry_theme.text_pad}, {.size = {.val = 100, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
250 {"arrow-pad", THEME_SIZE, {.size = &submenu_entry_theme.arrow_pad}, {.size = {.val = 50, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_PAD, 0},
251 {"arrow-size", THEME_SIZE, {.size = &submenu_entry_theme.arrow_size}, {.size = {.val = 200, .unit = LTK_UNIT_MM}}, 0, MAX_MENU_ARROW_SIZE, 0},
252 {"border-width", THEME_SIZE, {.size = &submenu_entry_theme.border_width}, {.size = {.val = 0, .unit = LTK_UNIT_PX}}, 0, MAX_MENU_BORDER_WIDTH, 0},
253 {"border-sides", THEME_BORDERSIDES, {.border = &submenu_entry_theme.border_sides}, {.border = LTK_BORDER_NONE}, 0, 0, 0},
254 {"compress-borders", THEME_BOOL, {.b = &submenu_entry_theme.compress_borders}, {.b = 0}, 0, 0, 0},
255 {"text", THEME_COLOR, {.color = &submenu_entry_theme.text}, {.color = "#FFFFFF"}, 0, 0, 0},
256 {"border", THEME_COLOR, {.color = &submenu_entry_theme.border}, {.color = "#FFFFFF"}, 0, 0, 0},
257 {"fill", THEME_COLOR, {.color = &submenu_entry_theme.fill}, {.color = "#113355"}, 0, 0, 0},
258 {"text-pressed", THEME_COLOR, {.color = &submenu_entry_theme.text_pressed}, {.color = "#000000"}, 0, 0, 0},
259 {"border-pressed", THEME_COLOR, {.color = &submenu_entry_theme.border_pressed}, {.color = "#FFFFFF"}, 0, 0, 0},
260 {"fill-pressed", THEME_COLOR, {.color = &submenu_entry_theme.fill_pressed}, {.color = "#113355"}, 0, 0, 0},
261 {"text-active", THEME_COLOR, {.color = &submenu_entry_theme.text_active}, {.color = "#000000"}, 0, 0, 0},
262 {"border-active", THEME_COLOR, {.color = &submenu_entry_theme.border_active}, {.color = "#FFFFFF"}, 0, 0, 0},
263 {"fill-active", THEME_COLOR, {.color = &submenu_entry_theme.fill_active}, {.color = "#738194"}, 0, 0, 0},
264 {"text-disabled", THEME_COLOR, {.color = &submenu_entry_theme.text_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
265 {"border-disabled", THEME_COLOR, {.color = &submenu_entry_theme.border_disabled}, {.color = "#FFFFFF"}, 0, 0, 0},
266 {"fill-disabled", THEME_COLOR, {.color = &submenu_entry_theme.fill_disabled}, {.color = "#292929"}, 0, 0, 0},
267 {"font-size", THEME_SIZE, {.size = &submenu_entry_theme.font_size}, {.size = {.val = 1200, .unit = LTK_UNIT_PT}}, 0, 20000, 0},
268 {"font", THEME_STRING, {.str = &submenu_entry_theme.font}, {.str = "Monospace"}, 0, 0, 0},
269 };
270 void
271 ltk_menu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
272 *p = menu_parseinfo;
273 *len = LENGTH(menu_parseinfo);
274 }
275
276 void
277 ltk_submenu_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
278 *p = submenu_parseinfo;
279 *len = LENGTH(submenu_parseinfo);
280 }
281
282 void
283 ltk_menuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
284 *p = menu_entry_parseinfo;
285 *len = LENGTH(menu_entry_parseinfo);
286 }
287
288 void
289 ltk_submenuentry_get_theme_parseinfo(ltk_theme_parseinfo **p, size_t *len) {
290 *p = submenu_entry_parseinfo;
291 *len = LENGTH(submenu_entry_parseinfo);
292 }
293
294 static void
295 ltk_menuentry_change_state(ltk_widget *self, ltk_widget_state old_state) {
296 ltk_menuentry *e = LTK_CAST_MENUENTRY(self);
297 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
298 ltk_menu *parentm = parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENU ? LTK_CAST_MENU(parent) : NULL;
299 ltk_widget *submenu = ltk_get_widget_or_null_from_id(e->submenu);
300 int in_submenu = IS_SUBMENU(parent);
301 int submenus_opened = parentm && parentm->popup_submenus;
302 if (!(self->state & (LTK_ACTIVE | LTK_PRESSED))) {
303 /* Note: This only has to take care of the submenu that is the direct child
304 of e because ltk_window_set_active_widget already calls change_state for
305 the whole hierarchy */
306 unpopup_active_entry(e);
307 } else if ((self->state & LTK_PRESSED) && !(old_state & LTK_PRESSED) && submenus_opened) {
308 parentm->popup_submenus = 0;
309 } else if (((self->state & LTK_PRESSED) ||
310 ((self->state & LTK_ACTIVE) && (in_submenu || submenus_opened))) &&
311 submenu && submenu->hidden) {
312 popup_active_menu(e);
313 if (parentm)
314 parentm->popup_submenus = 1;
315 }
316 }
317
318 static ltk_widget_id
319 ltk_menuentry_get_child(ltk_widget *self) {
320 ltk_menuentry *e = LTK_CAST_MENUENTRY(self);
321 ltk_widget *submenu = ltk_get_widget_or_null_from_id(e->submenu);
322 return submenu && !submenu->hidden ? e->submenu : LTK_WIDGET_ID_NONE;
323 }
324
325 const char *
326 ltk_menuentry_get_text(ltk_widget_id entryid) {
327 ltk_widget *entryw = ltk_get_widget_from_id(entryid);
328 ltk_menuentry *entry = LTK_CAST_MENUENTRY(entryw);
329 return ltk_text_line_get_text(entry->text_line);
330 }
331
332 static void
333 ltk_menuentry_draw(ltk_widget *self, ltk_surface *draw_surf, int x, int y, ltk_rect clip) {
334 /* FIXME: figure out how hidden should work */
335 if (self->hidden)
336 return;
337 ltk_menuentry *entry = LTK_CAST_MENUENTRY(self);
338 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
339 int in_submenu = IS_SUBMENU(parent);
340 struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
341 int bw = ltk_size_to_pixel(t->border_width, self->last_dpi);
342 int text_pad = ltk_size_to_pixel(t->text_pad, self->last_dpi);
343 int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi);
344 int arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi);
345 ltk_color *text, *border, *fill;
346 if (self->state & LTK_DISABLED) {
347 text = t->text_disabled;
348 border = t->border_disabled;
349 fill = t->fill_disabled;
350 } else if (self->state & LTK_PRESSED) {
351 text = t->text_pressed;
352 border = t->border_pressed;
353 fill = t->fill_pressed;
354 } else if (self->state & LTK_HOVERACTIVE) {
355 text = t->text_active;
356 border = t->border_active;
357 fill = t->fill_active;
358 } else {
359 text = t->text;
360 border = t->border;
361 fill = t->fill;
362 }
363 ltk_rect lrect = self->lrect;
364 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
365 if (clip_final.w <= 0 || clip_final.h <= 0)
366 return;
367 ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
368 ltk_surface_fill_rect(draw_surf, fill, surf_clip);
369
370 int text_w, text_h;
371 ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
372 int text_x = x + text_pad + bw;
373 int text_y = y + text_pad + bw;
374 ltk_rect text_clip = ltk_rect_intersect(surf_clip, (ltk_rect){text_x, text_y, text_w, text_h});
375 ltk_text_line_draw_clipped(entry->text_line, draw_surf, text, text_x, text_y, text_clip);
376
377 if (in_submenu && !LTK_WIDGET_ID_IS_NONE(entry->submenu)) {
378 ltk_point arrow_points[] = {
379 {x + lrect.w - arrow_pad - bw, y + lrect.h / 2},
380 {x + lrect.w - arrow_pad - bw - arrow_size, y + lrect.h / 2 - arrow_size / 2},
381 {x + lrect.w - arrow_pad - bw - arrow_size, y + lrect.h / 2 + arrow_size / 2}
382 };
383 ltk_surface_fill_polygon_clipped(draw_surf, text, arrow_points, LENGTH(arrow_points), surf_clip);
384 }
385 ltk_surface_draw_border_clipped(draw_surf, border, (ltk_rect){x, y, lrect.w, lrect.h}, bw, t->border_sides, surf_clip);
386 self->dirty = 0;
387 }
388
389 static void
390 ltk_menu_draw(ltk_widget *self, ltk_surface *s, int x, int y, ltk_rect clip) {
391 if (self->hidden)
392 return;
393 ltk_menu *menu = LTK_CAST_MENU(self);
394 ltk_rect lrect = self->lrect;
395 ltk_rect clip_final = ltk_rect_intersect(clip, (ltk_rect){0, 0, lrect.w, lrect.h});
396 if (clip_final.w <= 0 || clip_final.h <= 0)
397 return;
398
399 struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
400 int bw = ltk_size_to_pixel(t->border_width, self->last_dpi);
401 int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi);
402 int arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi);
403
404 ltk_rect surf_clip = {x + clip_final.x, y + clip_final.y, clip_final.w, clip_final.h};
405 ltk_surface_fill_rect(s, t->background, surf_clip);
406 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
407 /* FIXME: I guess it could be improved *slightly* by making the clip rect
408 smaller when scrollarrows are shown */
409 /* draw active entry after others so it isn't hidden with compress_borders */
410 ltk_widget *ptr = ltk_get_widget_from_id(ltk_array_get(menu->entries, i));
411 if ((ptr->state & (LTK_ACTIVE | LTK_PRESSED | LTK_HOVER)) && i < ltk_array_len(menu->entries) - 1) {
412 ltk_widget *ptr2 = ltk_get_widget_from_id(ltk_array_get(menu->entries, i + 1));
413 ltk_menuentry_draw(ptr2, s, x + ptr2->lrect.x, y + ptr2->lrect.y, ltk_rect_relative(ptr2->lrect, clip_final));
414 ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
415 i++;
416 } else {
417 ltk_menuentry_draw(ptr, s, x + ptr->lrect.x, y + ptr->lrect.y, ltk_rect_relative(ptr->lrect, clip_final));
418 }
419 }
420
421 /* FIXME: active, pressed states */
422 int sz = arrow_size + arrow_pad * 2;
423 int ww = self->lrect.w;
424 int wh = self->lrect.h;
425 int wx = x, wy = y;
426 int mbw = bw;
427 /* FIXME: handle pathological case where rect is so small that this still draws outside */
428 /* -> this is currently a mess because some parts handle clipping properly, but the scroll arrow drawing doesn't */
429 if (lrect.w < (int)self->ideal_w) {
430 ltk_surface_fill_rect(s, t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, sz, wh - mbw * 2});
431 ltk_surface_fill_rect(s, t->scroll_background, (ltk_rect){wx + ww - sz - mbw, wy + mbw, sz, wh - mbw * 2});
432 ltk_point arrow_points[3] = {
433 {wx + arrow_pad + mbw, wy + wh / 2},
434 {wx + arrow_pad + mbw + arrow_size, wy + wh / 2 - arrow_size / 2},
435 {wx + arrow_pad + mbw + arrow_size, wy + wh / 2 + arrow_size / 2}
436 };
437 ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3);
438 arrow_points[0] = (ltk_point){wx + ww - arrow_pad - mbw, wy + wh / 2};
439 arrow_points[1] = (ltk_point){wx + ww - arrow_pad - mbw - arrow_size, wy + wh / 2 - arrow_size / 2};
440 arrow_points[2] = (ltk_point){wx + ww - arrow_pad - mbw - arrow_size, wy + wh / 2 + arrow_size / 2};
441 ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3);
442 }
443 if (lrect.h < (int)self->ideal_h) {
444 ltk_widget *windoww = ltk_get_widget_from_id(self->window);
445 ltk_window *window = LTK_CAST_WINDOW(windoww);
446 ltk_surface_fill_rect(window->surface, t->scroll_background, (ltk_rect){wx + mbw, wy + mbw, ww - mbw * 2, sz});
447 ltk_surface_fill_rect(window->surface, t->scroll_background, (ltk_rect){wx + mbw, wy + wh - sz - mbw, ww - mbw * 2, sz});
448 ltk_point arrow_points[3] = {
449 {wx + ww / 2, wy + arrow_pad + mbw},
450 {wx + ww / 2 - arrow_size / 2, wy + arrow_pad + mbw + arrow_size},
451 {wx + ww / 2 + arrow_size / 2, wy + arrow_pad + mbw + arrow_size}
452 };
453 ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3);
454 arrow_points[0] = (ltk_point){wx + ww / 2, wy + wh - arrow_pad - mbw};
455 arrow_points[1] = (ltk_point){wx + ww / 2 - arrow_size / 2, wy + wh - arrow_pad - mbw - arrow_size};
456 arrow_points[2] = (ltk_point){wx + ww / 2 + arrow_size / 2, wy + wh - arrow_pad - mbw - arrow_size};
457 ltk_surface_fill_polygon(s, t->scroll_arrow_color, arrow_points, 3);
458 }
459 ltk_surface_draw_border_clipped(s, t->border, (ltk_rect){x, y, lrect.w, lrect.h}, mbw, LTK_BORDER_ALL, surf_clip);
460
461 self->dirty = 0;
462 }
463
464
465 static void
466 ltk_menu_resize(ltk_widget *self) {
467 ltk_menu *menu = LTK_CAST_MENU(self);
468 int max_x, max_y;
469 ltk_menu_get_max_scroll_offset(menu, &max_x, &max_y);
470 if (menu->x_scroll_offset > max_x)
471 menu->x_scroll_offset = max_x;
472 if (menu->y_scroll_offset > max_y)
473 menu->y_scroll_offset = max_y;
474
475 ltk_rect lrect = self->lrect;
476 struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
477 struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
478
479 int bw = ltk_size_to_pixel(t->border_width, self->last_dpi);
480 int pad = ltk_size_to_pixel(t->pad, self->last_dpi);
481 int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi);
482 int raw_arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi);
483 int entry_bw = ltk_size_to_pixel(et->border_width, self->last_dpi);
484
485 int ideal_w = self->ideal_w, ideal_h = self->ideal_h;
486 int arrow_size = arrow_pad * 2 + raw_arrow_size;
487 int start_x = lrect.w < ideal_w ? arrow_size : 0;
488 int start_y = lrect.h < ideal_h ? arrow_size : 0;
489 start_x += bw;
490 start_y += bw;
491
492 int mbw = bw;
493 int cur_abs_x = -(int)menu->x_scroll_offset + start_x + pad;
494 int cur_abs_y = -(int)menu->y_scroll_offset + start_y + pad;
495
496 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
497 ltk_widget *widget = ltk_get_widget_from_id(ltk_array_get(menu->entries, i));
498 widget->lrect.x = cur_abs_x;
499 widget->lrect.y = cur_abs_y;
500 if (menu->is_submenu) {
501 widget->lrect.w = ideal_w - 2 * pad - 2 * mbw;
502 widget->lrect.h = widget->ideal_h;
503 cur_abs_y += widget->ideal_h + pad;
504 if (et->compress_borders)
505 cur_abs_y -= entry_bw;
506 } else {
507 widget->lrect.w = widget->ideal_w;
508 widget->lrect.h = ideal_h - 2 * pad - 2 * mbw;
509 cur_abs_x += widget->ideal_w + pad;
510 if (et->compress_borders)
511 cur_abs_x -= entry_bw;
512 }
513 widget->crect = ltk_rect_intersect((ltk_rect){0, 0, self->crect.w, self->crect.h}, widget->lrect);
514 }
515 self->dirty = 1;
516 ltk_window_invalidate_widget_rect(self->window, self->id);
517 }
518
519 static void
520 ltk_menu_ensure_rect_shown(ltk_widget *self, ltk_rect r) {
521 ltk_menu *menu = LTK_CAST_MENU(self);
522 struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
523 int bw = ltk_size_to_pixel(theme->border_width, self->last_dpi);
524 int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, self->last_dpi);
525 int arrow_size = ltk_size_to_pixel(theme->arrow_size, self->last_dpi);
526 int extra_size = arrow_size + arrow_pad * 2 + bw;
527 int delta = 0;
528 if (self->lrect.w < (int)self->ideal_w && !menu->is_submenu) {
529 if (r.x + r.w > self->lrect.w - extra_size && r.w <= self->lrect.w - 2 * extra_size)
530 delta = r.x - (self->lrect.w - extra_size - r.w);
531 else if (r.x < extra_size || r.w > self->lrect.w - 2 * extra_size)
532 delta = r.x - extra_size;
533 if (delta)
534 ltk_menu_scroll(menu, 0, 0, 0, 1, delta);
535 } else if (self->lrect.h < (int)self->ideal_h && menu->is_submenu) {
536 if (r.y + r.h > self->lrect.h - extra_size && r.h <= self->lrect.h - 2 * extra_size)
537 delta = r.y - (self->lrect.h - extra_size - r.h);
538 else if (r.y < extra_size || r.h > self->lrect.h - 2 * extra_size)
539 delta = r.y - extra_size;
540 if (delta)
541 ltk_menu_scroll(menu, 0, 1, 0, 0, delta);
542 }
543 }
544
545 static void
546 ltk_menu_get_max_scroll_offset(ltk_menu *menu, int *x_ret, int *y_ret) {
547 ltk_widget *self = LTK_CAST_WIDGET(menu);
548 struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
549 int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, self->last_dpi);
550 int arrow_size = ltk_size_to_pixel(theme->arrow_size, self->last_dpi);
551 int extra_size = arrow_size * 2 + arrow_pad * 4;
552 *x_ret = 0;
553 *y_ret = 0;
554 if (self->lrect.w < (int)self->ideal_w) {
555 *x_ret = self->ideal_w - (self->lrect.w - extra_size);
556 }
557 if (self->lrect.h < (int)self->ideal_h) {
558 *y_ret = self->ideal_h - (self->lrect.h - extra_size);
559 }
560 }
561
562 static void
563 ltk_menu_scroll(ltk_menu *menu, char t, char b, char l, char r, int step) {
564 int max_scroll_x, max_scroll_y;
565 ltk_menu_get_max_scroll_offset(menu, &max_scroll_x, &max_scroll_y);
566 double y_old = menu->y_scroll_offset;
567 double x_old = menu->x_scroll_offset;
568 if (t)
569 menu->y_scroll_offset -= step;
570 else if (b)
571 menu->y_scroll_offset += step;
572 else if (l)
573 menu->x_scroll_offset -= step;
574 else if (r)
575 menu->x_scroll_offset += step;
576 if (menu->x_scroll_offset < 0)
577 menu->x_scroll_offset = 0;
578 if (menu->y_scroll_offset < 0)
579 menu->y_scroll_offset = 0;
580 if (menu->x_scroll_offset > max_scroll_x)
581 menu->x_scroll_offset = max_scroll_x;
582 if (menu->y_scroll_offset > max_scroll_y)
583 menu->y_scroll_offset = max_scroll_y;
584 /* FIXME: sensible epsilon? */
585 if (fabs(x_old - menu->x_scroll_offset) > 0.01 ||
586 fabs(y_old - menu->y_scroll_offset) > 0.01) {
587 ltk_widget *self = LTK_CAST_WIDGET(menu);
588 ltk_menu_resize(self);
589 menu->widget.dirty = 1;
590 ltk_window_invalidate_widget_rect(self->window, self->id);
591 }
592 }
593
594 /* FIXME: show scroll arrow disabled when nothing further */
595 static void
596 ltk_menu_scroll_callback(ltk_callback_arg data) {
597 ltk_widget_id id = LTK_CAST_ARG_WIDGET_ID(data);
598 ltk_widget *self = ltk_get_widget_from_id(id);
599 ltk_menu *menu = LTK_CAST_MENU(self);
600 ltk_menu_scroll(
601 menu,
602 menu->scroll_top_hover, menu->scroll_bottom_hover,
603 menu->scroll_left_hover, menu->scroll_right_hover, 2
604 );
605 }
606
607 static void
608 stop_scrolling(ltk_menu *menu) {
609 menu->scroll_top_hover = 0;
610 menu->scroll_bottom_hover = 0;
611 menu->scroll_left_hover = 0;
612 menu->scroll_right_hover = 0;
613 if (menu->scroll_timer_id >= 0)
614 ltk_unregister_timer(menu->scroll_timer_id);
615 }
616
617 /* FIXME: should ideal_w, ideal_h just be int? */
618 static ltk_widget_id
619 ltk_menu_get_child_at_pos(ltk_widget *self, int x, int y) {
620 ltk_menu *menu = LTK_CAST_MENU(self);
621 struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
622 int mbw = ltk_size_to_pixel(t->border_width, self->last_dpi);
623 int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi);
624 int raw_arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi);
625 int arrow_size = raw_arrow_size + arrow_pad * 2;
626 int start_x = mbw, end_x = self->lrect.w - mbw;
627 int start_y = mbw, end_y = self->lrect.h - mbw;
628 if (self->lrect.w < (int)self->ideal_w) {
629 start_x += arrow_size;
630 end_x -= arrow_size;
631 }
632 if (self->lrect.h < (int)self->ideal_h) {
633 start_y += arrow_size;
634 end_y -= arrow_size;
635 }
636 /* FIXME: use crect for this */
637 if (!ltk_collide_rect((ltk_rect){start_x, start_y, end_x - start_x, end_y - start_y}, x, y))
638 return LTK_WIDGET_ID_NONE;
639
640 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
641 ltk_widget_id childid = ltk_array_get(menu->entries, i);
642 ltk_widget *child = ltk_get_widget_from_id(childid);
643 if (ltk_collide_rect(child->crect, x, y))
644 return childid;
645 }
646 return LTK_WIDGET_ID_NONE;
647 }
648
649 /* FIXME: make sure timers are always destroyed when widget is destroyed */
650 static int
651 set_scroll_timer(ltk_menu *menu, int x, int y) {
652 ltk_widget *self = LTK_CAST_WIDGET(menu);
653 /* this check probably isn't necessary, but whatever */
654 if (x < 0 || y < 0 || x >= self->lrect.w || y >= self->lrect.h)
655 return 0;
656 int t = 0, b = 0, l = 0,r = 0;
657 struct theme *theme = menu->is_submenu ? &submenu_theme : &menu_theme;
658 int arrow_pad = ltk_size_to_pixel(theme->arrow_pad, self->last_dpi);
659 int raw_arrow_size = ltk_size_to_pixel(theme->arrow_size, self->last_dpi);
660 int arrow_size = raw_arrow_size + arrow_pad * 2;
661 if (self->lrect.w < (int)self->ideal_w) {
662 if (x < arrow_size)
663 l = 1;
664 else if (x > self->lrect.w - arrow_size)
665 r = 1;
666 }
667 if (self->lrect.h < (int)self->ideal_h) {
668 if (y < arrow_size)
669 t = 1;
670 else if (y > self->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(LTK_MAKE_ARG_WIDGET(self));
684 menu->scroll_timer_id = ltk_register_timer(0, 300, <k_menu_scroll_callback, LTK_MAKE_ARG_WIDGET_ID(self->id));
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_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
694 int in_submenu = IS_SUBMENU(parent);
695 int keep_popup = parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENU && (LTK_CAST_MENU(parent))->popup_submenus;
696 if (in_submenu || !keep_popup) {
697 ltk_window_unregister_all_popups(self->window);
698 }
699 ltk_widget_emit_signal(self->id, LTK_MENUENTRY_SIGNAL_PRESSED, LTK_EMPTY_ARGLIST);
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_CAST_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 /* FIXME: make sure timers are always removed when popups are removed */
722 static void
723 ltk_menu_hide(ltk_widget *self) {
724 ltk_menu *menu = LTK_CAST_MENU(self);
725 if (menu->scroll_timer_id >= 0)
726 ltk_unregister_timer(menu->scroll_timer_id);
727 menu->scroll_bottom_hover = menu->scroll_top_hover = 0;
728 menu->scroll_left_hover = menu->scroll_right_hover = 0;
729 ltk_window_unregister_popup(self->window, self->id);
730 ltk_window_invalidate_widget_rect(self->window, self->id);
731 /* FIXME: this is really ugly/hacky */
732 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
733 ltk_widget *pparent = parent ? ltk_get_widget_or_null_from_id(parent->parent) : NULL;
734 if (menu->unpopup_submenus_on_hide && parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENUENTRY &&
735 pparent && LTK_WIDGET_TYPE(pparent) == LTK_WIDGET_MENU) {
736 (LTK_CAST_MENU(pparent))->popup_submenus = 0;
737 }
738 menu->unpopup_submenus_on_hide = 1;
739 }
740
741 /* FIXME: hacky because entries need to know about their parents to be able to properly position the popup */
742 static void
743 popup_active_menu(ltk_menuentry *e) {
744 ltk_widget *self = LTK_CAST_WIDGET(e);
745 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(e->submenu);
746 if (!submenuw)
747 return;
748 ltk_menu *submenu = LTK_CAST_MENU(submenuw);
749 int in_submenu = 0, was_opened_left = 0;
750 ltk_rect menu_rect = self->lrect;
751 ltk_point entry_global = ltk_widget_pos_to_global(self, 0, 0);
752 ltk_point menu_global;
753 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
754 if (parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENU) {
755 ltk_menu *menu = LTK_CAST_MENU(parent);
756 in_submenu = menu->is_submenu;
757 was_opened_left = menu->was_opened_left;
758 menu_rect = LTK_CAST_WIDGET(menu)->lrect;
759 menu_global = ltk_widget_pos_to_global(parent, 0, 0);
760 } else {
761 menu_global = ltk_widget_pos_to_global(self, 0, 0);
762 }
763 ltk_widget *windoww = ltk_get_widget_from_id(self->window);
764 ltk_window *window = LTK_CAST_WINDOW(windoww);
765 int win_w = window->rect.w;
766 int win_h = window->rect.h;
767 ltk_widget_recalc_ideal_size(submenuw);
768 int ideal_w = submenuw->ideal_w;
769 int ideal_h = submenuw->ideal_h;
770 int x_final = 0, y_final = 0, w_final = ideal_w, h_final = ideal_h;
771 int submenu_bw = ltk_size_to_pixel(submenu_theme.border_width, self->last_dpi);
772 int submenu_pad = ltk_size_to_pixel(submenu_theme.pad, self->last_dpi);
773 int menu_bw = ltk_size_to_pixel(menu_theme.border_width, self->last_dpi);
774 if (in_submenu) {
775 int space_left = menu_global.x;
776 int space_right = win_w - (menu_global.x + menu_rect.w);
777 int x_right = menu_global.x + menu_rect.w;
778 int x_left = menu_global.x - ideal_w;
779 if (submenu_theme.compress_borders) {
780 x_right -= submenu_bw;
781 x_left += submenu_bw;
782 }
783 if (was_opened_left) {
784 if (x_left >= 0) {
785 x_final = x_left;
786 submenu->was_opened_left = 1;
787 } else if (space_right >= ideal_w) {
788 x_final = x_right;
789 submenu->was_opened_left = 0;
790 } else {
791 x_final = 0;
792 if (win_w < ideal_w)
793 w_final = win_w;
794 submenu->was_opened_left = 1;
795 }
796 } else {
797 if (space_right >= ideal_w) {
798 x_final = x_right;
799 submenu->was_opened_left = 0;
800 } else if (space_left >= ideal_w) {
801 x_final = x_left;
802 submenu->was_opened_left = 1;
803 } else {
804 x_final = win_w - ideal_w;
805 if (x_final < 0) {
806 x_final = 0;
807 w_final = win_w;
808 }
809 submenu->was_opened_left = 0;
810 }
811 }
812 /* subtract padding and border width so the actual entries are at the right position */
813 y_final = entry_global.y - submenu_pad - submenu_bw;
814 if (y_final + ideal_h > win_h)
815 y_final = win_h - ideal_h;
816 if (y_final < 0) {
817 y_final = 0;
818 h_final = win_h;
819 }
820 } else {
821 int space_top = menu_global.y;
822 int space_bottom = win_h - (menu_global.y + menu_rect.h);
823 int y_top = menu_global.y - ideal_h;
824 int y_bottom = menu_global.y + menu_rect.h;
825 if (menu_theme.compress_borders) {
826 y_top += menu_bw;
827 y_bottom -= menu_bw;
828 }
829 if (space_top > space_bottom) {
830 y_final = y_top;
831 if (y_final < 0) {
832 y_final = 0;
833 h_final = menu_rect.y;
834 }
835 submenu->was_opened_above = 1;
836 } else {
837 y_final = y_bottom;
838 if (space_bottom < ideal_h)
839 h_final = space_bottom;
840 submenu->was_opened_above = 0;
841 }
842 /* FIXME: maybe threshold so there's always at least a part of
843 the menu contents shown (instead of maybe just a few pixels) */
844 /* pathological case where window is way too small */
845 if (h_final <= 0) {
846 y_final = 0;
847 h_final = win_h;
848 }
849 x_final = entry_global.x;
850 if (x_final + ideal_w > win_w)
851 x_final = win_w - ideal_w;
852 if (x_final < 0) {
853 x_final = 0;
854 w_final = win_w;
855 }
856 }
857 /* reset everything just in case */
858 submenu->x_scroll_offset = submenu->y_scroll_offset = 0;
859 submenu->scroll_top_hover = submenu->scroll_bottom_hover = 0;
860 submenu->scroll_left_hover = submenu->scroll_right_hover = 0;
861 submenu->widget.lrect.x = x_final;
862 submenu->widget.lrect.y = y_final;
863 submenu->widget.lrect.w = w_final;
864 submenu->widget.lrect.h = h_final;
865 submenu->widget.crect = submenuw->lrect;
866 submenu->widget.dirty = 1;
867 submenu->widget.hidden = 0;
868 submenu->popup_submenus = 0;
869 submenu->unpopup_submenus_on_hide = 1;
870 ltk_widget_resize(submenuw);
871 ltk_window_register_popup(self->window, e->submenu);
872 ltk_window_invalidate_widget_rect(submenuw->window, submenuw->id);
873 }
874
875 static void
876 unpopup_active_entry(ltk_menuentry *e) {
877 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(e->submenu);
878 if (submenuw && !submenuw->hidden) {
879 ltk_menu *submenu = LTK_CAST_MENU(submenuw);
880 submenu->unpopup_submenus_on_hide = 0;
881 ltk_widget_hide(submenuw);
882 }
883 }
884
885 static int
886 ltk_menu_motion_notify(ltk_widget *self, ltk_motion_event *event) {
887 set_scroll_timer(LTK_CAST_MENU(self), event->x, event->y);
888 return 1;
889 }
890
891 static int
892 ltk_menu_mouse_enter(ltk_widget *self, ltk_motion_event *event) {
893 set_scroll_timer(LTK_CAST_MENU(self), event->x, event->y);
894 return 1;
895 }
896
897 static int
898 ltk_menu_mouse_leave(ltk_widget *self, ltk_motion_event *event) {
899 (void)event;
900 stop_scrolling(LTK_CAST_MENU(self));
901 return 1;
902 }
903
904 static void
905 ltk_menu_recalc_ideal_size(ltk_widget *self) {
906 ltk_menu *menu = LTK_CAST_MENU(self);
907 recalc_ideal_menu_size(menu);
908 }
909
910 static ltk_widget_id
911 ltk_menu_create_base(ltk_widget_id windowid, int is_submenu) {
912 ltk_menu *menu = ltk_malloc(sizeof(ltk_menu));
913 ltk_widget_id id = ltk_initialize_widget(LTK_CAST_WIDGET(menu), windowid, &vtable, 0, 0);
914 ltk_widget *self = LTK_CAST_WIDGET(menu);
915
916 int menu_pad = ltk_size_to_pixel(menu_theme.pad, self->last_dpi);
917 self->ideal_w = menu_pad;
918 self->ideal_h = menu_pad;
919 self->dirty = 1;
920
921 menu->entries = ltk_array_create(widget_id, 1);
922 menu->x_scroll_offset = menu->y_scroll_offset = 0;
923 menu->is_submenu = is_submenu;
924 menu->was_opened_left = 0;
925 menu->was_opened_above = 0;
926 menu->scroll_timer_id = -1;
927 menu->scroll_top_hover = menu->scroll_bottom_hover = 0;
928 menu->scroll_left_hover = menu->scroll_right_hover = 0;
929 menu->popup_submenus = 0;
930 menu->unpopup_submenus_on_hide = 1;
931 /* FIXME: hide widget by default so recalc doesn't cause
932 unnecessary redrawing */
933 recalc_ideal_menu_size(menu);
934
935 return id;
936 }
937
938 ltk_widget_id
939 ltk_menu_create(ltk_widget_id windowid) {
940 return ltk_menu_create_base(windowid, 0);
941 }
942
943 ltk_widget_id
944 ltk_submenu_create(ltk_widget_id windowid) {
945 return ltk_menu_create_base(windowid, 1);
946 }
947
948 static int
949 insert_entry(ltk_widget_id menuid, ltk_widget_id menuentryid, size_t idx) {
950 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
951 ltk_menu *menu = LTK_CAST_MENU(menuw);
952 if (!menu->entries)
953 menu->entries = ltk_array_create(widget_id, 1);
954 if (idx > ltk_array_len(menu->entries))
955 return 1;
956 ltk_array_insert(widget_id, menu->entries, idx, menuentryid);
957 return 0;
958 }
959
960 static void
961 recalc_ideal_menu_size(ltk_menu *menu) {
962 struct theme *t = menu->is_submenu ? &submenu_theme : &menu_theme;
963 struct entry_theme *et = menu->is_submenu ? &submenu_entry_theme : &menu_entry_theme;
964
965 ltk_widget *self = LTK_CAST_WIDGET(menu);
966 int bw = ltk_size_to_pixel(t->border_width, self->last_dpi);
967 int pad = ltk_size_to_pixel(t->pad, self->last_dpi);
968 int entry_bw = ltk_size_to_pixel(et->border_width, self->last_dpi);
969
970 self->ideal_w = self->ideal_h = pad + bw * 2;
971 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
972 ltk_widget_id id = ltk_array_get(menu->entries, i);
973 ltk_widget *ew = ltk_get_widget_from_id(id);
974 ltk_widget_recalc_ideal_size(ew);
975 if (menu->is_submenu) {
976 self->ideal_w = MAX((pad + bw) * 2 + (int)ew->ideal_w, (int)self->ideal_w);
977 self->ideal_h += ew->ideal_h + pad;
978 if (et->compress_borders && i != 0)
979 self->ideal_h -= entry_bw;
980 } else {
981 self->ideal_w += ew->ideal_w + pad;
982 if (et->compress_borders && i != 0)
983 self->ideal_w -= entry_bw;
984 self->ideal_h = MAX((pad + bw) * 2 + (int)ew->ideal_h, (int)self->ideal_h);
985 }
986 }
987 }
988
989 /* FIXME: rename these functions */
990 static void
991 recalc_ideal_menu_size_with_notification(ltk_widget *self, ltk_widget_id childid) {
992 ltk_menu *menu = LTK_CAST_MENU(self);
993 ltk_widget *child = ltk_get_widget_from_id(childid);
994 /* If widget with size change is submenu, it doesn't affect this menu */
995 if (child && LTK_WIDGET_TYPE(child) == LTK_WIDGET_MENU) {
996 ltk_widget_resize(child);
997 return;
998 }
999 unsigned int old_w = self->ideal_w, old_h = self->ideal_h;
1000 recalc_ideal_menu_size(menu);
1001 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1002 if ((old_w != self->ideal_w || old_h != self->ideal_h) &&
1003 parent && parent->vtable->child_size_change) {
1004 parent->vtable->child_size_change(parent, self->id);
1005 } else {
1006 ltk_menu_resize(self);
1007 }
1008 self->dirty = 1;
1009 if (!self->hidden)
1010 ltk_window_invalidate_widget_rect(self->window, self->id);
1011 }
1012
1013 static void
1014 recalc_ideal_menuentry_size(ltk_menuentry *entry) {
1015 ltk_widget *self = LTK_CAST_WIDGET(entry);
1016 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1017 int in_submenu = IS_SUBMENU(parent);
1018 struct entry_theme *t = in_submenu ? &submenu_entry_theme : &menu_entry_theme;
1019 int bw = ltk_size_to_pixel(t->border_width, self->last_dpi);
1020 int text_pad = ltk_size_to_pixel(t->text_pad, self->last_dpi);
1021 int arrow_pad = ltk_size_to_pixel(t->arrow_pad, self->last_dpi);
1022 int arrow_size = ltk_size_to_pixel(t->arrow_size, self->last_dpi);
1023 int extra_size = (in_submenu && !LTK_WIDGET_ID_IS_NONE(entry->submenu)) ? arrow_pad * 2 + arrow_size : 0;
1024
1025 int text_w, text_h;
1026 ltk_text_line_get_size(entry->text_line, &text_w, &text_h);
1027 self->ideal_w = text_w + extra_size + (text_pad + bw) * 2;
1028 self->ideal_h = MAX(text_h + text_pad * 2, extra_size) + bw * 2;
1029 }
1030
1031 static void
1032 ltk_menuentry_recalc_ideal_size(ltk_widget *self) {
1033 ltk_menuentry *entry = LTK_CAST_MENUENTRY(self);
1034 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1035 struct entry_theme *t = IS_SUBMENU(parent) ? &submenu_entry_theme : &menu_entry_theme;
1036 int font_size = ltk_size_to_pixel(t->font_size, self->last_dpi);
1037 ltk_text_line_set_font_size(entry->text_line, font_size);
1038 recalc_ideal_menuentry_size(entry);
1039 }
1040
1041 static void
1042 ltk_menuentry_set_font(ltk_menuentry *entry) {
1043 ltk_widget *self = LTK_CAST_WIDGET(entry);
1044 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1045 struct entry_theme *t = IS_SUBMENU(parent) ? &submenu_entry_theme : &menu_entry_theme;
1046 int font_size = ltk_size_to_pixel(t->font_size, self->last_dpi);
1047 ltk_text_line_set_font(entry->text_line, t->font, font_size);
1048 }
1049
1050 static void
1051 ltk_menuentry_recalc_ideal_size_with_notification(ltk_menuentry *entry) {
1052 recalc_ideal_menuentry_size(entry);
1053 /* FIXME: only call if something changed */
1054 ltk_widget *self = LTK_CAST_WIDGET(entry);
1055 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1056 if (parent && parent->vtable->child_size_change) {
1057 parent->vtable->child_size_change(parent, self->id);
1058 }
1059 }
1060
1061 ltk_widget_id
1062 ltk_menuentry_create(ltk_widget_id windowid, const char *text) {
1063 ltk_menuentry *e = ltk_malloc(sizeof(ltk_menuentry));
1064 ltk_widget *self = LTK_CAST_WIDGET(e);
1065 ltk_widget_id id = ltk_initialize_widget(self, windowid, &entry_vtable, 0, 0);
1066 e->text_line = ltk_text_line_create_const_text_default(
1067 menu_entry_theme.font,
1068 ltk_size_to_pixel(menu_entry_theme.font_size, self->last_dpi),
1069 text, -1
1070 );
1071 e->submenu = LTK_WIDGET_ID_NONE;
1072 /* Note: This is only set as a dummy value! The actual ideal size can't
1073 be set until it is part of a menu because it needs to know which
1074 theme it should use */
1075 recalc_ideal_menuentry_size(e);
1076 self->dirty = 1;
1077 return id;
1078 }
1079
1080 static int
1081 ltk_menuentry_remove_child(ltk_widget *self, ltk_widget_id widgetid) {
1082 ltk_menuentry *e = LTK_CAST_MENUENTRY(self);
1083 if (LTK_WIDGET_ID_IS_NONE(widgetid) || !LTK_WIDGET_ID_EQUAL(widgetid, e->submenu))
1084 return 1;
1085 ltk_widget *widget = ltk_get_widget_from_id(widgetid);
1086 widget->parent = LTK_WIDGET_ID_NONE;
1087 e->submenu = LTK_WIDGET_ID_NONE;
1088 ltk_menuentry_recalc_ideal_size_with_notification(e);
1089 return 0;
1090 }
1091
1092 static void
1093 ltk_menuentry_destroy(ltk_widget *self, int shallow) {
1094 ltk_menuentry *e = LTK_CAST_MENUENTRY(self);
1095 ltk_text_line_destroy(e->text_line);
1096 /* FIXME: function to call when parent is destroyed */
1097 /* also function to call when parent added */
1098 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(e->submenu);
1099 if (submenuw) {
1100 submenuw->parent = LTK_WIDGET_ID_NONE;
1101 if (!shallow)
1102 ltk_widget_destroy(submenuw, shallow);
1103 }
1104 ltk_free(e);
1105 }
1106
1107 int
1108 ltk_menu_insert_entry(ltk_widget_id menuid, ltk_widget_id entryid, size_t idx) {
1109 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1110 ltk_widget *entryw = ltk_get_widget_from_id(entryid);
1111 if (!LTK_WIDGET_ID_IS_NONE(entryw->parent))
1112 return 1; /* already child of some widget */
1113 if (insert_entry(menuid, entryid, idx))
1114 return 2; /* invalid index */
1115 entryw->parent = menuid;
1116 /* the theme may have changed if the entry switched between menu and submenu */
1117 ltk_menuentry_set_font(LTK_CAST_MENUENTRY(entryw));
1118 ltk_menuentry_recalc_ideal_size_with_notification(LTK_CAST_MENUENTRY(entryw));
1119 recalc_ideal_menu_size_with_notification(menuw, entryid);
1120 menuw->dirty = 1;
1121 return 0;
1122 }
1123
1124 int
1125 ltk_menu_add_entry(ltk_widget_id menuid, ltk_widget_id entryid) {
1126 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1127 ltk_menu *menu = LTK_CAST_MENU(menuw);
1128 return ltk_menu_insert_entry(menuid, entryid, ltk_array_len(menu->entries));
1129 }
1130
1131 /* FIXME: maybe allow any menu and just change is_submenu (also need to recalculate size then) */
1132 int
1133 ltk_menuentry_attach_submenu(ltk_widget_id entryid, ltk_widget_id submenuid) {
1134 ltk_widget *entryw = ltk_get_widget_from_id(entryid);
1135 ltk_menuentry *entry = LTK_CAST_MENUENTRY(entryw);
1136 ltk_widget *submenuw = ltk_get_widget_from_id(submenuid);
1137 ltk_menu *submenu = LTK_CAST_MENU(submenuw);
1138 if (!submenu->is_submenu)
1139 return 1; /* menu is not submenu */
1140 else if (!LTK_WIDGET_ID_IS_NONE(entry->submenu))
1141 return 2; /* entry already contains submenu */
1142 entry->submenu = submenuid;
1143 ltk_menuentry_recalc_ideal_size_with_notification(entry);
1144 entryw->dirty = 1;
1145 submenuw->hidden = 1;
1146 submenuw->parent = entryid;
1147 if (!entryw->hidden)
1148 ltk_window_invalidate_widget_rect(entryw->window, entryw->id);
1149 return 0;
1150 }
1151
1152 /* FIXME: hide all entries when menu hidden? */
1153
1154 ltk_widget_id
1155 ltk_menu_remove_entry_index(ltk_widget_id menuid, size_t idx) {
1156 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1157 ltk_menu *menu = LTK_CAST_MENU(menuw);
1158 if (idx >= ltk_array_len(menu->entries))
1159 return LTK_WIDGET_ID_NONE; /* invalid index */
1160 ltk_widget_id entryid = ltk_array_get(menu->entries, idx);
1161 ltk_widget *entry = ltk_get_widget_from_id(entryid);
1162 entry->parent = LTK_WIDGET_ID_NONE;
1163 /* I don't think this is needed because the entry isn't shown
1164 anywhere. Its size will be recalculated once it is added
1165 to a menu again. */
1166 /* ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[idx]); */
1167 ltk_array_delete(widget_id, menu->entries, idx, 1);
1168 recalc_ideal_menu_size_with_notification(menuw, LTK_WIDGET_ID_NONE);
1169 return entryid;
1170 }
1171
1172 size_t
1173 ltk_menu_get_entry_index(ltk_widget_id menuid, ltk_widget_id entryid) {
1174 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1175 ltk_menu *menu = LTK_CAST_MENU(menuw);
1176 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
1177 ltk_widget_id id = ltk_array_get(menu->entries, i);
1178 if (LTK_WIDGET_ID_EQUAL(id, entryid))
1179 return i;
1180 }
1181 return SIZE_MAX;
1182 }
1183
1184 size_t
1185 ltk_menu_get_num_entries(ltk_widget_id menuid) {
1186 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1187 ltk_menu *menu = LTK_CAST_MENU(menuw);
1188 return ltk_array_len(menu->entries);
1189 }
1190
1191 ltk_widget_id
1192 ltk_menu_get_entry(ltk_widget_id menuid, size_t idx) {
1193 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1194 ltk_menu *menu = LTK_CAST_MENU(menuw);
1195 if (idx >= ltk_array_len(menu->entries))
1196 return LTK_WIDGET_ID_NONE;
1197 return ltk_array_get(menu->entries, idx);
1198 }
1199
1200 int
1201 ltk_menu_remove_entry(ltk_widget_id menuid, ltk_widget_id entryid) {
1202 size_t idx = ltk_menu_get_entry_index(menuid, entryid);
1203 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1204 ltk_menu *menu = LTK_CAST_MENU(menuw);
1205 if (idx >= ltk_array_len(menu->entries))
1206 return 1;
1207 ltk_widget_id ret = ltk_menu_remove_entry_index(menuid, idx);
1208 if (LTK_WIDGET_ID_IS_NONE(ret)) /* shouldn't be possible */
1209 return 1;
1210 return 0;
1211 }
1212
1213 static int
1214 ltk_menu_remove_child(ltk_widget *self, ltk_widget_id childid) {
1215 /* FIXME: this is kind of ugly - it's just there to avoid
1216 aborting if a different widget type is given */
1217 ltk_widget *child = ltk_get_widget_from_id(childid);
1218 if (LTK_WIDGET_TYPE(child) != LTK_WIDGET_MENUENTRY)
1219 return 1;
1220 return ltk_menu_remove_entry(self->id, childid);
1221 }
1222
1223 /* TODO: add function to also destroy the entries when removing them */
1224 /* -> note: ltk_menu_remove_all_entries is used by ltk_menu_destroy, so
1225 that has to be updated if this changes */
1226 void
1227 ltk_menu_remove_all_entries(ltk_widget_id menuid) {
1228 ltk_widget *menuw = ltk_get_widget_from_id(menuid);
1229 ltk_menu *menu = LTK_CAST_MENU(menuw);
1230 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
1231 ltk_widget *child = ltk_get_widget_from_id(ltk_array_get(menu->entries, i));
1232 child->parent = LTK_WIDGET_ID_NONE;
1233 /* I don't think this is needed because the entry isn't shown
1234 anywhere. Its size will be recalculated once it is added
1235 to a menu again. */
1236 /* ltk_menuentry_recalc_ideal_size_with_notification(menu->entries[i]); */
1237 }
1238 ltk_array_reset(widget_id, menu->entries);
1239 recalc_ideal_menu_size_with_notification(menuw, LTK_WIDGET_ID_NONE);
1240 }
1241
1242 static ltk_widget_id
1243 ltk_menu_nearest_child(ltk_widget *self, ltk_rect rect) {
1244 ltk_menu *menu = LTK_CAST_MENU(self);
1245 ltk_widget_id minw = LTK_WIDGET_ID_NONE;
1246 int min_dist = INT_MAX;
1247 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
1248 ltk_widget *child = ltk_get_widget_from_id(ltk_array_get(menu->entries, i));
1249 ltk_rect r = child->lrect;
1250 /* FIXME: maybe simplify this since all entries are
1251 laid out horizontal/vertical anyways */
1252 int dist = ltk_rect_fakedist(rect, r);
1253 if (dist < min_dist) {
1254 min_dist = dist;
1255 minw = ltk_array_get(menu->entries, i);
1256 }
1257 }
1258 return minw;
1259 }
1260
1261 /* FIXME: These need to be updated if all menus are allowed to be horizontal or vertical! */
1262 /* FIXME: this doesn't work properly when parent and child are both on the same side,
1263 but I guess there's no good way to fix that */
1264 /* FIXME: behavior is a bit weird when e.g. moving down when every active menu entry in hierarchy
1265 is already at bottom of respective menu - the top-level menu will give the first submenu in
1266 the current active hierarchy as child widget again, and nearest_child on that submenu will
1267 (probably) give the bottom widget again, so nothing changes except that all submenus except
1268 for the first and second one disappear */
1269 static ltk_widget_id
1270 ltk_menu_nearest_child_left(ltk_widget *self, ltk_widget_id childid) {
1271 ltk_menu *menu = LTK_CAST_MENU(self);
1272 ltk_widget *entryw = ltk_get_widget_from_id(childid);
1273 ltk_menuentry *entry = LTK_CAST_MENUENTRY(entryw);
1274 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(entry->submenu);
1275 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1276 ltk_widget_id left = LTK_WIDGET_ID_NONE;
1277 if (!menu->is_submenu) {
1278 left = ltk_menu_prev_child(self, childid);
1279 } else if (submenuw && !submenuw->hidden && LTK_CAST_MENU(submenuw)->was_opened_left) {
1280 left = submenuw->id;
1281 } else if (parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENUENTRY) {
1282 ltk_widget *grandparent = ltk_get_widget_or_null_from_id(parent->parent);
1283 if (!menu->was_opened_left && IS_SUBMENU(grandparent)) {
1284 left = self->parent;
1285 } else if (!IS_SUBMENU(grandparent) && LTK_WIDGET_TYPE(grandparent) == LTK_WIDGET_MENU) {
1286 left = ltk_menu_prev_child(grandparent, self->parent);
1287 }
1288 }
1289 return left;
1290 }
1291
1292 static ltk_widget_id
1293 ltk_menu_nearest_child_right(ltk_widget *self, ltk_widget_id childid) {
1294 ltk_menu *menu = LTK_CAST_MENU(self);
1295 ltk_widget *entryw = ltk_get_widget_from_id(childid);
1296 ltk_menuentry *entry = LTK_CAST_MENUENTRY(entryw);
1297 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(entry->submenu);
1298 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1299 ltk_widget_id right = LTK_WIDGET_ID_NONE;
1300 if (!menu->is_submenu) {
1301 right = ltk_menu_next_child(self, childid);
1302 } else if (submenuw && !submenuw->hidden && !LTK_CAST_MENU(submenuw)->was_opened_left) {
1303 right = entry->submenu;
1304 } else if (parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENUENTRY) {
1305 ltk_widget *grandparent = ltk_get_widget_or_null_from_id(parent->parent);
1306 if (menu->was_opened_left && IS_SUBMENU(grandparent)) {
1307 right = self->parent;
1308 } else if (!IS_SUBMENU(grandparent) && LTK_WIDGET_TYPE(grandparent) == LTK_WIDGET_MENU) {
1309 right = ltk_menu_next_child(grandparent, self->parent);
1310 }
1311 }
1312 return right;
1313 }
1314
1315 static ltk_widget_id
1316 ltk_menu_nearest_child_above(ltk_widget *self, ltk_widget_id childid) {
1317 ltk_menu *menu = LTK_CAST_MENU(self);
1318 ltk_widget *childw = ltk_get_widget_from_id(childid);
1319 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1320 ltk_widget_id above = LTK_WIDGET_ID_NONE;
1321 if (menu->is_submenu) {
1322 above = ltk_menu_prev_child(self, childid);
1323 if (LTK_WIDGET_ID_IS_NONE(above) && parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENUENTRY) {
1324 ltk_widget *grandparent = ltk_get_widget_or_null_from_id(parent->parent);
1325 if (grandparent && LTK_WIDGET_TYPE(grandparent) == LTK_WIDGET_MENU) {
1326 ltk_menu *gpmenu = LTK_CAST_MENU(grandparent);
1327 if (!menu->was_opened_above && !gpmenu->is_submenu) {
1328 above = self->parent;
1329 } else if (gpmenu->is_submenu) {
1330 above = ltk_menu_prev_child(grandparent, self->parent);
1331 }
1332 }
1333 }
1334 } else if (LTK_WIDGET_TYPE(childw) == LTK_WIDGET_MENUENTRY) {
1335 ltk_menuentry *e = LTK_CAST_MENUENTRY(childw);
1336 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(e->submenu);
1337 if (submenuw && !submenuw->hidden && LTK_CAST_MENU(submenuw)->was_opened_above) {
1338 above = e->submenu;
1339 }
1340 }
1341 return above;
1342 }
1343
1344 static ltk_widget_id
1345 ltk_menu_nearest_child_below(ltk_widget *self, ltk_widget_id childid) {
1346 ltk_menu *menu = LTK_CAST_MENU(self);
1347 ltk_widget *childw = ltk_get_widget_from_id(childid);
1348 ltk_widget *parent = ltk_get_widget_or_null_from_id(self->parent);
1349 ltk_widget_id below = LTK_WIDGET_ID_NONE;
1350 if (menu->is_submenu) {
1351 below = ltk_menu_next_child(self, childid);
1352 if (LTK_WIDGET_ID_IS_NONE(below) && parent && LTK_WIDGET_TYPE(parent) == LTK_WIDGET_MENUENTRY) {
1353 ltk_widget *grandparent = ltk_get_widget_or_null_from_id(parent->parent);
1354 if (grandparent && LTK_WIDGET_TYPE(grandparent) == LTK_WIDGET_MENU) {
1355 ltk_menu *gpmenu = LTK_CAST_MENU(grandparent);
1356 if (menu->was_opened_above && !gpmenu->is_submenu) {
1357 below = self->parent;
1358 } else if (gpmenu->is_submenu) {
1359 below = ltk_menu_next_child(grandparent, self->parent);
1360 }
1361 }
1362 }
1363 } else if (LTK_WIDGET_TYPE(childw) == LTK_WIDGET_MENUENTRY) {
1364 ltk_menuentry *e = LTK_CAST_MENUENTRY(childw);
1365 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(e->submenu);
1366 if (submenuw && !submenuw->hidden && !LTK_CAST_MENU(submenuw)->was_opened_above) {
1367 below = e->submenu;
1368 }
1369 }
1370 return below;
1371 }
1372
1373 static ltk_widget_id
1374 ltk_menu_prev_child(ltk_widget *self, ltk_widget_id childid) {
1375 ltk_menu *menu = LTK_CAST_MENU(self);
1376 for (size_t i = ltk_array_len(menu->entries); i-- > 0;) {
1377 ltk_widget_id id = ltk_array_get(menu->entries, i);
1378 if (LTK_WIDGET_ID_EQUAL(id, childid))
1379 return i > 0 ? ltk_array_get(menu->entries, i - 1) : LTK_WIDGET_ID_NONE;
1380 }
1381 return LTK_WIDGET_ID_NONE;
1382 }
1383
1384 static ltk_widget_id
1385 ltk_menu_next_child(ltk_widget *self, ltk_widget_id childid) {
1386 ltk_menu *menu = LTK_CAST_MENU(self);
1387 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
1388 ltk_widget_id id = ltk_array_get(menu->entries, i);
1389 if (LTK_WIDGET_ID_EQUAL(id, childid))
1390 return i < ltk_array_len(menu->entries) - 1 ? ltk_array_get(menu->entries, i + 1) : LTK_WIDGET_ID_NONE;
1391 }
1392 return LTK_WIDGET_ID_NONE;
1393 }
1394
1395 static ltk_widget_id
1396 ltk_menu_first_child(ltk_widget *self) {
1397 ltk_menu *menu = LTK_CAST_MENU(self);
1398 return ltk_array_len(menu->entries) > 0 ? ltk_array_get(menu->entries, 0) : LTK_WIDGET_ID_NONE;
1399 }
1400
1401 static ltk_widget_id
1402 ltk_menu_last_child(ltk_widget *self) {
1403 ltk_menu *menu = LTK_CAST_MENU(self);
1404 return ltk_array_len(menu->entries) > 0 ? ltk_array_get(menu->entries, ltk_array_len(menu->entries) - 1) : LTK_WIDGET_ID_NONE;
1405 }
1406
1407 /* FIXME: unregister from window popups? */
1408 int
1409 ltk_menuentry_detach_submenu(ltk_widget_id entryid) {
1410 ltk_widget *entryw = ltk_get_widget_from_id(entryid);
1411 ltk_menuentry *entry = LTK_CAST_MENUENTRY(entryw);
1412 ltk_widget *submenuw = ltk_get_widget_or_null_from_id(entry->submenu);
1413 if (!submenuw)
1414 return 1;
1415 submenuw->parent = LTK_WIDGET_ID_NONE;
1416 entry->submenu = LTK_WIDGET_ID_NONE;
1417 ltk_menuentry_recalc_ideal_size_with_notification(entry);
1418 return 0;
1419 }
1420
1421 static void
1422 ltk_menu_destroy(ltk_widget *self, int shallow) {
1423 ltk_menu *menu = LTK_CAST_MENU(self);
1424 if (menu->scroll_timer_id >= 0)
1425 ltk_unregister_timer(menu->scroll_timer_id);
1426 ltk_window_unregister_popup(self->window, self->id);
1427 if (!shallow) {
1428 for (size_t i = 0; i < ltk_array_len(menu->entries); i++) {
1429 /* for efficiency - to avoid ltk_widget_destroy calling
1430 ltk_menu_remove_child for each of the entries */
1431 ltk_widget *child = ltk_get_widget_from_id(ltk_array_get(menu->entries, i));
1432 child->parent = LTK_WIDGET_ID_NONE;
1433 ltk_widget_destroy(child, shallow);
1434 }
1435 ltk_array_destroy(widget_id, menu->entries);
1436 } else {
1437 ltk_menu_remove_all_entries(self->id);
1438 }
1439 ltk_free(menu);
1440 }