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