toaruos/lib/menu.c
K. Lange b003fe060c menu: fix silly old bug with submenu highlighting
When using the keyboard to close a submenu, its entry in the parent
window would remain highlighted when navigating to the next entry
as the menu's child was still set to that submenu's child and we
were not checking if it was still open.
2023-02-15 20:26:59 +09:00

1055 lines
29 KiB
C

/**
* @brief Cascading graphical menu library.
*
* C reimplementation of the original Python menu library.
* Used to provide menu bars and the applications menu.
*
* @copyright
* This file is part of ToaruOS and is released under the terms
* of the NCSA / University of Illinois License - see LICENSE.md
* Copyright (C) 2018-2021 K. Lange
*/
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <math.h>
#include <sys/types.h>
#include <dlfcn.h>
#include <toaru/yutani.h>
#include <toaru/graphics.h>
#include <toaru/hashmap.h>
#include <toaru/list.h>
#include <toaru/icon_cache.h>
#include <toaru/text.h>
#include <toaru/markup_text.h>
#include <toaru/menu.h>
#define MENU_ENTRY_HEIGHT 20
#define MENU_BACKGROUND rgb(239,238,232)
#define MENU_ICON_SIZE 16
#define HILIGHT_BORDER_TOP rgb(54,128,205)
#define HILIGHT_GRADIENT_TOP rgb(93,163,236)
#define HILIGHT_GRADIENT_BOTTOM rgb(56,137,220)
#define HILIGHT_BORDER_BOTTOM rgb(47,106,167)
static hashmap_t * menu_windows = NULL;
static yutani_t * my_yctx = NULL;
static struct MenuList * hovered_menu = NULL;
int menu_definitely_close(struct MenuList * menu);
__attribute__((constructor))
static void _init_menus(void) {
menu_windows = hashmap_create_int(10);
markup_text_init();
}
hashmap_t * menu_get_windows_hash(void) {
return menu_windows;
}
static int string_width(const char * s) {
return markup_string_width(s);
}
static int draw_string(gfx_context_t * ctx, int x, int y, uint32_t color, const char * s) {
return markup_draw_string(ctx,x,y+13,s,color);
}
void _menu_draw_MenuEntry_Normal(gfx_context_t * ctx, struct MenuEntry * self, int offset) {
struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self;
_self->offset = offset;
/* Background gradient */
if (_self->hilight) {
draw_line(ctx, 1, _self->width-2, offset, offset, HILIGHT_BORDER_TOP);
draw_line(ctx, 1, _self->width-2, offset + _self->height - 1, offset + _self->height - 1, HILIGHT_BORDER_BOTTOM);
for (int i = 1; i < self->height-1; ++i) {
int thing = ((i - 1) * 256) / (_self->height - 2);
if (thing > 255) thing = 255;
if (thing < 0) thing = 0;
uint32_t c = interp_colors(HILIGHT_GRADIENT_TOP, HILIGHT_GRADIENT_BOTTOM, thing);
draw_line(ctx, 1, self->width-2, offset + i, offset + i, c);
}
}
/* Icon */
if (_self->icon) {
sprite_t * icon = icon_get_16(_self->icon);
if (icon->width == MENU_ICON_SIZE) {
draw_sprite(ctx, icon, 4, offset + 2);
} else {
draw_sprite_scaled(ctx, icon, 4, offset + 2, MENU_ICON_SIZE, MENU_ICON_SIZE);
}
}
/* Foreground text color */
uint32_t color = _self->hilight ? rgb(255,255,255) : rgb(0,0,0);
/* Draw title */
draw_string(ctx, 22, offset + 1, color, _self->title);
}
void _menu_focus_MenuEntry_Normal(struct MenuEntry * self, int focused) {
if (focused) {
if (self->_owner && self->_owner->child) {
menu_definitely_close(self->_owner->child);
self->_owner->child = NULL;
}
}
}
void _menu_activate_MenuEntry_Normal(struct MenuEntry * self, int flags) {
struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self;
list_t * menu_keys = hashmap_keys(menu_windows);
hovered_menu = NULL;
foreach(_key, menu_keys) {
yutani_window_t * window = hashmap_get(menu_windows, (void*)(uintptr_t)_key->value);
if (window) {
struct MenuList * menu = window->user_data;
menu_definitely_close(menu);
if (menu->parent && menu->parent->child == menu) {
menu->parent->child = NULL;
}
}
}
list_free(menu_keys);
free(menu_keys);
if (_self->callback) {
_self->callback(_self);
}
}
static struct MenuEntryVTable _menu_vtable_MenuEntry_Normal = {
.methods = 3,
.renderer = _menu_draw_MenuEntry_Normal,
.focus_change = _menu_focus_MenuEntry_Normal,
.activate = _menu_activate_MenuEntry_Normal,
};
struct MenuEntry * menu_create_normal(const char * icon, const char * action, const char * title, void (*callback)(struct MenuEntry *)) {
struct MenuEntry_Normal * out = malloc(sizeof(struct MenuEntry_Normal));
out->_type = MenuEntry_Normal;
out->height = MENU_ENTRY_HEIGHT;
out->hilight = 0;
out->vtable = &_menu_vtable_MenuEntry_Normal;
out->icon = icon ? strdup(icon) : NULL;
out->title = strdup(title);
out->action = action ? strdup(action) : NULL;
out->callback = callback;
out->rwidth = 50 + string_width(out->title);
return (struct MenuEntry *)out;
}
void _menu_draw_MenuEntry_Submenu(gfx_context_t * ctx, struct MenuEntry * self, int offset) {
struct MenuEntry_Submenu * _self = (struct MenuEntry_Submenu *)self;
int h = _self->hilight;
if (_self->_owner && _self->_my_child && _self->_owner->child == _self->_my_child && !_self->_my_child->closed) {
_self->hilight = 1;
}
_menu_draw_MenuEntry_Normal(ctx,self,offset);
/* Draw the tick on the right side to indicate this is a submenu */
uint32_t color = _self->hilight ? rgb(255,255,255) : rgb(0,0,0);
sprite_t * tick = icon_get_16("menu-tick");
draw_sprite_alpha_paint(ctx, tick, _self->width - 16, offset + 2, 1.0, color);
_self->hilight = h;
}
void _menu_focus_MenuEntry_Submenu(struct MenuEntry * self, int focused) {
if (focused) {
self->vtable->activate(self, focused);
}
}
void _menu_activate_MenuEntry_Submenu(struct MenuEntry * self, int focused) {
struct MenuEntry_Submenu * _self = (struct MenuEntry_Submenu *)self;
if (_self->_owner && _self->_owner->set) {
/* Show a menu */
struct MenuList * new_menu = menu_set_get_menu(_self->_owner->set, (char *)_self->action);
if (_self->_owner->child && _self->_owner->child != new_menu) {
menu_definitely_close(_self->_owner->child);
_self->_owner->child = NULL;
}
new_menu->parent = _self->_owner;
new_menu->parent->child = new_menu;
_self->_my_child = new_menu;
if (new_menu->closed) {
menu_prepare(new_menu, _self->_owner->window->ctx);
int offset_x = _self->_owner->window->width - 2;
if (_self->_owner->window->width + _self->_owner->window->x - 2 + new_menu->window->width > _self->_owner->window->ctx->display_width) {
offset_x = 2 - new_menu->window->width;
}
yutani_window_move_relative(_self->_owner->window->ctx, new_menu->window, _self->_owner->window,
offset_x, _self->offset - 4);
yutani_flip(_self->_owner->window->ctx, new_menu->window);
}
}
}
static struct MenuEntryVTable _menu_vtable_MenuEntry_Submenu = {
.methods = 3,
.renderer = _menu_draw_MenuEntry_Submenu,
.focus_change = _menu_focus_MenuEntry_Submenu,
.activate = _menu_activate_MenuEntry_Submenu,
};
struct MenuEntry * menu_create_submenu(const char * icon, const char * action, const char * title) {
struct MenuEntry_Submenu * out = malloc(sizeof(struct MenuEntry_Submenu));
out->_type = MenuEntry_Submenu;
out->height = MENU_ENTRY_HEIGHT;
out->hilight = 0;
out->vtable = &_menu_vtable_MenuEntry_Submenu;
out->icon = icon ? strdup(icon) : NULL;
out->title = strdup(title);
out->action = action ? strdup(action) : NULL;
out->rwidth = 50 + string_width(out->title);
return (struct MenuEntry *)out;
}
void _menu_draw_MenuEntry_Separator(gfx_context_t * ctx, struct MenuEntry * self, int offset) {
self->offset = offset;
draw_line(ctx, 2, self->width-4, offset+3, offset+3, rgb(178,178,178));
draw_line(ctx, 2, self->width-5, offset+4, offset+4, rgb(250,250,250));
}
void _menu_focus_MenuEntry_Separator(struct MenuEntry * self, int focused) {
if (focused) {
if (self->_owner && self->_owner->child) {
menu_definitely_close(self->_owner->child);
self->_owner->child = NULL;
}
}
}
void _menu_activate_MenuEntry_Separator(struct MenuEntry * self, int focused) {
}
static struct MenuEntryVTable _menu_vtable_MenuEntry_Separator = {
.methods = 3,
.renderer = _menu_draw_MenuEntry_Separator,
.focus_change = _menu_focus_MenuEntry_Separator,
.activate = _menu_activate_MenuEntry_Separator,
};
struct MenuEntry * menu_create_separator(void) {
struct MenuEntry_Separator * out = malloc(sizeof(struct MenuEntry_Separator));
out->_type = MenuEntry_Separator;
out->height = 6;
out->hilight = 0;
out->rwidth = 10; /* at least a bit please */
out->vtable = &_menu_vtable_MenuEntry_Separator;
return (struct MenuEntry *)out;
}
void menu_update_title(struct MenuEntry * self, char * new_title) {
if (self->_type == MenuEntry_Normal) {
struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self;
if (_self->title) {
free((void*)_self->title);
}
_self->title = strdup(new_title);
_self->rwidth = 50 + string_width(_self->title);
} else if (self->_type == MenuEntry_Submenu) {
struct MenuEntry_Submenu * _self = (struct MenuEntry_Submenu *)self;
if (_self->title) {
free((void*)_self->title);
}
_self->title = strdup(new_title);
_self->rwidth = 50 + string_width(_self->title);
}
}
void menu_update_icon(struct MenuEntry * self, char * newIcon) {
switch (self->_type) {
case MenuEntry_Normal: {
struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self;
if (_self->icon) free(_self->icon);
_self->icon = newIcon ? strdup(newIcon) : NULL;
break;
}
case MenuEntry_Submenu: {
struct MenuEntry_Submenu * _self = (struct MenuEntry_Submenu *)self;
if (_self->icon) free(_self->icon);
_self->icon = newIcon ? strdup(newIcon) : NULL;
break;
}
default:
break;
}
}
void menu_free_entry(struct MenuEntry * self) {
switch (self->_type) {
case MenuEntry_Normal: {
struct MenuEntry_Normal * _self = (struct MenuEntry_Normal *)self;
if (_self->icon) free(_self->icon);
if (_self->title) free(_self->title);
if (_self->action) free(_self->action);
break;
}
case MenuEntry_Submenu: {
struct MenuEntry_Submenu * _self = (struct MenuEntry_Submenu *)self;
if (_self->icon) free(_self->icon);
if (_self->title) free(_self->title);
if (_self->action) free(_self->action);
break;
}
default:
break;
}
free(self);
}
static int _close_enough(struct yutani_msg_window_mouse_event * me) {
if (me->command == YUTANI_MOUSE_EVENT_RAISE && sqrt(pow(me->new_x - me->old_x, 2) + pow(me->new_y - me->old_y, 2)) < 10) {
return 1;
}
return 0;
}
static char read_buf[1024];
static size_t available = 0;
static size_t offset = 0;
static size_t read_from = 0;
static char * read_line(FILE * f, char * out, ssize_t len) {
while (len > 0) {
if (available == 0) {
if (offset == 1024) {
offset = 0;
}
size_t r = read(fileno(f), &read_buf[offset], 1024 - offset);
read_from = offset;
available = r;
offset += available;
}
if (available == 0) {
*out = '\0';
return out;
}
while (read_from < offset && len > 0) {
*out = read_buf[read_from];
len--;
read_from++;
available--;
if (*out == '\n') {
return out;
}
out++;
}
}
return out;
}
void menu_calculate_dimensions(struct MenuList * menu, int * height, int * width) {
list_t * list = menu->entries;
*width = 0;
*height = (menu->flags & MENU_FLAG_BUBBLE) ? 16 : 8; /* TODO top and height */
foreach(node, list) {
struct MenuEntry * entry = node->value;
*height += entry->height;
if (*width < entry->rwidth) {
*width = entry->rwidth;
}
}
/* Go back through and update actual widths */
foreach(node, list) {
struct MenuEntry * entry = node->value;
entry->width = *width;
}
}
struct MenuList * menu_set_get_root(struct MenuSet * menu) {
return (void*)hashmap_get(menu->_menus,"_");
}
struct MenuList * menu_set_get_menu(struct MenuSet * menu, char * submenu) {
return (void*)hashmap_get(menu->_menus, submenu);
}
void menu_insert(struct MenuList * menu, struct MenuEntry * entry) {
list_insert(menu->entries, entry);
entry->_owner = menu;
}
struct MenuList * menu_create(void) {
struct MenuList * p = malloc(sizeof(struct MenuList));
p->entries = list_create();
p->ctx = NULL;
p->window = NULL;
p->set = NULL;
p->child = NULL;
p->_bar = NULL;
p->parent = NULL;
p->closed = 1;
p->flags = 0;
p->tail_offset = 0;
return p;
}
struct MenuSet * menu_set_create(void) {
struct MenuSet * _out = malloc(sizeof(struct MenuSet));
_out->_menus = hashmap_create(10);
return _out;
}
void menu_set_insert(struct MenuSet * set, char * action, struct MenuList * menu) {
hashmap_set(set->_menus, action, menu);
menu->set = set;
}
struct MenuSet * menu_set_from_description(const char * path, void (*callback)(struct MenuEntry *)) {
FILE * f;
if (!strcmp(path,"-")) {
f = stdin;
} else {
f = fopen(path,"r");
}
if (!f) {
return NULL;
}
struct MenuSet * _out = malloc(sizeof(struct MenuSet));
hashmap_t * out = hashmap_create(10);
_out->_menus = out;
struct MenuList * current_menu = NULL;
/* Read through the file */
char line[256];
while (1) {
memset(line, 0, 256);
read_line(f, line, 256);
if (!*line) break;
if (line[strlen(line)-1] == '\n') {
line[strlen(line)-1] = '\0';
}
if (!*line) continue; /* skip blank */
if (*line == ':') {
/* New menu */
struct MenuList * p = menu_create();
p->set = _out;
hashmap_set(out, line+1, p);
current_menu = p;
} else if (*line == '#') {
/* Comment */
continue;
} else if (*line == '-') {
if (!current_menu) {
fprintf(stderr, "Tried to add separator with no active menu.\n");
goto failure;
}
menu_insert(current_menu, menu_create_separator());
} else if (*line == '&') {
if (!current_menu) {
fprintf(stderr, "Tried to add submenu with no active menu.\n");
goto failure;
}
char * action = line+1;
char * icon = strstr(action,",");
if (!icon) {
fprintf(stderr, "Malformed line in submenu: no icon\n");
goto failure;
}
*icon = '\0';
icon++;
char * title = strstr(icon,",");
if (!title) {
fprintf(stderr, "Malformed line in submenu: no title\n");
goto failure;
}
*title = '\0';
title++;
menu_insert(current_menu, menu_create_submenu(icon,action,title));
} else {
if (!current_menu) {
fprintf(stderr, "Tried to add item with no active menu.\n");
goto failure;
}
char * action = line;
char * icon = strstr(action,",");
if (!icon) {
fprintf(stderr, "Malformed line in action: no icon\n");
goto failure;
}
*icon = '\0';
icon++;
char * title = strstr(icon,",");
if (!title) {
fprintf(stderr, "Malformed line in action: no title\n");
goto failure;
}
*title = '\0';
title++;
menu_insert(current_menu, menu_create_normal(icon,action,title,callback));
}
}
return _out;
failure:
fprintf(stderr, "malformed description file\n");
if (f != stdin) {
fclose(f);
}
free(out);
return NULL;
}
static void _menu_redraw(yutani_window_t * menu_window, yutani_t * yctx, struct MenuList * menu, int expose) {
gfx_context_t * ctx = menu->ctx;
list_t * entries = menu->entries;
/* Window background */
if (menu->flags & MENU_FLAG_BUBBLE) {
draw_fill(ctx, rgba(0,0,0,0));
draw_rounded_rectangle(ctx, 0, 6, ctx->width, ctx->height - 6, 6, rgb(109,111,112));
draw_rounded_rectangle(ctx, 1, 7, ctx->width-2, ctx->height - 8, 5, MENU_BACKGROUND);
/* Figure out where to draw the tail */
int tail_left = 0;
/* Do we have a set tail? */
#define TAIL_BOUND 8
if (menu->flags & MENU_FLAG_TAIL_POSITION) {
tail_left = (menu->tail_offset < TAIL_BOUND) ? TAIL_BOUND : (menu->tail_offset > ctx->width - TAIL_BOUND) ? (ctx->width - TAIL_BOUND) : menu->tail_offset;
} else if (menu->flags & MENU_FLAG_BUBBLE_LEFT) {
tail_left = 16;
} else if (menu->flags & MENU_FLAG_BUBBLE_RIGHT) {
tail_left = ctx->width - 16;
} else if (menu->flags & MENU_FLAG_BUBBLE_CENTER) {
tail_left = ctx->width / 2;
}
for (int i = 1; i < 7; ++i) {
draw_line(ctx, tail_left - i, tail_left + i, i, i, MENU_BACKGROUND);
}
draw_line_aa(ctx, tail_left - 6, tail_left, 6, 0, rgb(109,111,112), 0.5);
draw_line_aa(ctx, tail_left + 6, tail_left, 6, 0, rgb(109,111,112), 0.5);
} else {
draw_fill(ctx, MENU_BACKGROUND);
/* Window border */
draw_line(ctx, 0, ctx->width-1, 0, 0, rgb(109,111,112));
draw_line(ctx, 0, 0, 0, ctx->height-1, rgb(109,111,112));
draw_line(ctx, ctx->width-1, ctx->width-1, 0, ctx->height-1, rgb(109,111,112));
draw_line(ctx, 0, ctx->width-1, ctx->height-1, ctx->height-1, rgb(109,111,112));
}
/* Draw menu entries */
int offset = (menu->flags & MENU_FLAG_BUBBLE) ? 12 : 4;
foreach(node, entries) {
struct MenuEntry * entry = node->value;
if (entry->vtable->methods >= 1 && entry->vtable->renderer) {
entry->vtable->renderer(ctx, entry, offset);
}
offset += entry->height;
}
flip(ctx);
if (expose) {
yutani_flip(yctx, menu_window);
}
}
void menu_prepare(struct MenuList * menu, yutani_t * yctx) {
/* Calculate window dimensions */
int height, width;
menu_calculate_dimensions(menu,&height, &width);
my_yctx = yctx;
menu->closed = 0;
/* Create window */
yutani_window_t * menu_window = yutani_window_create_flags(yctx, width, height,
((menu->flags & MENU_FLAG_BUBBLE) ? YUTANI_WINDOW_FLAG_ALT_ANIMATION :
YUTANI_WINDOW_FLAG_NO_ANIMATION) | YUTANI_WINDOW_FLAG_DISALLOW_DRAG | YUTANI_WINDOW_FLAG_DISALLOW_RESIZE);
yutani_set_stack(yctx, menu_window, YUTANI_ZORDER_MENU);
if (menu->ctx) {
reinit_graphics_yutani(menu->ctx, menu_window);
} else {
menu->ctx = init_graphics_yutani_double_buffer(menu_window);
}
menu_window->user_data = menu;
menu->window = menu_window;
_menu_redraw(menu_window, yctx, menu, 0);
hashmap_set(menu_windows, (void*)(uintptr_t)menu_window->wid, menu_window);
}
void menu_show(struct MenuList * menu, yutani_t * yctx) {
menu_prepare(menu, yctx);
yutani_flip(yctx, menu->window);
}
void menu_show_at(struct MenuList * menu, yutani_window_t * parent, int x, int y) {
menu_prepare(menu, parent->ctx);
if (parent->x + x + menu->window->width > parent->ctx->display_width) x -= menu->window->width;
if (parent->y + y + menu->window->height > parent->ctx->display_height) y -= menu->window->height;
yutani_window_move_relative(parent->ctx, menu->window, parent, x, y);
yutani_flip(parent->ctx, menu->window);
}
int menu_has_eventual_child(struct MenuList * root, struct MenuList * child) {
if (!child) return 0;
if (root == child) return 1;
struct MenuList * candidate = root->child;
while (candidate && candidate != child) {
if (candidate == root->child) {
root->child = NULL;
return 1;
}
candidate = root->child;
}
return (candidate == child);
}
int menu_definitely_close(struct MenuList * menu) {
if (menu->child) {
menu_definitely_close(menu->child);
menu->child = NULL;
}
if (menu->closed) {
return 0;
}
/* if focused_widget, leave focus on widget */
foreach(node, menu->entries) {
struct MenuEntry * entry = node->value;
entry->hilight = 0;
}
menu->closed = 1;
yutani_wid_t wid = menu->window->wid;
yutani_close(menu->window->ctx, menu->window);
menu->window = NULL;
hashmap_remove(menu_windows, (void*)(uintptr_t)wid);
return 0;
}
int menu_leave(struct MenuList * menu) {
if (!hovered_menu) {
while (menu->parent) {
menu = menu->parent;
}
menu_definitely_close(menu);
return 0;
}
if (!menu_has_eventual_child(menu, hovered_menu)) {
/* Get all menus */
list_t * menu_keys = hashmap_keys(menu_windows);
foreach(_key, menu_keys) {
yutani_window_t * window = hashmap_get(menu_windows, (void *)(uintptr_t)_key->value);
if (window) {
struct MenuList * menu = window->user_data;
if (!hovered_menu || (menu != hovered_menu->child && !menu_has_eventual_child(menu, hovered_menu))) {
menu_definitely_close(menu);
if (menu->parent && menu->parent->child == menu) {
menu->parent->child = NULL;
}
}
}
}
list_free(menu_keys);
free(menu_keys);
}
return 0;
}
void menu_key_action(struct MenuList * menu, struct yutani_msg_key_event * me) {
if (me->event.action != KEY_ACTION_DOWN) return;
yutani_window_t * window = menu->window;
yutani_t * yctx = window->ctx;
hovered_menu = menu;
/* Find hilighted entry */
struct MenuEntry * hilighted = NULL;
struct MenuEntry * previous = NULL;
struct MenuEntry * next = NULL;
int got_it = 0;
foreach(node, menu->entries) {
struct MenuEntry * entry = node->value;
if (entry->hilight) {
hilighted = entry;
got_it = 1;
continue;
}
if (got_it && entry->_type != MenuEntry_Separator) {
next = entry;
break;
}
if (entry->_type != MenuEntry_Separator) {
previous = entry;
}
}
if (me->event.keycode == KEY_ARROW_DOWN) {
if (hilighted) {
hilighted->hilight = 0;
hilighted = next;
}
if (!hilighted) {
/* Use the first entry */
hilighted = menu->entries->head->value;
}
hilighted->hilight = 1;
_menu_redraw(window,yctx,menu,1);
} else if (me->event.keycode == KEY_ARROW_UP) {
if (hilighted) {
hilighted->hilight = 0;
hilighted = previous;
}
if (!hilighted) {
/* Use the last entry */
hilighted = menu->entries->tail->value;
}
hilighted->hilight = 1;
_menu_redraw(window,yctx,menu,1);
} else if (me->event.keycode == KEY_ARROW_RIGHT) {
if (!hilighted) {
hilighted = menu->entries->head->value;
}
if (hilighted) {
hilighted->hilight = 1;
if (hilighted->_type == MenuEntry_Submenu) {
if (hilighted->vtable->methods >= 3 && hilighted->vtable->activate) {
hilighted->vtable->activate(hilighted, 0);
}
_menu_redraw(window,yctx,menu,1);
} else {
struct menu_bar * bar = NULL;
struct MenuList * p = menu;
do {
if (p->_bar) {
bar = p->_bar;
break;
}
} while ((p = p->parent));
if (bar) {
menu_definitely_close(p);
int active = (bar->active_entry_idx + 1 + bar->num_entries) % (bar->num_entries);
bar->active_entry = &bar->entries[active];
if (bar->redraw_callback) {
bar->redraw_callback(bar);
}
menu_bar_show_menu(yctx, bar->window, bar, -1, bar->active_entry);
} else {
_menu_redraw(window,yctx,menu,1);
}
}
}
} else if (me->event.key == '\n') {
if (!hilighted) {
hilighted = menu->entries->head->value;
}
if (hilighted) {
hilighted->hilight = 1;
if (hilighted->vtable->methods >= 3 && hilighted->vtable->activate) {
hilighted->vtable->activate(hilighted, 0);
}
}
} else if (me->event.keycode == KEY_ARROW_LEFT) {
if (menu->parent) {
hovered_menu = menu->parent;
} /* else previous from menu bar? */
menu_definitely_close(menu);
if (menu->_bar) {
int active = (menu->_bar->active_entry_idx - 1 + menu->_bar->num_entries) % (menu->_bar->num_entries);
menu->_bar->active_entry = &menu->_bar->entries[active];
if (menu->_bar->redraw_callback) {
menu->_bar->redraw_callback(menu->_bar);
}
menu_bar_show_menu(yctx, menu->_bar->window, menu->_bar, -1, menu->_bar->active_entry);
} else if (menu->parent && menu->parent->window) {
yutani_focus_window(yctx, menu->parent->window->wid);
}
} else if (me->event.keycode == KEY_ESCAPE) {
hovered_menu = NULL;
menu_leave(menu);
}
}
void menu_mouse_action(struct MenuList * menu, struct yutani_msg_window_mouse_event * me) {
yutani_window_t * window = menu->window;
yutani_t * yctx = window->ctx;
int offset = (menu->flags & MENU_FLAG_BUBBLE) ? 12 : 4;
int changed = 0;
foreach(node, menu->entries) {
struct MenuEntry * entry = node->value;
if (me->new_y >= offset && me->new_y < offset + entry->height &&
me->new_x >= 0 && me->new_x < entry->width) {
if (!entry->hilight) {
changed = 1;
entry->hilight = 1;
if (entry->vtable->methods >= 2 && entry->vtable->focus_change) {
entry->vtable->focus_change(entry, 1);
}
}
if (entry->vtable->methods >= 4 && entry->vtable->mouse_event) {
if (entry->vtable->mouse_event(entry, me)) {
_menu_redraw(window,yctx,menu,1);
}
} else if (me->command == YUTANI_MOUSE_EVENT_CLICK || _close_enough(me)) {
if (entry->vtable->methods >= 3 && entry->vtable->activate) {
entry->vtable->activate(entry, 0);
}
}
} else {
if (entry->hilight) {
changed = 1;
entry->hilight = 0;
if (entry->vtable->methods >= 2 && entry->vtable->focus_change) {
entry->vtable->focus_change(entry, 0);
}
}
}
offset += entry->height;
}
if (changed) {
_menu_redraw(window,yctx,menu,1);
}
}
void menu_force_redraw(struct MenuList * menu) {
yutani_window_t * window = menu->window;
yutani_t * yctx = window->ctx;
_menu_redraw(window,yctx,menu,1);
}
struct MenuList * menu_any_contains(int x, int y) {
struct MenuList * out = NULL;
list_t * menu_keys = hashmap_keys(menu_windows);
foreach(_key, menu_keys) {
yutani_window_t * window = hashmap_get(menu_windows, (void*)_key->value);
if (window) {
if (x >= (int)window->x && x < (int)window->x + (int)window->width && y >= (int)window->y && y < (int)window->y + (int)window->height) {
out = window->user_data;
break;
}
}
}
list_free(menu_keys);
free(menu_keys);
return out;
}
int menu_process_event(yutani_t * yctx, yutani_msg_t * m) {
if (m) {
switch (m->type) {
case YUTANI_MSG_KEY_EVENT:
{
struct yutani_msg_key_event * me = (void*)m->data;
if (hashmap_has(menu_windows, (void*)(uintptr_t)me->wid)) {
yutani_window_t * window = hashmap_get(menu_windows, (void *)(uintptr_t)me->wid);
struct MenuList * menu = window->user_data;
menu_key_action(menu, me);
}
}
break;
case YUTANI_MSG_WINDOW_MOUSE_EVENT:
{
struct yutani_msg_window_mouse_event * me = (void*)m->data;
if (hashmap_has(menu_windows, (void*)(uintptr_t)me->wid)) {
yutani_window_t * window = hashmap_get(menu_windows, (void *)(uintptr_t)me->wid);
struct MenuList * menu = window->user_data;
if (me->new_x >= 0 && me->new_x < (int)window->width && me->new_y >= 0 && me->new_y < (int)window->height) {
if (hovered_menu != menu) {
hovered_menu = menu;
}
} else {
if (hovered_menu) {
struct MenuList * t = menu_any_contains(me->new_x + window->x, me->new_y + window->y);
if (t) {
hovered_menu = t;
} else {
hovered_menu = NULL;
}
}
}
menu_mouse_action(menu, me);
}
}
break;
case YUTANI_MSG_WINDOW_FOCUS_CHANGE:
{
struct yutani_msg_window_focus_change * me = (void*)m->data;
if (hashmap_has(menu_windows, (void*)(uintptr_t)me->wid)) {
yutani_window_t * window = hashmap_get(menu_windows, (void *)(uintptr_t)me->wid);
struct MenuList * menu = window->user_data;
if (!me->focused) {
/* XXX leave menu */
menu_leave(menu);
/* if root and not window.root.menus and window.root.focused */
return 1;
} else {
window->focused = me->focused;
/* Redraw? */
}
}
}
break;
default:
break;
}
}
return 0;
}
void menu_bar_render(struct menu_bar * self, gfx_context_t * ctx) {
int _x = self->x;
int _y = self->y;
int width = self->width;
uint32_t menu_bar_color = rgb(59,59,59);
for (int y = 0; y < MENU_BAR_HEIGHT; ++y) {
for (int x = 0; x < width; ++x) {
GFX(ctx, x+_x,y+_y) = menu_bar_color;
}
}
/* for each menu entry */
int offset = _x;
struct menu_bar_entries * _entries = self->entries;
if (!self->num_entries) {
while (_entries->title) {
_entries++;
self->num_entries++;
}
_entries = self->entries;
}
while (_entries->title) {
int w = string_width(_entries->title) + 11;
if ((self->active_menu && hashmap_has(menu_get_windows_hash(), (void*)(uintptr_t)self->active_menu_wid)) && _entries == self->active_entry) {
for (int y = _y; y < _y + MENU_BAR_HEIGHT; ++y) {
for (int x = offset + 2; x < offset + 2 + w; ++x) {
GFX(ctx, x, y) = rgb(93,163,236);
}
}
}
draw_string(ctx, offset + 7, _y + 2, 0xFFFFFFFF, _entries->title);
offset += w;
_entries++;
}
}
void menu_bar_show_menu(yutani_t * yctx, yutani_window_t * window, struct menu_bar * self, int offset, struct menu_bar_entries * _entries) {
struct MenuList * new_menu = menu_set_get_menu(self->set, _entries->action);
int i = 0;
if (offset == -1) {
/* Must calculate */
offset = self->x;
struct menu_bar_entries * e = self->entries;
while (e->title) {
if (e == _entries) break;
offset += string_width(e->title) + 10;
e++;
i++;
}
} else {
struct menu_bar_entries * e = self->entries;
while (e->title) {
if (e == _entries) break;
e++;
i++;
}
}
menu_prepare(new_menu, yctx);
yutani_window_move_relative(yctx, new_menu->window, window, offset, self->y + MENU_BAR_HEIGHT);
yutani_flip(yctx, new_menu->window);
self->active_menu = new_menu;
self->active_menu->_bar = self;
self->active_menu_wid = new_menu->window->wid;
self->active_entry = _entries;
self->active_entry_idx = i;
if (self->redraw_callback) {
self->redraw_callback(self);
}
}
int menu_bar_mouse_event(yutani_t * yctx, yutani_window_t * window, struct menu_bar * self, struct yutani_msg_window_mouse_event * me, int x, int y) {
if (x < self->x || x >= self->x + self->width || y < self->y || y >= self->y + 24 /* base height */) {
return 0;
}
int offset = self->x;
struct menu_bar_entries * _entries = self->entries;
while (_entries->title) {
int w = string_width(_entries->title) + 11;
if (x >= offset && x < offset + w) {
if (me->command == YUTANI_MOUSE_EVENT_CLICK || _close_enough(me)) {
menu_bar_show_menu(yctx, window, self,offset,_entries);
} else if (self->active_menu && hashmap_has(menu_get_windows_hash(), (void*)(uintptr_t)self->active_menu_wid) && _entries != self->active_entry) {
menu_definitely_close(self->active_menu);
menu_bar_show_menu(yctx, window, self,offset,_entries);
}
}
offset += w;
_entries++;
}
if (x >= offset && me->command == YUTANI_MOUSE_EVENT_DOWN && me->buttons & YUTANI_MOUSE_BUTTON_LEFT) {
yutani_window_drag_start(yctx, window);
}
return 0;
}