From 7d04ff366c604c2d154e09784de4de43bf21d45d Mon Sep 17 00:00:00 2001 From: David Barr Date: Mon, 14 Oct 2024 20:15:26 +0100 Subject: [PATCH] Add files via upload --- menu/menu.c | 1750 ++++++++++++++++++++++++++++++++++++++++++++++ menu/meson.build | 3 + theme.c | 1630 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3383 insertions(+) create mode 100644 menu/menu.c create mode 100644 menu/meson.build create mode 100644 theme.c diff --git a/menu/menu.c b/menu/menu.c new file mode 100644 index 00000000..c08371c1 --- /dev/null +++ b/menu/menu.c @@ -0,0 +1,1750 @@ +// SPDX-License-Identifier: GPL-2.0-only +#define _POSIX_C_SOURCE 200809L +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "action.h" +#include "common/buf.h" +#include "common/dir.h" +#include "common/font.h" +#include "common/list.h" +#include "common/mem.h" +#include "common/nodename.h" +#include "common/scaled-font-buffer.h" +#include "common/scene-helpers.h" +#include "common/spawn.h" +#include "common/string-helpers.h" +#include "labwc.h" +#include "menu/menu.h" +#include "workspaces.h" +#include "view.h" +#include "node.h" +#include "theme.h" + +#define PIPEMENU_MAX_BUF_SIZE 1048576 /* 1 MiB */ +#define PIPEMENU_TIMEOUT_IN_MS 4000 /* 4 seconds */ + +/* state-machine variables for processing */ +static bool in_item; +static struct menuitem *current_item; +static struct action *current_item_action; + +static int menu_level; +static struct menu *current_menu; + +static bool waiting_for_pipe_menu; +static struct menuitem *selected_item; + +struct menu_pipe_context { + struct server *server; + struct menuitem *item; + struct buf buf; + struct wl_event_source *event_read; + struct wl_event_source *event_timeout; + pid_t pid; + int pipe_fd; +}; + +/* TODO: split this whole file into parser.c and actions.c*/ + +static bool +is_unique_id(struct server *server, const char *id) +{ + struct menu *menu; + wl_list_for_each(menu, &server->menus, link) { + if (!strcmp(menu->id, id)) { + return false; + } + } + return true; +} + +static struct menu * +menu_create(struct server *server, const char *id, const char *label) +{ + if (!is_unique_id(server, id)) { + wlr_log(WLR_ERROR, "menu id %s already exists", id); + } + + struct menu *menu = znew(*menu); + wl_list_append(&server->menus, &menu->link); + + wl_list_init(&menu->menuitems); + menu->id = xstrdup(id); + menu->label = xstrdup(label ? label : id); + menu->parent = current_menu; + menu->server = server; + menu->is_pipemenu = waiting_for_pipe_menu; + menu->size.width = server->theme->menu_min_width; + /* menu->size.height will be kept up to date by adding items */ + menu->scene_tree = wlr_scene_tree_create(server->menu_tree); + wlr_scene_node_set_enabled(&menu->scene_tree->node, false); + + // Create border rectangle + menu->border = wlr_scene_rect_create(menu->scene_tree, + menu->size.width + 2 * server->theme->menu_border_width, + 2 * server->theme->menu_border_width, + server->theme->menu_border_color); + + // Create background rectangle + menu->background = wlr_scene_rect_create(menu->scene_tree, + menu->size.width, + menu->size.height, + server->theme->menu_items_bg_color); + + // Position background inside border + wlr_scene_node_set_position(&menu->background->node, + server->theme->menu_border_width, + server->theme->menu_border_width); + + // Create a new scene tree for menu items + menu->items_tree = wlr_scene_tree_create(menu->scene_tree); + wlr_scene_node_set_position(&menu->items_tree->node, + server->theme->menu_border_width, + server->theme->menu_border_width); + + return menu; +} + +struct menu * +menu_get_by_id(struct server *server, const char *id) +{ + if (!id) { + return NULL; + } + struct menu *menu; + wl_list_for_each(menu, &server->menus, link) { + if (!strcmp(menu->id, id)) { + return menu; + } + } + return NULL; +} + +static void +menu_update_width(struct menu *menu) +{ + struct menuitem *item; + struct theme *theme = menu->server->theme; + int max_width = theme->menu_min_width; + + /* Get widest menu item, clamped by menu_max_width */ + wl_list_for_each(item, &menu->menuitems, link) { + if (item->native_width > max_width) { + max_width = item->native_width < theme->menu_max_width + ? item->native_width : theme->menu_max_width; + } + } + menu->size.width = max_width + 2 * theme->menu_item_padding_x; + +// Update border size + wlr_scene_rect_set_size(menu->border, + menu->size.width + 2 * theme->menu_border_width, + menu->size.height + 2 * theme->menu_border_width); + +// Update background size and position + wlr_scene_rect_set_size(menu->background, + menu->size.width, + menu->size.height); + wlr_scene_node_set_position(&menu->background->node, + theme->menu_border_width, + theme->menu_border_width); + +// Update items_tree position + wlr_scene_node_set_position(&menu->items_tree->node, + theme->menu_border_width, + theme->menu_border_width); + + /* + * TODO: This function is getting a bit unwieldy. Consider calculating + * the menu-window width up-front to avoid this post_processing() and + * second-bite-of-the-cherry stuff + */ + + /* Update all items for the new size */ + wl_list_for_each(item, &menu->menuitems, link) { + wlr_scene_rect_set_size( + wlr_scene_rect_from_node(item->normal.background), + menu->size.width, item->height); + + /* + * Separator lines are special because they change width with + * the menu. + */ + if (item->type == LAB_MENU_SEPARATOR_LINE) { + int width = menu->size.width + - 2 * theme->menu_separator_padding_width + - 2 * theme->menu_item_padding_x; + wlr_scene_rect_set_size( + wlr_scene_rect_from_node(item->normal.text), + width, theme->menu_separator_line_thickness); + } else if (item->type == LAB_MENU_TITLE) { + if (item->native_width > max_width) { + scaled_font_buffer_set_max_width(item->normal.buffer, + max_width); + } + if (theme->menu_title_text_justify == LAB_JUSTIFY_CENTER) { + int x, y; + x = (max_width - theme->menu_item_padding_x - + item->native_width) / 2; + x = x < 0 ? 0 : x; + y = (theme->menu_item_height - item->normal.buffer->height) / 2; + wlr_scene_node_set_position(item->normal.text, x, y); + } + } + + if (item->selectable) { + /* Only selectable items have item->selected.background */ + wlr_scene_rect_set_size( + wlr_scene_rect_from_node(item->selected.background), + menu->size.width, item->height); + } + + if (item->native_width > max_width || item->submenu || item->execute) { + scaled_font_buffer_set_max_width(item->normal.buffer, + max_width); + if (item->selectable) { + scaled_font_buffer_set_max_width(item->selected.buffer, + max_width); + } + } + } +} + +static void +post_processing(struct server *server) +{ + struct menu *menu; + wl_list_for_each(menu, &server->menus, link) { + menu_update_width(menu); + } +} + +static void +validate_menu(struct menu *menu) +{ + struct menuitem *item; + struct action *action, *action_tmp; + wl_list_for_each(item, &menu->menuitems, link) { + wl_list_for_each_safe(action, action_tmp, &item->actions, link) { + if (!action_is_valid(action)) { + wl_list_remove(&action->link); + action_free(action); + wlr_log(WLR_ERROR, "Removed invalid menu action"); + } + } + } +} + +static void +validate(struct server *server) +{ + struct menu *menu; + wl_list_for_each(menu, &server->menus, link) { + validate_menu(menu); + } +} + +static struct menuitem * +item_create(struct menu *menu, const char *text, bool show_arrow) +{ + assert(menu); + assert(text); + + struct menuitem *menuitem = znew(*menuitem); + menuitem->parent = menu; + menuitem->selectable = true; + menuitem->type = LAB_MENU_ITEM; + struct server *server = menu->server; + struct theme *theme = server->theme; + + const char *arrow = show_arrow ? "›" : NULL; + + menuitem->height = theme->menu_item_height; + + int x, y; + menuitem->native_width = font_width(&rc.font_menuitem, text); + if (arrow) { + menuitem->native_width += font_width(&rc.font_menuitem, arrow); + } + + /* Menu item root node */ + menuitem->tree = wlr_scene_tree_create(menu->items_tree); + node_descriptor_create(&menuitem->tree->node, + LAB_NODE_DESC_MENUITEM, menuitem); + + /* Tree for each state to hold background and text buffer */ + menuitem->normal.tree = wlr_scene_tree_create(menuitem->tree); + menuitem->selected.tree = wlr_scene_tree_create(menuitem->tree); + + /* Item background nodes */ + menuitem->normal.background = &wlr_scene_rect_create( + menuitem->normal.tree, + menu->size.width, theme->menu_item_height, + theme->menu_items_bg_color)->node; + menuitem->selected.background = &wlr_scene_rect_create( + menuitem->selected.tree, + menu->size.width, theme->menu_item_height, + theme->menu_items_active_bg_color)->node; + + /* Font nodes */ + menuitem->normal.buffer = scaled_font_buffer_create(menuitem->normal.tree); + menuitem->selected.buffer = scaled_font_buffer_create(menuitem->selected.tree); + if (!menuitem->normal.buffer || !menuitem->selected.buffer) { + wlr_log(WLR_ERROR, "Failed to create menu item '%s'", text); + /* + * Destroying the root node will destroy everything, + * including the node descriptor and scaled_font_buffers. + */ + wlr_scene_node_destroy(&menuitem->tree->node); + free(menuitem); + return NULL; + } + menuitem->normal.text = &menuitem->normal.buffer->scene_buffer->node; + menuitem->selected.text = &menuitem->selected.buffer->scene_buffer->node; + + /* Font buffers */ + scaled_font_buffer_update(menuitem->normal.buffer, text, menuitem->native_width, + &rc.font_menuitem, theme->menu_items_text_color, + theme->menu_items_bg_color, arrow); + scaled_font_buffer_update(menuitem->selected.buffer, text, menuitem->native_width, + &rc.font_menuitem, theme->menu_items_active_text_color, + theme->menu_items_active_bg_color, arrow); + + /* Center font nodes */ + x = theme->menu_item_padding_x; + y = (theme->menu_item_height - menuitem->normal.buffer->height) / 2; + wlr_scene_node_set_position(menuitem->normal.text, x, y); + y = (theme->menu_item_height - menuitem->selected.buffer->height) / 2; + wlr_scene_node_set_position(menuitem->selected.text, x, y); + + /* Position the item in relation to its menu */ + wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height); + + /* Hide selected state */ + wlr_scene_node_set_enabled(&menuitem->selected.tree->node, false); + + /* Update menu extents */ + menu->size.height += menuitem->height; + + wl_list_append(&menu->menuitems, &menuitem->link); + wl_list_init(&menuitem->actions); + return menuitem; +} + +static struct menuitem * +separator_create(struct menu *menu, const char *label) +{ + struct menuitem *menuitem = znew(*menuitem); + menuitem->parent = menu; + menuitem->selectable = false; + menuitem->type = string_null_or_empty(label) ? LAB_MENU_SEPARATOR_LINE + : LAB_MENU_TITLE; + struct server *server = menu->server; + struct theme *theme = server->theme; + + if (menuitem->type == LAB_MENU_TITLE) { + menuitem->height = theme->menu_item_height; + menuitem->native_width = font_width(&rc.font_menuheader, label); + } else if (menuitem->type == LAB_MENU_SEPARATOR_LINE) { + menuitem->height = theme->menu_separator_line_thickness + + 2 * theme->menu_separator_padding_height; + } + + /* Menu item root node */ + menuitem->tree = wlr_scene_tree_create(menu->items_tree); + node_descriptor_create(&menuitem->tree->node, + LAB_NODE_DESC_MENUITEM, menuitem); + + /* Tree to hold background and text/line buffer */ + menuitem->normal.tree = wlr_scene_tree_create(menuitem->tree); + + /* Item background nodes */ + float *bg_color = menuitem->type == LAB_MENU_TITLE + ? theme->menu_title_bg_color : theme->menu_items_bg_color; + float *text_color = menuitem->type == LAB_MENU_TITLE + ? theme->menu_title_text_color : theme->menu_items_text_color; + menuitem->normal.background = &wlr_scene_rect_create( + menuitem->normal.tree, + menu->size.width, menuitem->height, bg_color)->node; + + /* Draw separator line or title */ + if (menuitem->type == LAB_MENU_TITLE) { + menuitem->normal.buffer = scaled_font_buffer_create(menuitem->normal.tree); + if (!menuitem->normal.buffer) { + wlr_log(WLR_ERROR, "Failed to create menu item '%s'", label); + wlr_scene_node_destroy(&menuitem->tree->node); + free(menuitem); + return NULL; + } + menuitem->normal.text = &menuitem->normal.buffer->scene_buffer->node; + /* Font buffer */ + scaled_font_buffer_update(menuitem->normal.buffer, label, + menuitem->native_width, &rc.font_menuheader, + text_color, bg_color, /* arrow */ NULL); + /* Center font nodes */ + int x, y; + x = theme->menu_item_padding_x; + y = (theme->menu_item_height - menuitem->normal.buffer->height) / 2; + wlr_scene_node_set_position(menuitem->normal.text, x, y); + } else { + int nominal_width = theme->menu_min_width; + menuitem->normal.text = &wlr_scene_rect_create( + menuitem->normal.tree, nominal_width, + theme->menu_separator_line_thickness, + theme->menu_separator_color)->node; + wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height); + /* Vertically center-align separator line */ + wlr_scene_node_set_position(menuitem->normal.text, + theme->menu_separator_padding_width + + theme->menu_item_padding_x, + theme->menu_separator_padding_height); + } + wlr_scene_node_set_position(&menuitem->tree->node, 0, menu->size.height); + + menu->size.height += menuitem->height; + wl_list_append(&menu->menuitems, &menuitem->link); + wl_list_init(&menuitem->actions); + return menuitem; +} + +/* + * Handle the following: + * + * + * + * + * + */ +static void +fill_item(char *nodename, char *content) +{ + /* + * Nodenames for most menu-items end with '.item.menu' but top-level + * pipemenu items do not have the associated element so merely + * end with a '.item' + */ + string_truncate_at_pattern(nodename, ".item.menu"); + string_truncate_at_pattern(nodename, ".item"); + + /* defines the start of a new item */ + if (!strcmp(nodename, "label")) { + current_item = item_create(current_menu, content, false); + current_item_action = NULL; + } else if (!current_item) { + wlr_log(WLR_ERROR, "expect element first. " + "nodename: '%s' content: '%s'", nodename, content); + } else if (!strcmp(nodename, "icon")) { + /* + * Do nothing as we don't support menu icons - just avoid + * logging errors if a menu.xml file contains icon="" entries. + */ + } else if (!strcmp(nodename, "name.action")) { + current_item_action = action_create(content); + if (current_item_action) { + wl_list_append(¤t_item->actions, + ¤t_item_action->link); + } + } else if (!current_item_action) { + wlr_log(WLR_ERROR, "expect element first. " + "nodename: '%s' content: '%s'", nodename, content); + } else { + action_arg_from_xml_node(current_item_action, nodename, content); + } +} + +static void +item_destroy(struct menuitem *item) +{ + if (item->pipe_ctx) { + item->pipe_ctx->item = NULL; + } + wl_list_remove(&item->link); + action_list_free(&item->actions); + wlr_scene_node_destroy(&item->tree->node); + free(item->execute); + free(item->id); + free(item); +} + +/* + * We support XML CDATA for in menu.xml in order to provide backward + * compatibility with obmenu-generator. For example: + * + * + * + * + * + * + * + * + * + * is an old, deprecated openbox variety of . We support it + * for backward compatibility with old openbox-menu generators. It has the same + * function and + * + * The following nodenames support CDATA. + * - command.action.item.*menu.openbox_menu + * - execute.action.item.*menu.openbox_menu + * - command.action.item.openbox_pipe_menu + * - execute.action.item.openbox_pipe_menu + * - command.action.item.*menu.openbox_pipe_menu + * - execute.action.item.*menu.openbox_pipe_menu + * + * The *menu allows nested menus with nodenames such as ...menu.menu... or + * ...menu.menu.menu... and so on. We could use match_glob() for all of the + * above but it seems simpler to just check the first three fields. + */ +static bool +nodename_supports_cdata(char *nodename) +{ + return !strncmp("command.action.", nodename, 15) + || !strncmp("execute.action.", nodename, 15); +} + +static void +entry(xmlNode *node, char *nodename, char *content) +{ + if (!nodename) { + return; + } + xmlChar *cdata = NULL; + if (!content && nodename_supports_cdata(nodename)) { + cdata = xmlNodeGetContent(node); + } + if (!content && !cdata) { + return; + } + string_truncate_at_pattern(nodename, ".openbox_menu"); + string_truncate_at_pattern(nodename, ".openbox_pipe_menu"); + if (getenv("LABWC_DEBUG_MENU_NODENAMES")) { + printf("%s: %s\n", nodename, content ? content : (char *)cdata); + } + if (in_item) { + fill_item(nodename, content ? content : (char *)cdata); + } + xmlFree(cdata); +} + +static void +process_node(xmlNode *node) +{ + static char buffer[256]; + + char *content = (char *)node->content; + if (xmlIsBlankNode(node)) { + return; + } + char *name = nodename(node, buffer, sizeof(buffer)); + entry(node, name, content); +} + +static void xml_tree_walk(xmlNode *node, struct server *server); + +static void +traverse(xmlNode *n, struct server *server) +{ + xmlAttr *attr; + + process_node(n); + for (attr = n->properties; attr; attr = attr->next) { + xml_tree_walk(attr->children, server); + } + xml_tree_walk(n->children, server); +} + +static int +nr_parents(xmlNode *n) +{ + assert(n); + int i = 0; + for (xmlNode *node = n->parent; node && i < INT_MAX; ++i) { + node = node->parent; + } + return i; +} + +/* + * Return true for the highest level static menu definitions in the format + * below. We use the fact that the id-attribute has two nodal parents ( + * and ) as the test here. + * + * + * + * ... + * + * + * + * Return false for any other element which could be either: + * + * (a) one found in a pipemenu; or + * (b) one that links to a submenu as follows (but is a child to another + * element. + * + * + * + * + * + */ +static bool +is_toplevel_static_menu_definition(xmlNode *n, char *id) +{ + /* + * Catch elements in pipemenus + * + * For pipemenus we cannot just rely on nr_parents() because they have + * their own hierarchy, so we just use the fact that a pipemenu cannot + * be the root-menu. + */ + if (menu_level) { + return false; + } + + return id && nr_parents(n) == 2; +} + +/* + * elements have three different roles: + * * Definition of (sub)menu - has ID, LABEL and CONTENT + * * Menuitem of pipemenu type - has ID, LABEL and EXECUTE + * * Menuitem of submenu type - has ID only + */ +static void +handle_menu_element(xmlNode *n, struct server *server) +{ + char *label = (char *)xmlGetProp(n, (const xmlChar *)"label"); + char *execute = (char *)xmlGetProp(n, (const xmlChar *)"execute"); + char *id = (char *)xmlGetProp(n, (const xmlChar *)"id"); + + if (execute && label && id) { + wlr_log(WLR_DEBUG, "pipemenu '%s:%s:%s'", id, label, execute); + if (!current_menu) { + /* + * We currently do not support pipemenus without a + * parent such as the one the example below: + * + * + * + * + * + * + * TODO: Consider supporting this + */ + wlr_log(WLR_ERROR, + "pipemenu '%s:%s:%s' has no parent ", + id, label, execute); + goto error; + } + current_item = item_create(current_menu, label, /* arrow */ true); + current_item_action = NULL; + current_item->execute = xstrdup(execute); + current_item->id = xstrdup(id); + } else if ((label && id) || is_toplevel_static_menu_definition(n, id)) { + /* + * (label && id) refers to which is an + * inline menu definition. + * + * is_toplevel_static_menu_definition() catches: + * + * + * + * + * which is the highest level a menu can be defined at. + * + * Openbox spec requires a label="" defined here, but it is + * actually pointless so we handle it with or without the label + * attribute to make it easier for users to define "root-menu" + * and "client-menu". + */ + struct menu **submenu = NULL; + if (menu_level > 0) { + /* + * In a nested (inline) menu definition we need to + * create an item pointing to the new submenu + */ + current_item = item_create(current_menu, label, true); + if (current_item) { + submenu = ¤t_item->submenu; + } else { + submenu = NULL; + } + } + ++menu_level; + current_menu = menu_create(server, id, label); + if (submenu) { + *submenu = current_menu; + } + traverse(n, server); + current_menu = current_menu->parent; + --menu_level; + } else if (id) { + /* + * (when inside another element) creates an + * entry which points to a menu defined elsewhere. + * + * This is only supported in static menus. Pipemenus need to use + * nested (inline) menu definitions, otherwise we could have a + * pipemenu opening the "root-menu" or similar. + */ + + if (current_menu && current_menu->is_pipemenu) { + wlr_log(WLR_ERROR, + "cannot link to static menu from pipemenu"); + goto error; + } + + struct menu *menu = menu_get_by_id(server, id); + if (menu) { + current_item = item_create(current_menu, menu->label, true); + if (current_item) { + current_item->submenu = menu; + } + } else { + wlr_log(WLR_ERROR, "no menu with id '%s'", id); + } + } +error: + free(label); + free(execute); + free(id); +} + +/* This can be one of and */ +static void +handle_separator_element(xmlNode *n) +{ + char *label = (char *)xmlGetProp(n, (const xmlChar *)"label"); + current_item = separator_create(current_menu, label); + free(label); +} + +static void +xml_tree_walk(xmlNode *node, struct server *server) +{ + for (xmlNode *n = node; n && n->name; n = n->next) { + if (!strcasecmp((char *)n->name, "comment")) { + continue; + } + if (!strcasecmp((char *)n->name, "menu")) { + handle_menu_element(n, server); + continue; + } + if (!strcasecmp((char *)n->name, "separator")) { + handle_separator_element(n); + continue; + } + if (!strcasecmp((char *)n->name, "item")) { + if (!current_menu) { + wlr_log(WLR_ERROR, + "ignoring without parent "); + continue; + } + in_item = true; + traverse(n, server); + in_item = false; + continue; + } + traverse(n, server); + } +} + +static bool +parse_buf(struct server *server, struct buf *buf) +{ + xmlDoc *d = xmlParseMemory(buf->data, buf->len); + if (!d) { + wlr_log(WLR_ERROR, "xmlParseMemory()"); + return false; + } + xml_tree_walk(xmlDocGetRootElement(d), server); + xmlFreeDoc(d); + xmlCleanupParser(); + return true; +} + +/* + * @stream can come from either of the following: + * - fopen() in the case of reading a file such as menu.xml + * - popen() when processing pipemenus + */ +static void +parse_stream(struct server *server, FILE *stream) +{ + char *line = NULL; + size_t len = 0; + struct buf b = BUF_INIT; + + while (getline(&line, &len, stream) != -1) { + char *p = strrchr(line, '\n'); + if (p) { + *p = '\0'; + } + buf_add(&b, line); + } + free(line); + parse_buf(server, &b); + buf_reset(&b); +} + +static void +parse_xml(const char *filename, struct server *server) +{ + struct wl_list paths; + paths_config_create(&paths, filename); + + bool should_merge_config = rc.merge_config; + struct wl_list *(*iter)(struct wl_list *list); + iter = should_merge_config ? paths_get_prev : paths_get_next; + + for (struct wl_list *elm = iter(&paths); elm != &paths; elm = iter(elm)) { + struct path *path = wl_container_of(elm, path, link); + FILE *stream = fopen(path->string, "r"); + if (!stream) { + continue; + } + wlr_log(WLR_INFO, "read menu file %s", path->string); + parse_stream(server, stream); + fclose(stream); + if (!should_merge_config) { + break; + } + } + paths_destroy(&paths); +} + +static int +menu_get_full_width(struct menu *menu) +{ + int width = menu->size.width - menu->server->theme->menu_overlap_x; + int child_width; + int max_child_width = 0; + struct menuitem *item; + wl_list_for_each(item, &menu->menuitems, link) { + if (!item->submenu) { + continue; + } + child_width = menu_get_full_width(item->submenu); + if (child_width > max_child_width) { + max_child_width = child_width; + } + } + return width + max_child_width; +} + +/** + * get_submenu_position() - get output layout coordinates of menu window + * @item: the menuitem that triggers the submenu (static or dynamic) + */ +static struct wlr_box +get_submenu_position(struct menuitem *item, enum menu_align align) +{ + struct wlr_box pos = { 0 }; + struct menu *menu = item->parent; + struct theme *theme = menu->server->theme; + pos.x = menu->scene_tree->node.x; + pos.y = menu->scene_tree->node.y; + + if (align & LAB_MENU_OPEN_RIGHT) { + pos.x += menu->size.width - theme->menu_overlap_x; + } + pos.y += item->tree->node.y - theme->menu_overlap_y; + return pos; +} + +static void +menu_configure(struct menu *menu, int lx, int ly, enum menu_align align) +{ + struct theme *theme = menu->server->theme; + + /* Get output local coordinates + output usable area */ + double ox = lx; + double oy = ly; + struct wlr_output *wlr_output = wlr_output_layout_output_at( + menu->server->output_layout, lx, ly); + struct output *output = wlr_output ? output_from_wlr_output( + menu->server, wlr_output) : NULL; + if (!output) { + wlr_log(WLR_ERROR, + "Failed to position menu %s (%s) and its submenus: " + "Not enough screen space", menu->id, menu->label); + return; + } + wlr_output_layout_output_coords(menu->server->output_layout, + wlr_output, &ox, &oy); + + if (align == LAB_MENU_OPEN_AUTO) { + int full_width = menu_get_full_width(menu); + if (ox + full_width > output->usable_area.width) { + align = LAB_MENU_OPEN_LEFT; + } else { + align = LAB_MENU_OPEN_RIGHT; + } + } + + if (oy + menu->size.height > output->usable_area.height) { + align &= ~LAB_MENU_OPEN_BOTTOM; + align |= LAB_MENU_OPEN_TOP; + } else { + align &= ~LAB_MENU_OPEN_TOP; + align |= LAB_MENU_OPEN_BOTTOM; + } + + if (align & LAB_MENU_OPEN_LEFT) { + lx -= menu->size.width - theme->menu_overlap_x; + } + if (align & LAB_MENU_OPEN_TOP) { + ly -= menu->size.height; + if (menu->parent) { + /* For submenus adjust y to bottom left corner */ + ly += theme->menu_item_height; + } + } + wlr_scene_node_set_position(&menu->scene_tree->node, lx, ly); + + /* Needed for pipemenus to inherit alignment */ + menu->align = align; + + struct menuitem *item; + wl_list_for_each(item, &menu->menuitems, link) { + if (!item->submenu) { + continue; + } + struct wlr_box pos = get_submenu_position(item, align); + menu_configure(item->submenu, pos.x, pos.y, align); + } +} + +static void +menu_hide_submenu(struct server *server, const char *id) +{ + struct menu *menu, *hide_menu; + hide_menu = menu_get_by_id(server, id); + if (!hide_menu) { + return; + } + wl_list_for_each(menu, &server->menus, link) { + bool should_reposition = false; + struct menuitem *item, *next; + wl_list_for_each_safe(item, next, &menu->menuitems, link) { + if (item->submenu == hide_menu) { + item_destroy(item); + should_reposition = true; + } + } + + if (!should_reposition) { + continue; + } + /* Re-position items vertically */ + menu->size.height = 0; + wl_list_for_each(item, &menu->menuitems, link) { + wlr_scene_node_set_position(&item->tree->node, 0, + menu->size.height); + menu->size.height += item->height; + } + } +} + +static void +init_client_send_to_menu(struct server *server) +{ + /* Just create placeholder. Contents will be created when launched */ + menu_create(server, "client-send-to-menu", ""); +} + +/* + * This is client-send-to-menu + * an internal menu similar to root-menu and client-menu + * + * This will look at workspaces and produce a menu + * with the workspace names that can be used with + * SendToDesktop, left/right options are included. + */ +void +update_client_send_to_menu(struct server *server) +{ + struct menu *menu = menu_get_by_id(server, + "client-send-to-menu"); + + if (menu) { + struct menuitem *item, *next; + wl_list_for_each_safe(item, next, &menu->menuitems, link) { + item_destroy(item); + } + } + + menu->size.height = 0; + + struct workspace *workspace; + + wl_list_for_each(workspace, &server->workspaces.all, link) { + if (workspace == server->workspaces.current) { + current_item = item_create(menu, strdup_printf(">%s<", workspace->name), + /*show arrow*/ false); + } else { + current_item = item_create(menu, workspace->name, /*show arrow*/ false); + } + fill_item("name.action", "SendToDesktop"); + fill_item("to.action", workspace->name); + } + + menu_update_width(menu); +} + +static void +init_client_list_combined_menu(struct server *server) +{ + /* Just create placeholder. Contents will be created when launched */ + menu_create(server, "client-list-combined-menu", ""); +} + +/* + * This is client-list-combined-menu an internal menu similar to root-menu and + * client-menu. + * + * This will look at workspaces and produce a menu with the workspace name as a + * separator label and the titles of the view, if any, below each workspace + * name. Active view is indicated by "*" preceeding title. + */ +void +update_client_list_combined_menu(struct server *server) +{ + struct menu *menu = menu_get_by_id(server, "client-list-combined-menu"); + + if (!menu) { + /* Menu is created on compositor startup/reconfigure */ + wlr_log(WLR_ERROR, "client-list-combined-menu does not exist"); + return; + } + + struct menuitem *item, *next; + wl_list_for_each_safe(item, next, &menu->menuitems, link) { + item_destroy(item); + } + + menu->size.height = 0; + + struct workspace *workspace; + struct view *view; + struct buf buffer = BUF_INIT; + + wl_list_for_each(workspace, &server->workspaces.all, link) { + buf_add_fmt(&buffer, workspace == server->workspaces.current ? ">%s<" : "%s", + workspace->name); + current_item = separator_create(menu, buffer.data); + buf_clear(&buffer); + + wl_list_for_each(view, &server->views, link) { + if (view->workspace == workspace) { + const char *title = view_get_string_prop(view, "title"); + if (!view->toplevel.handle || string_null_or_empty(title)) { + continue; + } + + if (view == server->active_view) { + buf_add(&buffer, "[*]"); + } + + else if (view->minimized) { + buf_add(&buffer, "[_] "); + } + buf_add(&buffer, title); + + current_item = item_create(menu, buffer.data, /*show arrow*/ false); + current_item->id = xstrdup(menu->id); + current_item->client_list_view = view; + fill_item("name.action", "Focus"); + fill_item("name.action", "Raise"); + buf_clear(&buffer); + } + } + current_item = item_create(menu, _("Go there..."), /*show arrow*/ false); + current_item->id = xstrdup(menu->id); + fill_item("name.action", "GoToDesktop"); + fill_item("to.action", workspace->name); + } + buf_reset(&buffer); + menu_update_width(menu); +} + +static void +init_rootmenu(struct server *server) +{ + struct menu *menu = menu_get_by_id(server, "root-menu"); + + /* Default menu if no menu.xml found */ + if (!menu) { + current_menu = NULL; + menu = menu_create(server, "root-menu", ""); + } + if (wl_list_empty(&menu->menuitems)) { + current_item = item_create(menu, _("Reconfigure"), false); + fill_item("name.action", "Reconfigure"); + current_item = item_create(menu, _("Exit"), false); + fill_item("name.action", "Exit"); + } +} + +static void +init_windowmenu(struct server *server) +{ + struct menu *menu = menu_get_by_id(server, "client-menu"); + + /* Default menu if no menu.xml found */ + if (!menu) { + current_menu = NULL; + menu = menu_create(server, "client-menu", ""); + } + if (wl_list_empty(&menu->menuitems)) { + current_item = item_create(menu, _("Minimize"), false); + fill_item("name.action", "Iconify"); + current_item = item_create(menu, _("Maximize"), false); + fill_item("name.action", "ToggleMaximize"); + current_item = item_create(menu, _("Fullscreen"), false); + fill_item("name.action", "ToggleFullscreen"); + current_item = item_create(menu, _("Roll Up/Down"), false); + fill_item("name.action", "ToggleShade"); + current_item = item_create(menu, _("Decorations"), false); + fill_item("name.action", "ToggleDecorations"); + current_item = item_create(menu, _("Always on Top"), false); + fill_item("name.action", "ToggleAlwaysOnTop"); + + /* Workspace sub-menu */ + struct menu *workspace_menu = menu_create(server, "workspaces", ""); + current_item = item_create(workspace_menu, _("Move Left"), false); + /* + * is true by default so + * GoToDesktop will be called as part of the action. + */ + fill_item("name.action", "SendToDesktop"); + fill_item("to.action", "left"); + current_item = item_create(workspace_menu, _("Move Right"), false); + fill_item("name.action", "SendToDesktop"); + fill_item("to.action", "right"); + current_item = separator_create(workspace_menu, ""); + current_item = item_create(workspace_menu, + _("Always on Visible Workspace"), false); + fill_item("name.action", "ToggleOmnipresent"); + + current_item = item_create(menu, _("Workspace"), true); + current_item->submenu = workspace_menu; + + current_item = item_create(menu, _("Close"), false); + fill_item("name.action", "Close"); + } + + if (wl_list_length(&rc.workspace_config.workspaces) == 1) { + menu_hide_submenu(server, "workspaces"); + } +} + +void +menu_init(struct server *server) +{ + wl_list_init(&server->menus); + parse_xml("menu.xml", server); + init_rootmenu(server); + init_windowmenu(server); + init_client_list_combined_menu(server); + init_client_send_to_menu(server); + post_processing(server); + validate(server); +} + +static void +nullify_item_pointing_to_this_menu(struct menu *menu) +{ + struct menu *iter; + wl_list_for_each(iter, &menu->server->menus, link) { + struct menuitem *item; + wl_list_for_each(item, &iter->menuitems, link) { + if (item->submenu == menu) { + item->submenu = NULL; + /* + * Let's not return early here in case we have + * multiple items pointing to the same menu. + */ + } + } + + /* This is important for pipe-menus */ + if (iter->parent == menu) { + iter->parent = NULL; + } + } +} + +static void +menu_free(struct menu *menu) +{ + /* Keep items clean on pipemenu destruction */ + nullify_item_pointing_to_this_menu(menu); + + struct menuitem *item, *next; + wl_list_for_each_safe(item, next, &menu->menuitems, link) { + item_destroy(item); + } + + /* + * Destroying the root node will destroy everything, + * including node descriptors and scaled_font_buffers. + */ + wlr_scene_node_destroy(&menu->border->node); + wlr_scene_node_destroy(&menu->background->node); + wlr_scene_node_destroy(&menu->items_tree->node); + wlr_scene_node_destroy(&menu->scene_tree->node); + + wl_list_remove(&menu->link); + zfree(menu->id); + zfree(menu->label); + zfree(menu); +} + +/** + * menu_free_from - free menu list starting from current point + * @from: point to free from (if NULL, all menus are freed) + */ +static void +menu_free_from(struct server *server, struct menu *from) +{ + bool destroying = !from; + struct menu *menu, *tmp_menu; + wl_list_for_each_safe(menu, tmp_menu, &server->menus, link) { + if (menu == from) { + destroying = true; + } + if (!destroying) { + continue; + } + + menu_free(menu); + } +} + +void +menu_finish(struct server *server) +{ + menu_free_from(server, NULL); + + /* Reset state vars for starting fresh when Reload is triggered */ + current_item = NULL; + current_item_action = NULL; + current_menu = NULL; +} + +/* Sets selection (or clears selection if passing NULL) */ +static void +menu_set_selection(struct menu *menu, struct menuitem *item) +{ + /* Clear old selection */ + if (menu->selection.item) { + wlr_scene_node_set_enabled( + &menu->selection.item->normal.tree->node, true); + wlr_scene_node_set_enabled( + &menu->selection.item->selected.tree->node, false); + } + /* Set new selection */ + if (item) { + wlr_scene_node_set_enabled(&item->normal.tree->node, false); + wlr_scene_node_set_enabled(&item->selected.tree->node, true); + } + menu->selection.item = item; +} + +static void +close_all_submenus(struct menu *menu) +{ + struct menuitem *item; + wl_list_for_each(item, &menu->menuitems, link) { + if (item->submenu) { + wlr_scene_node_set_enabled( + &item->submenu->scene_tree->node, false); + close_all_submenus(item->submenu); + } + } + menu->selection.menu = NULL; +} + +/* + * We only destroy pipemenus when closing the entire menu-tree so that pipemenu + * are cached (for as long as the menu is open). This drastically improves the + * felt performance when interacting with multiple pipe menus where a single + * item may be selected multiple times. + */ +static void +destroy_pipemenus(struct server *server) +{ + wlr_log(WLR_DEBUG, "number of menus before close=%d", + wl_list_length(&server->menus)); + + struct menu *iter, *tmp; + wl_list_for_each_safe(iter, tmp, &server->menus, link) { + if (iter->is_pipemenu) { + menu_free(iter); + } + } + + wlr_log(WLR_DEBUG, "number of menus after close=%d", + wl_list_length(&server->menus)); +} + +static void +_close(struct menu *menu) +{ + wlr_scene_node_set_enabled(&menu->scene_tree->node, false); + menu_set_selection(menu, NULL); + if (menu->selection.menu) { + _close(menu->selection.menu); + menu->selection.menu = NULL; + } +} + +static void +menu_close(struct menu *menu) +{ + if (!menu) { + wlr_log(WLR_ERROR, "Trying to close non exiting menu"); + return; + } + _close(menu); +} + +void +menu_open_root(struct menu *menu, int x, int y) +{ + assert(menu); + if (menu->server->menu_current) { + menu_close(menu->server->menu_current); + destroy_pipemenus(menu->server); + } + close_all_submenus(menu); + menu_set_selection(menu, NULL); + menu_configure(menu, x, y, LAB_MENU_OPEN_AUTO); + wlr_scene_node_set_enabled(&menu->scene_tree->node, true); + menu->server->menu_current = menu; + menu->server->input_mode = LAB_INPUT_STATE_MENU; + selected_item = NULL; +} + +static void +create_pipe_menu(struct menu_pipe_context *ctx) +{ + assert(ctx->item); + + struct menu *pipe_parent = ctx->item->parent; + if (!pipe_parent) { + wlr_log(WLR_INFO, "[pipemenu %ld] invalid parent", + (long)ctx->pid); + return; + } + if (!pipe_parent->scene_tree->node.enabled) { + wlr_log(WLR_INFO, "[pipemenu %ld] parent menu already closed", + (long)ctx->pid); + return; + } + + /* + * Pipemenus do not contain a toplevel element so we have to + * create that first `struct menu`. + */ + struct menu *pipe_menu = menu_create(ctx->server, ctx->item->id, /*label*/ NULL); + pipe_menu->is_pipemenu = true; + pipe_menu->triggered_by_view = pipe_parent->triggered_by_view; + pipe_menu->parent = pipe_parent; + + menu_level++; + current_menu = pipe_menu; + if (!parse_buf(ctx->server, &ctx->buf)) { + menu_free(pipe_menu); + ctx->item->submenu = NULL; + goto restore_menus; + } + ctx->item->submenu = pipe_menu; + + /* + * TODO: refactor validate() and post_processing() to only + * operate from current point onwards + */ + + /* Set menu-widths before configuring */ + post_processing(ctx->server); + + enum menu_align align = ctx->item->parent->align; + struct wlr_box pos = get_submenu_position(ctx->item, align); + menu_configure(pipe_menu, pos.x, pos.y, align); + + validate(ctx->server); + + /* Finally open the new submenu tree */ + wlr_scene_node_set_enabled(&pipe_menu->scene_tree->node, true); + pipe_parent->selection.menu = pipe_menu; + +restore_menus: + current_menu = pipe_parent; + menu_level--; +} + +static void +pipemenu_ctx_destroy(struct menu_pipe_context *ctx) +{ + wl_event_source_remove(ctx->event_read); + wl_event_source_remove(ctx->event_timeout); + spawn_piped_close(ctx->pid, ctx->pipe_fd); + buf_reset(&ctx->buf); + if (ctx->item) { + ctx->item->pipe_ctx = NULL; + } + free(ctx); + waiting_for_pipe_menu = false; +} + +static int +handle_pipemenu_timeout(void *_ctx) +{ + struct menu_pipe_context *ctx = _ctx; + wlr_log(WLR_ERROR, "[pipemenu %ld] timeout reached, killing %s", + (long)ctx->pid, ctx->item ? ctx->item->execute : "n/a"); + kill(ctx->pid, SIGTERM); + pipemenu_ctx_destroy(ctx); + return 0; +} + +static bool +starts_with_less_than(const char *s) +{ + return (s + strspn(s, " \t\r\n"))[0] == '<'; +} + +static int +handle_pipemenu_readable(int fd, uint32_t mask, void *_ctx) +{ + struct menu_pipe_context *ctx = _ctx; + /* two 4k pages + 1 NULL byte */ + char data[8193]; + ssize_t size; + + if (!ctx->item) { + /* parent menu item got destroyed in the meantime */ + wlr_log(WLR_INFO, "[pipemenu %ld] parent menu item destroyed", + (long)ctx->pid); + kill(ctx->pid, SIGTERM); + goto clean_up; + } + + do { + /* leave space for terminating NULL byte */ + size = read(fd, data, sizeof(data) - 1); + } while (size == -1 && errno == EINTR); + + if (size == -1) { + wlr_log_errno(WLR_ERROR, "[pipemenu %ld] failed to read data (%s)", + (long)ctx->pid, ctx->item->execute); + goto clean_up; + } + + /* Limit pipemenu buffer to 1 MiB for safety */ + if (ctx->buf.len + size > PIPEMENU_MAX_BUF_SIZE) { + wlr_log(WLR_ERROR, "[pipemenu %ld] too big (> %d bytes); killing %s", + (long)ctx->pid, PIPEMENU_MAX_BUF_SIZE, ctx->item->execute); + kill(ctx->pid, SIGTERM); + goto clean_up; + } + + wlr_log(WLR_DEBUG, "[pipemenu %ld] read %ld bytes of data", (long)ctx->pid, size); + if (size) { + data[size] = '\0'; + buf_add(&ctx->buf, data); + return 0; + } + + /* Guard against badly formed data such as binary input */ + if (!starts_with_less_than(ctx->buf.data)) { + wlr_log(WLR_ERROR, "expect xml data to start with '<'; abort pipemenu"); + goto clean_up; + } + + create_pipe_menu(ctx); + +clean_up: + pipemenu_ctx_destroy(ctx); + return 0; +} + +static void +parse_pipemenu(struct menuitem *item) +{ + if (!is_unique_id(item->parent->server, item->id)) { + wlr_log(WLR_ERROR, "duplicate id '%s'; abort pipemenu", item->id); + return; + } + + if (item->pipe_ctx) { + wlr_log(WLR_ERROR, "item already has a pipe context attached"); + return; + } + + int pipe_fd = 0; + pid_t pid = spawn_piped(item->execute, &pipe_fd); + if (pid <= 0) { + wlr_log(WLR_ERROR, "Failed to spawn pipe menu process %s", item->execute); + return; + } + + waiting_for_pipe_menu = true; + struct menu_pipe_context *ctx = znew(*ctx); + ctx->server = item->parent->server; + ctx->item = item; + ctx->pid = pid; + ctx->pipe_fd = pipe_fd; + ctx->buf = BUF_INIT; + item->pipe_ctx = ctx; + + ctx->event_read = wl_event_loop_add_fd(ctx->server->wl_event_loop, + pipe_fd, WL_EVENT_READABLE, handle_pipemenu_readable, ctx); + + ctx->event_timeout = wl_event_loop_add_timer(ctx->server->wl_event_loop, + handle_pipemenu_timeout, ctx); + wl_event_source_timer_update(ctx->event_timeout, PIPEMENU_TIMEOUT_IN_MS); + + wlr_log(WLR_DEBUG, "[pipemenu %ld] executed: %s", (long)ctx->pid, ctx->item->execute); +} + +static void +menu_process_item_selection(struct menuitem *item) +{ + assert(item); + + /* Do not keep selecting the same item */ + if (item == selected_item) { + return; + } + + if (waiting_for_pipe_menu) { + return; + } + selected_item = item; + + if (!item->selectable) { + return; + } + + /* We are on an item that has new focus */ + menu_set_selection(item->parent, item); + if (item->parent->selection.menu) { + /* Close old submenu tree */ + menu_close(item->parent->selection.menu); + } + + /* Pipemenu */ + if (item->execute && !item->submenu) { + /* pipemenus are generated async */ + parse_pipemenu(item); + return; + } + + if (item->submenu) { + /* Sync the triggering view */ + item->submenu->triggered_by_view = item->parent->triggered_by_view; + /* Ensure the submenu has its parent set correctly */ + item->submenu->parent = item->parent; + /* And open the new submenu tree */ + wlr_scene_node_set_enabled( + &item->submenu->scene_tree->node, true); + } + + item->parent->selection.menu = item->submenu; +} + +/* Get the deepest submenu with active item selection or the root menu itself */ +static struct menu * +get_selection_leaf(struct server *server) +{ + struct menu *menu = server->menu_current; + if (!menu) { + return NULL; + } + + while (menu->selection.menu) { + if (!menu->selection.menu->selection.item) { + return menu; + } + menu = menu->selection.menu; + } + + return menu; +} + +/* Selects the next or previous sibling of the currently selected item */ +static void +menu_item_select(struct server *server, bool forward) +{ + struct menu *menu = get_selection_leaf(server); + if (!menu) { + return; + } + + struct menuitem *item = NULL; + struct menuitem *selection = menu->selection.item; + struct wl_list *start = selection ? &selection->link : &menu->menuitems; + struct wl_list *current = start; + while (!item || !item->selectable) { + current = forward ? current->next : current->prev; + if (current == start) { + return; + } + if (current == &menu->menuitems) { + /* Allow wrap around */ + item = NULL; + continue; + } + item = wl_container_of(current, item, link); + } + + menu_process_item_selection(item); +} + +static bool +menu_execute_item(struct menuitem *item) +{ + assert(item); + + if (item->submenu || !item->selectable) { + /* We received a click on a separator or item that just opens a submenu */ + return false; + } + + /* + * We close the menu here to provide a faster feedback to the user. + * We do that without resetting the input state so src/cursor.c + * can do its own clean up on the following RELEASE event. + */ + struct server *server = item->parent->server; + menu_close(server->menu_current); + server->input_mode = LAB_INPUT_STATE_PASSTHROUGH; + cursor_update_focus(server); + + /* + * We call the actions after closing the menu so that virtual keyboard + * input is sent to the focused_surface instead of being absorbed by the + * menu. Consider for example: `wlrctl keyboard type abc` + * + * We cannot call menu_close_root() directly here because it does both + * menu_close() and destroy_pipemenus() which we have to handle + * before/after action_run() respectively. + */ + if (item->id && !strcmp(item->id, "client-list-combined-menu") + && item->client_list_view) { + actions_run(item->client_list_view, server, &item->actions, NULL); + } else { + actions_run(item->parent->triggered_by_view, server, + &item->actions, NULL); + } + + server->menu_current = NULL; + destroy_pipemenus(server); + return true; +} + +/* Keyboard based selection */ +void +menu_item_select_next(struct server *server) +{ + menu_item_select(server, /* forward */ true); +} + +void +menu_item_select_previous(struct server *server) +{ + menu_item_select(server, /* forward */ false); +} + +bool +menu_call_selected_actions(struct server *server) +{ + struct menu *menu = get_selection_leaf(server); + if (!menu || !menu->selection.item) { + return false; + } + + return menu_execute_item(menu->selection.item); +} + +/* Selects the first item on the submenu attached to the current selection */ +void +menu_submenu_enter(struct server *server) +{ + struct menu *menu = get_selection_leaf(server); + if (!menu || !menu->selection.menu) { + return; + } + + struct wl_list *start = &menu->selection.menu->menuitems; + struct wl_list *current = start; + struct menuitem *item = NULL; + while (!item || !item->selectable) { + current = current->next; + if (current == start) { + return; + } + item = wl_container_of(current, item, link); + } + + menu_process_item_selection(item); +} + +/* Re-selects the selected item on the parent menu of the current selection */ +void +menu_submenu_leave(struct server *server) +{ + struct menu *menu = get_selection_leaf(server); + if (!menu || !menu->parent || !menu->parent->selection.item) { + return; + } + + menu_process_item_selection(menu->parent->selection.item); +} + +/* Mouse based selection */ +void +menu_process_cursor_motion(struct wlr_scene_node *node) +{ + assert(node && node->data); + struct menuitem *item = node_menuitem_from_node(node); + menu_process_item_selection(item); +} + +bool +menu_call_actions(struct wlr_scene_node *node) +{ + assert(node && node->data); + struct menuitem *item = node_menuitem_from_node(node); + + return menu_execute_item(item); +} + +void +menu_close_root(struct server *server) +{ + assert(server->input_mode == LAB_INPUT_STATE_MENU); + if (server->menu_current) { + menu_close(server->menu_current); + server->menu_current = NULL; + destroy_pipemenus(server); + } + server->input_mode = LAB_INPUT_STATE_PASSTHROUGH; +} + +void +menu_reconfigure(struct server *server) +{ + menu_finish(server); + server->menu_current = NULL; + menu_init(server); +} diff --git a/menu/meson.build b/menu/meson.build new file mode 100644 index 00000000..51306a44 --- /dev/null +++ b/menu/meson.build @@ -0,0 +1,3 @@ +labwc_sources += files( + 'menu.c', +) diff --git a/theme.c b/theme.c new file mode 100644 index 00000000..0681af70 --- /dev/null +++ b/theme.c @@ -0,0 +1,1630 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * Theme engine for labwc + * + * Copyright (C) Johan Malm 2020-2023 + */ + +#define _POSIX_C_SOURCE 200809L +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "common/macros.h" +#include "common/dir.h" +#include "common/font.h" +#include "common/graphic-helpers.h" +#include "common/match.h" +#include "common/mem.h" +#include "common/parse-bool.h" +#include "common/parse-double.h" +#include "common/string-helpers.h" +#include "config/rcxml.h" +#include "img/img-png.h" +#include "labwc.h" + +#if HAVE_RSVG +#include "img/img-svg.h" +#endif + +#include "img/img-xbm.h" +#include "theme.h" +#include "buffer.h" +#include "ssd.h" + +struct button { + const char *name; + const char *alt_name; + const char *fallback_button; /* built-in 6x6 button */ + enum ssd_part_type type; + uint8_t state_set; +}; + +enum corner { + LAB_CORNER_UNKNOWN = 0, + LAB_CORNER_TOP_LEFT, + LAB_CORNER_TOP_RIGHT, +}; + +struct rounded_corner_ctx { + struct wlr_box *box; + double radius; + double line_width; + float *fill_color; + float *border_color; + enum corner corner; +}; + +#define zero_array(arr) memset(arr, 0, sizeof(arr)) + +static struct lab_data_buffer *rounded_rect(struct rounded_corner_ctx *ctx); + +/* 1 degree in radians (=2π/360) */ +static const double deg = 0.017453292519943295; + +static void +zdrop(struct lab_data_buffer **buffer) +{ + if (*buffer) { + wlr_buffer_drop(&(*buffer)->base); + *buffer = NULL; + } +} + +static struct lab_data_buffer * +copy_icon_buffer(struct theme *theme, struct lab_data_buffer *icon_buffer) +{ + assert(icon_buffer); + + struct surface_context icon = + get_cairo_surface_from_lab_data_buffer(icon_buffer); + int icon_width = cairo_image_surface_get_width(icon.surface); + int icon_height = cairo_image_surface_get_height(icon.surface); + + int width = theme->window_button_width; + int height = theme->window_button_height; + + /* + * Proportionately increase size of hover_buffer if the non-hover + * 'donor' buffer is larger than the allocated space. It will get + * scaled down again by wlroots when rendered and as required by the + * current output scale. + * + * This ensures that icons > width or > height keep their aspect ratio + * and are rendered the same as without the hover overlay. + */ + double scale = (width && height) ? + MAX((double)icon_width / width, (double)icon_height / height) : 1.0; + if (scale < 1.0) { + scale = 1.0; + } + int buffer_width = (double)width * scale; + int buffer_height = (double)height * scale; + struct lab_data_buffer *buffer = buffer_create_cairo( + buffer_width, buffer_height, 1.0); + cairo_t *cairo = buffer->cairo; + + cairo_set_source_surface(cairo, icon.surface, + (buffer_width - icon_width) / 2, (buffer_height - icon_height) / 2); + cairo_paint(cairo); + + /* + * Scale cairo context so that we can draw hover overlay or rounded + * corner on this buffer in the scene coordinates. + */ + cairo_scale(cairo, scale, scale); + + if (icon.is_duplicate) { + cairo_surface_destroy(icon.surface); + } + + return buffer; +} + +static void +create_hover_fallback(struct theme *theme, + struct lab_data_buffer **hover_buffer, + struct lab_data_buffer *icon_buffer) +{ + assert(icon_buffer); + assert(!*hover_buffer); + + int width = theme->window_button_width; + int height = theme->window_button_height; + + *hover_buffer = copy_icon_buffer(theme, icon_buffer); + cairo_t *cairo = (*hover_buffer)->cairo; + + /* Overlay (pre-multiplied alpha) */ + float overlay_color[4] = { 0.15f, 0.15f, 0.15f, 0.3f}; + set_cairo_color(cairo, overlay_color); + int radius = theme->window_button_hover_bg_corner_radius; + + cairo_new_sub_path(cairo); + cairo_arc(cairo, radius, radius, radius, 180 * deg, 270 * deg); + cairo_line_to(cairo, width - radius, 0); + cairo_arc(cairo, width - radius, radius, radius, -90 * deg, 0 * deg); + cairo_line_to(cairo, width, height - radius); + cairo_arc(cairo, width - radius, height - radius, radius, 0 * deg, 90 * deg); + cairo_line_to(cairo, radius, height); + cairo_arc(cairo, radius, height - radius, radius, 90 * deg, 180 * deg); + cairo_close_path(cairo); + cairo_fill(cairo); + + cairo_surface_flush(cairo_get_target(cairo)); +} + +static void +create_rounded_buffer(struct theme *theme, enum corner corner, + struct lab_data_buffer **rounded_buffer, + struct lab_data_buffer *icon_buffer) +{ + *rounded_buffer = copy_icon_buffer(theme, icon_buffer); + cairo_t *cairo = (*rounded_buffer)->cairo; + + int width = theme->window_button_width; + int height = theme->window_button_height; + + /* + * Round the corner button by cropping the region within the window + * border. See the picture in #2189 for reference. + */ + int margin_x = theme->window_titlebar_padding_width; + int margin_y = (theme->title_height - theme->window_button_height) / 2; + float white[4] = {1, 1, 1, 1}; + struct rounded_corner_ctx rounded_ctx = { + .box = &(struct wlr_box){ + .width = margin_x + width, + .height = margin_y + height, + }, + .radius = rc.corner_radius, + .line_width = theme->border_width, + .fill_color = white, + .border_color = white, + .corner = corner, + }; + struct lab_data_buffer *mask_buffer = rounded_rect(&rounded_ctx); + cairo_set_operator(cairo, CAIRO_OPERATOR_DEST_IN); + cairo_set_source_surface(cairo, cairo_get_target(mask_buffer->cairo), + (corner == LAB_CORNER_TOP_LEFT) ? -margin_x : 0, + -margin_y); + cairo_paint(cairo); + + cairo_surface_flush(cairo_get_target(cairo)); + wlr_buffer_drop(&mask_buffer->base); +} + +/* + * Scan theme directories with button names (name + postfix) and write the full + * path of the found button file to @buf. An empty string is set if a button + * file is not found. + */ +static void +get_button_filename(char *buf, size_t len, const char *name, const char *postfix) +{ + buf[0] = '\0'; + + char filename[4096]; + snprintf(filename, sizeof(filename), "%s%s", name, postfix); + + struct wl_list paths; + paths_theme_create(&paths, rc.theme_name, filename); + + /* + * You can't really merge buttons, so let's just iterate forwards + * and stop on the first hit + */ + struct path *path; + wl_list_for_each(path, &paths, link) { + if (access(path->string, R_OK) == 0) { + snprintf(buf, len, "%s", path->string); + break; + } + } + paths_destroy(&paths); +} + +static void +load_button(struct theme *theme, struct button *b, int active) +{ + struct lab_data_buffer *(*buttons)[LAB_BS_ALL + 1] = + theme->window[active].buttons; + struct lab_data_buffer **buffer = &buttons[b->type][b->state_set]; + float *rgba = theme->window[active].button_colors[b->type]; + char filename[4096]; + + zdrop(buffer); + + int size = theme->window_button_height; + float scale = 1; /* TODO: account for output scale */ + + /* PNG */ + get_button_filename(filename, sizeof(filename), b->name, + active ? "-active.png" : "-inactive.png"); + img_png_load(filename, buffer, size, scale); + +#if HAVE_RSVG + /* SVG */ + if (!*buffer) { + get_button_filename(filename, sizeof(filename), b->name, + active ? "-active.svg" : "-inactive.svg"); + img_svg_load(filename, buffer, size, scale); + } +#endif + + /* XBM */ + if (!*buffer) { + get_button_filename(filename, sizeof(filename), b->name, ".xbm"); + img_xbm_load(filename, buffer, rgba); + } + + /* + * XBM (alternative name) + * For example max_hover_toggled instead of max_toggled_hover + */ + if (!*buffer && b->alt_name) { + get_button_filename(filename, sizeof(filename), + b->alt_name, ".xbm"); + img_xbm_load(filename, buffer, rgba); + } + + /* + * Builtin bitmap + * + * Applicable to basic buttons such as max, max_toggled and iconify. + * There are no bitmap fallbacks for *_hover icons. + */ + if (!*buffer && b->fallback_button) { + img_xbm_from_bitmap(b->fallback_button, buffer, rgba); + } + + /* + * If hover-icons do not exist, add fallbacks by copying the non-hover + * variant and then adding an overlay. + */ + if (!*buffer && (b->state_set & LAB_BS_HOVERD)) { + uint8_t non_hover_state_set = b->state_set & ~LAB_BS_HOVERD; + create_hover_fallback(theme, buffer, + buttons[b->type][non_hover_state_set]); + } + + struct title_button *leftmost_button = NULL; + wl_list_for_each(leftmost_button, + &rc.title_buttons_left, link) { + break; + } + struct title_button *rightmost_button = NULL; + wl_list_for_each_reverse(rightmost_button, + &rc.title_buttons_right, link) { + break; + } + + /* + * If the loaded button is at the corner of the titlebar, also create + * rounded variants. + */ + uint8_t rounded_state_set = b->state_set | LAB_BS_ROUNDED; + if (leftmost_button && leftmost_button->type == b->type) { + create_rounded_buffer(theme, LAB_CORNER_TOP_LEFT, + &buttons[b->type][rounded_state_set], *buffer); + } else if (rightmost_button && rightmost_button->type == b->type) { + create_rounded_buffer(theme, LAB_CORNER_TOP_RIGHT, + &buttons[b->type][rounded_state_set], *buffer); + } +} + +/* + * We use the following button filename schema: "BUTTON [TOGGLED] [STATE]" + * with the words separated by underscore, and the following meaning: + * - BUTTON can be one of 'max', 'iconify', 'close', 'menu' + * - TOGGLED is either 'toggled' or nothing + * - STATE is 'hover' or nothing. In future, 'pressed' may be supported too. + * + * We believe that this is how the vast majority of extant openbox themes out + * there are constructed and it is consistent with the openbox.org wiki. But + * please be aware that it is actually different to vanilla Openbox which uses: + * "BUTTON [STATE] [TOGGLED]" following an unfortunate commit in 2014 which + * broke themes and led to some distros patching Openbox: + * https://github.com/danakj/openbox/commit/35e92e4c2a45b28d5c2c9b44b64aeb4222098c94 + * + * Arch Linux and Debian patch Openbox to keep the old syntax (the one we use). + * https://gitlab.archlinux.org/archlinux/packaging/packages/openbox/-/blob/main/debian-887908.patch?ref_type=heads + * This patch does the following: + * - reads "%s_toggled_pressed.xbm" and "%s_toggled_hover.xbm" instead of the + * 'hover_toggled' equivalents. + * - parses 'toggled.unpressed', toggled.pressed' and 'toggled.hover' instead + * of the other way around ('*.toggled') when processing themerc. + * + * For compatibility with distros which do not apply similar patches, we support + * the hover-before-toggle too, for example: + * + * .name = "max_toggled_hover", + * .alt_name = "max_hover_toggled", + * + * ...in the button array definition below. + */ +static void +load_buttons(struct theme *theme) +{ + struct button buttons[] = { { + .name = "menu", + .type = LAB_SSD_BUTTON_WINDOW_MENU, + .state_set = 0, + .fallback_button = (const char[]){ 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00 }, + }, { + /* menu icon is loaded again as a fallback of window icon */ + .name = "menu", + .type = LAB_SSD_BUTTON_WINDOW_ICON, + .state_set = 0, + .fallback_button = (const char[]){ 0x00, 0x18, 0x3c, 0x3c, 0x18, 0x00 }, + }, { + .name = "iconify", + .type = LAB_SSD_BUTTON_ICONIFY, + .state_set = 0, + .fallback_button = (const char[]){ 0x00, 0x00, 0x00, 0x00, 0x3f, 0x3f }, + }, { + .name = "max", + .type = LAB_SSD_BUTTON_MAXIMIZE, + .state_set = 0, + .fallback_button = (const char[]){ 0x3f, 0x3f, 0x21, 0x21, 0x21, 0x3f }, + }, { + .name = "max_toggled", + .type = LAB_SSD_BUTTON_MAXIMIZE, + .state_set = LAB_BS_TOGGLED, + .fallback_button = (const char[]){ 0x3e, 0x22, 0x2f, 0x29, 0x39, 0x0f }, + }, { + .name = "shade", + .type = LAB_SSD_BUTTON_SHADE, + .state_set = 0, + .fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x0c, 0x1e, 0x3f }, + }, { + .name = "shade_toggled", + .type = LAB_SSD_BUTTON_SHADE, + .state_set = LAB_BS_TOGGLED, + .fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x3f, 0x1e, 0x0c }, + }, { + .name = "desk", + .type = LAB_SSD_BUTTON_OMNIPRESENT, + .state_set = 0, + .fallback_button = (const char[]){ 0x33, 0x33, 0x00, 0x00, 0x33, 0x33 }, + }, { + .name = "desk_toggled", + .type = LAB_SSD_BUTTON_OMNIPRESENT, + .state_set = LAB_BS_TOGGLED, + .fallback_button = (const char[]){ 0x00, 0x1e, 0x1a, 0x16, 0x1e, 0x00 }, + }, { + .name = "close", + .type = LAB_SSD_BUTTON_CLOSE, + .state_set = 0, + .fallback_button = (const char[]){ 0x33, 0x3f, 0x1e, 0x1e, 0x3f, 0x33 }, + }, { + .name = "menu_hover", + .type = LAB_SSD_BUTTON_WINDOW_MENU, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + /* menu_hover icon is loaded again as a fallback of window icon */ + .name = "menu_hover", + .type = LAB_SSD_BUTTON_WINDOW_ICON, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "iconify_hover", + .type = LAB_SSD_BUTTON_ICONIFY, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "max_hover", + .type = LAB_SSD_BUTTON_MAXIMIZE, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "max_toggled_hover", + .alt_name = "max_hover_toggled", + .type = LAB_SSD_BUTTON_MAXIMIZE, + .state_set = LAB_BS_TOGGLED | LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "shade_hover", + .type = LAB_SSD_BUTTON_SHADE, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "shade_toggled_hover", + .alt_name = "shade_hover_toggled", + .type = LAB_SSD_BUTTON_SHADE, + .state_set = LAB_BS_TOGGLED | LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "desk_hover", + /* no fallback (non-hover variant is used instead) */ + .type = LAB_SSD_BUTTON_OMNIPRESENT, + .state_set = LAB_BS_HOVERD, + }, { + .name = "desk_toggled_hover", + .alt_name = "desk_hover_toggled", + .type = LAB_SSD_BUTTON_OMNIPRESENT, + .state_set = LAB_BS_TOGGLED | LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, { + .name = "close_hover", + .type = LAB_SSD_BUTTON_CLOSE, + .state_set = LAB_BS_HOVERD, + /* no fallback (non-hover variant is used instead) */ + }, }; + + for (size_t i = 0; i < ARRAY_SIZE(buttons); ++i) { + struct button *b = &buttons[i]; + load_button(theme, b, THEME_INACTIVE); + load_button(theme, b, THEME_ACTIVE); + } +} + +static int +hex_to_dec(char c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + return 0; +} + +/** + * parse_hexstr - parse #rrggbb + * @hex: hex string to be parsed + * @rgba: pointer to float[4] for return value + */ +static void +parse_hexstr(const char *hex, float *rgba) +{ + if (!hex || hex[0] != '#' || strlen(hex) < 7) { + return; + } + rgba[0] = (hex_to_dec(hex[1]) * 16 + hex_to_dec(hex[2])) / 255.0; + rgba[1] = (hex_to_dec(hex[3]) * 16 + hex_to_dec(hex[4])) / 255.0; + rgba[2] = (hex_to_dec(hex[5]) * 16 + hex_to_dec(hex[6])) / 255.0; + rgba[3] = 1.0; + + size_t len = strlen(hex); + if (len >= 9 && hex[7] == ' ') { + /* Deprecated #aabbcc 100 alpha encoding to support openbox themes */ + rgba[3] = atoi(hex + 8) / 100.0; + wlr_log(WLR_ERROR, + "The theme uses deprecated alpha notation %s, please convert to " + "#rrggbbaa to ensure your config works on newer labwc releases", hex); + } else if (len == 9) { + /* Inline alpha encoding like #aabbccff */ + rgba[3] = (hex_to_dec(hex[7]) * 16 + hex_to_dec(hex[8])) / 255.0; + } else if (len > 7) { + /* More than just #aabbcc */ + wlr_log(WLR_ERROR, "invalid alpha color encoding: '%s'", hex); + } + + /* Pre-multiply everything as expected by wlr_scene */ + rgba[0] *= rgba[3]; + rgba[1] *= rgba[3]; + rgba[2] *= rgba[3]; +} + +static void +parse_hexstrs(const char *hexes, float colors[3][4]) +{ + gchar **elements = g_strsplit(hexes, ",", -1); + for (int i = 0; elements[i] && i < 3; i++) { + parse_hexstr(elements[i], colors[i]); + } + g_strfreev(elements); +} + +static enum lab_justification +parse_justification(const char *str) +{ + if (!strcasecmp(str, "Center")) { + return LAB_JUSTIFY_CENTER; + } else if (!strcasecmp(str, "Right")) { + return LAB_JUSTIFY_RIGHT; + } else { + return LAB_JUSTIFY_LEFT; + } +} + +/* + * We generally use Openbox defaults, but if no theme file can be found it's + * better to populate the theme variables with some sane values as no-one + * wants to use openbox without a theme - it'll all just be black and white. + * + * Openbox doesn't actual start if it can't find a theme. As it's normally + * packaged with Clearlooks, this is not a problem, but for labwc I thought + * this was a bit hard-line. People might want to try labwc without having + * Openbox (and associated themes) installed. + * + * theme_builtin() applies a theme that is similar to vanilla GTK + */ +static void +theme_builtin(struct theme *theme, struct server *server) +{ + theme->border_width = 1; + theme->menu_border_width = 1; + theme->window_titlebar_padding_height = 0; + theme->window_titlebar_padding_width = 0; + theme->title_height = INT_MIN; + theme->menu_overlap_x = 0; + theme->menu_overlap_y = 0; + + parse_hexstr("#e1dedb", theme->window_active_border_color); + parse_hexstr("#f6f5f4", theme->window_inactive_border_color); + + parse_hexstr("#ff0000", theme->window_toggled_keybinds_color); + + parse_hexstr("#e1dedb", theme->window_active_title_bg_color); + parse_hexstr("#f6f5f4", theme->window_inactive_title_bg_color); + + parse_hexstr("#000000", theme->window_active_label_text_color); + parse_hexstr("#000000", theme->window_inactive_label_text_color); + theme->window_label_text_justify = parse_justification("Center"); + theme->menu_title_text_justify = parse_justification("Center"); + + theme->window_button_width = 26; + theme->window_button_height = 26; + theme->window_button_spacing = 0; + theme->window_button_hover_bg_corner_radius = 0; + + for (enum ssd_part_type type = LAB_SSD_BUTTON_FIRST; + type <= LAB_SSD_BUTTON_LAST; type++) { + parse_hexstr("#000000", + theme->window[THEME_INACTIVE].button_colors[type]); + parse_hexstr("#000000", + theme->window[THEME_ACTIVE].button_colors[type]); + } + + theme->window_active_shadow_size = 60; + theme->window_inactive_shadow_size = 40; + parse_hexstr("#00000060", theme->window_active_shadow_color); + parse_hexstr("#00000040", theme->window_inactive_shadow_color); + parse_hexstr("#eaeaea", theme->menu_border_color); + parse_hexstr("#fcfbfa", theme->menu_items_bg_color); + parse_hexstr("#000000", theme->menu_items_text_color); + parse_hexstr("#e1dedb", theme->menu_items_active_bg_color); + parse_hexstr("#000000", theme->menu_items_active_text_color); + + theme->menu_item_padding_x = 7; + theme->menu_item_padding_y = 4; + + theme->menu_min_width = 20; + theme->menu_max_width = 200; + + theme->menu_separator_line_thickness = 1; + theme->menu_separator_padding_width = 6; + theme->menu_separator_padding_height = 3; + parse_hexstr("#888888", theme->menu_separator_color); + + parse_hexstr("#589bda", theme->menu_title_bg_color); + + parse_hexstr("#ffffff", theme->menu_title_text_color); + + theme->osd_window_switcher_width = 600; + theme->osd_window_switcher_width_is_percent = false; + theme->osd_window_switcher_padding = 4; + theme->osd_window_switcher_item_padding_x = 10; + theme->osd_window_switcher_item_padding_y = 1; + theme->osd_window_switcher_item_active_border_width = 2; + + /* inherit settings in post_processing() if not set elsewhere */ + theme->osd_window_switcher_preview_border_width = INT_MIN; + zero_array(theme->osd_window_switcher_preview_border_color); + theme->osd_window_switcher_preview_border_color[0][0] = FLT_MIN; + + theme->osd_workspace_switcher_boxes_width = 20; + theme->osd_workspace_switcher_boxes_height = 20; + + /* inherit settings in post_processing() if not set elsewhere */ + theme->osd_bg_color[0] = FLT_MIN; + theme->osd_border_width = INT_MIN; + theme->osd_border_color[0] = FLT_MIN; + theme->osd_label_text_color[0] = FLT_MIN; + + if (wlr_renderer_is_pixman(server->renderer)) { + /* Draw only outlined overlay by default to save CPU resource */ + theme->snapping_overlay_region.bg_enabled = false; + theme->snapping_overlay_edge.bg_enabled = false; + theme->snapping_overlay_region.border_enabled = true; + theme->snapping_overlay_edge.border_enabled = true; + } else { + theme->snapping_overlay_region.bg_enabled = true; + theme->snapping_overlay_edge.bg_enabled = true; + theme->snapping_overlay_region.border_enabled = false; + theme->snapping_overlay_edge.border_enabled = false; + } + + parse_hexstr("#8080b380", theme->snapping_overlay_region.bg_color); + parse_hexstr("#8080b380", theme->snapping_overlay_edge.bg_color); + + /* inherit settings in post_processing() if not set elsewhere */ + theme->snapping_overlay_region.border_width = INT_MIN; + theme->snapping_overlay_edge.border_width = INT_MIN; + zero_array(theme->snapping_overlay_region.border_color); + theme->snapping_overlay_region.border_color[0][0] = FLT_MIN; + zero_array(theme->snapping_overlay_edge.border_color); + theme->snapping_overlay_edge.border_color[0][0] = FLT_MIN; + + /* magnifier */ + parse_hexstr("#ff0000", theme->mag_border_color); + theme->mag_border_width = 1; +} + +static int +get_int_if_positive(const char *content, const char *field) +{ + int value = atoi(content); + if (value < 0) { + wlr_log(WLR_ERROR, + "%s cannot be negative, clamping it to 0.", field); + value = 0; + } + return value; +} + +static void +entry(struct theme *theme, const char *key, const char *value) +{ + if (!key || !value) { + return; + } + + /* + * Note that in order for the pattern match to apply to more than just + * the first instance, "else if" cannot be used throughout this function + */ + if (match_glob(key, "border.width")) { + theme->border_width = get_int_if_positive( + value, "border.width"); + } + if (match_glob(key, "window.titlebar.padding.width")) { + theme->window_titlebar_padding_width = get_int_if_positive( + value, "window.titlebar.padding.width"); + } + if (match_glob(key, "window.titlebar.padding.height")) { + theme->window_titlebar_padding_height = get_int_if_positive( + value, "window.titlebar.padding.height"); + } + if (match_glob(key, "titlebar.height")) { + wlr_log(WLR_ERROR, "titlebar.height is no longer supported"); + } + if (match_glob(key, "padding.height")) { + wlr_log(WLR_ERROR, "padding.height is no longer supported"); + } + if (match_glob(key, "menu.items.padding.x")) { + theme->menu_item_padding_x = get_int_if_positive( + value, "menu.items.padding.x"); + } + if (match_glob(key, "menu.items.padding.y")) { + theme->menu_item_padding_y = get_int_if_positive( + value, "menu.items.padding.y"); + } + if (match_glob(key, "menu.title.text.justify")) { + theme->menu_title_text_justify = parse_justification(value); + } + if (match_glob(key, "menu.overlap.x")) { + theme->menu_overlap_x = get_int_if_positive( + value, "menu.overlap.x"); + } + if (match_glob(key, "menu.overlap.y")) { + theme->menu_overlap_y = get_int_if_positive( + value, "menu.overlap.y"); + } + + if (match_glob(key, "window.active.border.color")) { + parse_hexstr(value, theme->window_active_border_color); + } + if (match_glob(key, "window.inactive.border.color")) { + parse_hexstr(value, theme->window_inactive_border_color); + } + /* border.color is obsolete, but handled for backward compatibility */ + if (match_glob(key, "border.color")) { + parse_hexstr(value, theme->window_active_border_color); + parse_hexstr(value, theme->window_inactive_border_color); + } + + if (match_glob(key, "window.active.indicator.toggled-keybind.color")) { + parse_hexstr(value, theme->window_toggled_keybinds_color); + } + + if (match_glob(key, "window.active.title.bg.color")) { + parse_hexstr(value, theme->window_active_title_bg_color); + } + if (match_glob(key, "window.inactive.title.bg.color")) { + parse_hexstr(value, theme->window_inactive_title_bg_color); + } + + if (match_glob(key, "window.active.label.text.color")) { + parse_hexstr(value, theme->window_active_label_text_color); + } + if (match_glob(key, "window.inactive.label.text.color")) { + parse_hexstr(value, theme->window_inactive_label_text_color); + } + if (match_glob(key, "window.label.text.justify")) { + theme->window_label_text_justify = parse_justification(value); + } + + if (match_glob(key, "window.button.width")) { + theme->window_button_width = atoi(value); + if (theme->window_button_width < 1) { + wlr_log(WLR_ERROR, "window.button.width cannot " + "be less than 1, clamping it to 1."); + theme->window_button_width = 1; + } + } + if (match_glob(key, "window.button.height")) { + theme->window_button_height = atoi(value); + if (theme->window_button_height < 1) { + wlr_log(WLR_ERROR, "window.button.height cannot " + "be less than 1, clamping it to 1."); + theme->window_button_height = 1; + } + } + if (match_glob(key, "window.button.spacing")) { + theme->window_button_spacing = get_int_if_positive( + value, "window.button.spacing"); + } + if (match_glob(key, "window.button.hover.bg.corner-radius")) { + theme->window_button_hover_bg_corner_radius = get_int_if_positive( + value, "window.button.hover.bg.corner-radius"); + } + + /* universal button */ + if (match_glob(key, "window.active.button.unpressed.image.color")) { + for (enum ssd_part_type type = LAB_SSD_BUTTON_FIRST; + type <= LAB_SSD_BUTTON_LAST; type++) { + parse_hexstr(value, + theme->window[THEME_ACTIVE].button_colors[type]); + } + } + if (match_glob(key, "window.inactive.button.unpressed.image.color")) { + for (enum ssd_part_type type = LAB_SSD_BUTTON_FIRST; + type <= LAB_SSD_BUTTON_LAST; type++) { + parse_hexstr(value, + theme->window[THEME_INACTIVE].button_colors[type]); + } + } + + /* individual buttons */ + if (match_glob(key, "window.active.button.menu.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_WINDOW_MENU]); + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_WINDOW_ICON]); + } + if (match_glob(key, "window.active.button.iconify.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_ICONIFY]); + } + if (match_glob(key, "window.active.button.max.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_MAXIMIZE]); + } + if (match_glob(key, "window.active.button.shade.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_SHADE]); + } + if (match_glob(key, "window.active.button.desk.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_OMNIPRESENT]); + } + if (match_glob(key, "window.active.button.close.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_ACTIVE] + .button_colors[LAB_SSD_BUTTON_CLOSE]); + } + if (match_glob(key, "window.inactive.button.menu.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_WINDOW_MENU]); + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_WINDOW_ICON]); + } + if (match_glob(key, "window.inactive.button.iconify.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_ICONIFY]); + } + if (match_glob(key, "window.inactive.button.max.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_MAXIMIZE]); + } + if (match_glob(key, "window.inactive.button.shade.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_SHADE]); + } + if (match_glob(key, "window.inactive.button.desk.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_OMNIPRESENT]); + } + if (match_glob(key, "window.inactive.button.close.unpressed.image.color")) { + parse_hexstr(value, theme->window[THEME_INACTIVE] + .button_colors[LAB_SSD_BUTTON_CLOSE]); + } + + /* window drop-shadows */ + if (match_glob(key, "window.active.shadow.size")) { + theme->window_active_shadow_size = get_int_if_positive( + value, "window.active.shadow.size"); + } + if (match_glob(key, "window.inactive.shadow.size")) { + theme->window_inactive_shadow_size = get_int_if_positive( + value, "window.inactive.shadow.size"); + } + if (match_glob(key, "window.active.shadow.color")) { + parse_hexstr(value, theme->window_active_shadow_color); + } + if (match_glob(key, "window.inactive.shadow.color")) { + parse_hexstr(value, theme->window_inactive_shadow_color); + } + + if (match_glob(key, "menu.width.min")) { + theme->menu_min_width = get_int_if_positive( + value, "menu.width.min"); + } + if (match_glob(key, "menu.width.max")) { + theme->menu_max_width = get_int_if_positive( + value, "menu.width.max"); + } + + if (match_glob(key, "menu.items.bg.color")) { + parse_hexstr(value, theme->menu_items_bg_color); + } + + if (match_glob(key, "menu.border.color")) { + parse_hexstr(value, theme->menu_border_color); + } + + if (match_glob(key, "menu.border.width")) { + theme->menu_border_width = get_int_if_positive( + value, "menu.border.width"); + } + + if (match_glob(key, "menu.items.text.color")) { + parse_hexstr(value, theme->menu_items_text_color); + } + if (match_glob(key, "menu.items.active.bg.color")) { + parse_hexstr(value, theme->menu_items_active_bg_color); + } + if (match_glob(key, "menu.items.active.text.color")) { + parse_hexstr(value, theme->menu_items_active_text_color); + } + + if (match_glob(key, "menu.separator.width")) { + theme->menu_separator_line_thickness = get_int_if_positive( + value, "menu.separator.width"); + } + if (match_glob(key, "menu.separator.padding.width")) { + theme->menu_separator_padding_width = get_int_if_positive( + value, "menu.separator.padding.width"); + } + if (match_glob(key, "menu.separator.padding.height")) { + theme->menu_separator_padding_height = get_int_if_positive( + value, "menu.separator.padding.height"); + } + if (match_glob(key, "menu.separator.color")) { + parse_hexstr(value, theme->menu_separator_color); + } + + if (match_glob(key, "menu.title.bg.color")) { + parse_hexstr(value, theme->menu_title_bg_color); + } + + if (match_glob(key, "menu.title.text.color")) { + parse_hexstr(value, theme->menu_title_text_color); + } + + if (match_glob(key, "osd.bg.color")) { + parse_hexstr(value, theme->osd_bg_color); + } + if (match_glob(key, "osd.border.width")) { + theme->osd_border_width = get_int_if_positive( + value, "osd.border.width"); + } + if (match_glob(key, "osd.border.color")) { + parse_hexstr(value, theme->osd_border_color); + } + if (match_glob(key, "osd.window-switcher.width")) { + if (strrchr(value, '%')) { + theme->osd_window_switcher_width_is_percent = true; + } else { + theme->osd_window_switcher_width_is_percent = false; + } + theme->osd_window_switcher_width = get_int_if_positive( + value, "osd.window-switcher.width"); + } + if (match_glob(key, "osd.window-switcher.padding")) { + theme->osd_window_switcher_padding = get_int_if_positive( + value, "osd.window-switcher.padding"); + } + if (match_glob(key, "osd.window-switcher.item.padding.x")) { + theme->osd_window_switcher_item_padding_x = + get_int_if_positive( + value, "osd.window-switcher.item.padding.x"); + } + if (match_glob(key, "osd.window-switcher.item.padding.y")) { + theme->osd_window_switcher_item_padding_y = + get_int_if_positive( + value, "osd.window-switcher.item.padding.y"); + } + if (match_glob(key, "osd.window-switcher.item.active.border.width")) { + theme->osd_window_switcher_item_active_border_width = + get_int_if_positive( + value, "osd.window-switcher.item.active.border.width"); + } + if (match_glob(key, "osd.window-switcher.preview.border.width")) { + theme->osd_window_switcher_preview_border_width = + get_int_if_positive( + value, "osd.window-switcher.preview.border.width"); + } + if (match_glob(key, "osd.window-switcher.preview.border.color")) { + parse_hexstrs(value, theme->osd_window_switcher_preview_border_color); + } + if (match_glob(key, "osd.workspace-switcher.boxes.width")) { + theme->osd_workspace_switcher_boxes_width = + get_int_if_positive( + value, "osd.workspace-switcher.boxes.width"); + } + if (match_glob(key, "osd.workspace-switcher.boxes.height")) { + theme->osd_workspace_switcher_boxes_height = + get_int_if_positive( + value, "osd.workspace-switcher.boxes.height"); + } + if (match_glob(key, "osd.label.text.color")) { + parse_hexstr(value, theme->osd_label_text_color); + } + if (match_glob(key, "snapping.overlay.region.bg.enabled")) { + set_bool(value, &theme->snapping_overlay_region.bg_enabled); + } + if (match_glob(key, "snapping.overlay.edge.bg.enabled")) { + set_bool(value, &theme->snapping_overlay_edge.bg_enabled); + } + if (match_glob(key, "snapping.overlay.region.border.enabled")) { + set_bool(value, &theme->snapping_overlay_region.border_enabled); + } + if (match_glob(key, "snapping.overlay.edge.border.enabled")) { + set_bool(value, &theme->snapping_overlay_edge.border_enabled); + } + if (match_glob(key, "snapping.overlay.region.bg.color")) { + parse_hexstr(value, theme->snapping_overlay_region.bg_color); + } + if (match_glob(key, "snapping.overlay.edge.bg.color")) { + parse_hexstr(value, theme->snapping_overlay_edge.bg_color); + } + if (match_glob(key, "snapping.overlay.region.border.width")) { + theme->snapping_overlay_region.border_width = get_int_if_positive( + value, "snapping.overlay.region.border.width"); + } + if (match_glob(key, "snapping.overlay.edge.border.width")) { + theme->snapping_overlay_edge.border_width = get_int_if_positive( + value, "snapping.overlay.edge.border.width"); + } + if (match_glob(key, "snapping.overlay.region.border.color")) { + parse_hexstrs(value, theme->snapping_overlay_region.border_color); + } + if (match_glob(key, "snapping.overlay.edge.border.color")) { + parse_hexstrs(value, theme->snapping_overlay_edge.border_color); + } + + if (match_glob(key, "magnifier.border.width")) { + theme->mag_border_width = get_int_if_positive( + value, "magnifier.border.width"); + } + if (match_glob(key, "magnifier.border.color")) { + parse_hexstr(value, theme->mag_border_color); + } +} + +static void +parse_config_line(char *line, char **key, char **value) +{ + char *p = strchr(line, ':'); + if (!p) { + return; + } + *p = '\0'; + *key = string_strip(line); + *value = string_strip(++p); +} + +static void +process_line(struct theme *theme, char *line) +{ + if (line[0] == '\0' || line[0] == '#') { + return; + } + char *key = NULL, *value = NULL; + parse_config_line(line, &key, &value); + entry(theme, key, value); +} + +static void +theme_read(struct theme *theme, struct wl_list *paths) +{ + bool should_merge_config = rc.merge_config; + struct wl_list *(*iter)(struct wl_list *list); + iter = should_merge_config ? paths_get_prev : paths_get_next; + + for (struct wl_list *elm = iter(paths); elm != paths; elm = iter(elm)) { + struct path *path = wl_container_of(elm, path, link); + FILE *stream = fopen(path->string, "r"); + if (!stream) { + continue; + } + + wlr_log(WLR_INFO, "read theme %s", path->string); + + char *line = NULL; + size_t len = 0; + while (getline(&line, &len, stream) != -1) { + char *p = strrchr(line, '\n'); + if (p) { + *p = '\0'; + } + process_line(theme, line); + } + zfree(line); + fclose(stream); + if (!should_merge_config) { + break; + } + } +} + +static struct lab_data_buffer * +rounded_rect(struct rounded_corner_ctx *ctx) +{ + if (ctx->corner == LAB_CORNER_UNKNOWN) { + return NULL; + } + + double w = ctx->box->width; + double h = ctx->box->height; + double r = ctx->radius; + + struct lab_data_buffer *buffer; + /* TODO: scale */ + buffer = buffer_create_cairo(w, h, 1); + + cairo_t *cairo = buffer->cairo; + cairo_surface_t *surf = cairo_get_target(cairo); + + /* set transparent background */ + cairo_set_operator(cairo, CAIRO_OPERATOR_CLEAR); + cairo_paint(cairo); + + /* + * Create outline path and fill. Illustration of top-left corner buffer: + * + * _,,ooO"""""""""+ + * ,oO"' ^ | + * ,o" | | + * o" |r | + * o' | | + * O r v | + * O<--------->+ | + * O | + * O | + * O | + * +--------------------+ + */ + cairo_set_line_width(cairo, 0.0); + cairo_new_sub_path(cairo); + switch (ctx->corner) { + case LAB_CORNER_TOP_LEFT: + cairo_arc(cairo, r, r, r, 180 * deg, 270 * deg); + cairo_line_to(cairo, w, 0); + cairo_line_to(cairo, w, h); + cairo_line_to(cairo, 0, h); + break; + case LAB_CORNER_TOP_RIGHT: + cairo_arc(cairo, w - r, r, r, -90 * deg, 0 * deg); + cairo_line_to(cairo, w, h); + cairo_line_to(cairo, 0, h); + cairo_line_to(cairo, 0, 0); + break; + default: + wlr_log(WLR_ERROR, "unknown corner type"); + } + cairo_close_path(cairo); + cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); + set_cairo_color(cairo, ctx->fill_color); + cairo_fill_preserve(cairo); + cairo_stroke(cairo); + + /* + * Stroke horizontal and vertical borders, shown by Xs and Ys + * respectively in the figure below: + * + * _,,ooO"XXXXXXXXX + * ,oO"' | + * ,o" | + * o" | + * o' | + * O | + * Y | + * Y | + * Y | + * Y | + * Y--------------------+ + */ + cairo_set_line_cap(cairo, CAIRO_LINE_CAP_BUTT); + set_cairo_color(cairo, ctx->border_color); + cairo_set_line_width(cairo, ctx->line_width); + double half_line_width = ctx->line_width / 2.0; + switch (ctx->corner) { + case LAB_CORNER_TOP_LEFT: + cairo_move_to(cairo, half_line_width, h); + cairo_line_to(cairo, half_line_width, r); + cairo_move_to(cairo, r, half_line_width); + cairo_line_to(cairo, w, half_line_width); + break; + case LAB_CORNER_TOP_RIGHT: + cairo_move_to(cairo, 0, half_line_width); + cairo_line_to(cairo, w - r, half_line_width); + cairo_move_to(cairo, w - half_line_width, r); + cairo_line_to(cairo, w - half_line_width, h); + break; + default: + wlr_log(WLR_ERROR, "unknown corner type"); + } + cairo_stroke(cairo); + + /* + * If radius==0 the borders stroked above go right up to (and including) + * the corners, so there is not need to do any more. + */ + if (!r) { + goto out; + } + + /* + * Stroke the arc section of the border of the corner piece. + * + * Note: This figure is drawn at a more zoomed in scale compared with + * those above. + * + * ,,ooooO"" ^ + * ,ooo""' | | + * ,oOO" | | line-thickness + * ,OO" | | + * ,OO" _,,ooO"" v + * ,O" ,oO"' + * ,O' ,o" + * ,O' o" + * o' o' + * O O + * O---------O + + * <-----------------> + * radius + * + * We handle the edge-case where line-thickness > radius by merely + * setting line-thickness = radius and in effect drawing a quadrant of a + * circle. In this case the X and Y borders butt up against the arc and + * overlap each other (as their line-thicknesses are greater than the + * line-thickness of the arc). As a result, there is no inner rounded + * corners. + * + * So, in order to have inner rounded corners cornerRadius should be + * greater than border.width. + * + * Also, see diagrams in https://github.com/labwc/labwc/pull/990 + */ + double line_width = MIN(ctx->line_width, r); + cairo_set_line_width(cairo, line_width); + half_line_width = line_width / 2.0; + switch (ctx->corner) { + case LAB_CORNER_TOP_LEFT: + cairo_move_to(cairo, half_line_width, r); + cairo_arc(cairo, r, r, r - half_line_width, 180 * deg, 270 * deg); + break; + case LAB_CORNER_TOP_RIGHT: + cairo_move_to(cairo, w - r, half_line_width); + cairo_arc(cairo, w - r, r, r - half_line_width, -90 * deg, 0 * deg); + break; + default: + break; + } + cairo_stroke(cairo); + +out: + cairo_surface_flush(surf); + + return buffer; +} + +static void +create_corners(struct theme *theme) +{ + int corner_width = ssd_get_corner_width(); + + struct wlr_box box = { + .x = 0, + .y = 0, + .width = corner_width + theme->border_width, + .height = theme->title_height + theme->border_width, + }; + + struct rounded_corner_ctx ctx = { + .box = &box, + .radius = rc.corner_radius, + .line_width = theme->border_width, + .fill_color = theme->window_active_title_bg_color, + .border_color = theme->window_active_border_color, + .corner = LAB_CORNER_TOP_LEFT, + }; + theme->corner_top_left_active_normal = rounded_rect(&ctx); + + ctx.fill_color = theme->window_inactive_title_bg_color, + ctx.border_color = theme->window_inactive_border_color, + theme->corner_top_left_inactive_normal = rounded_rect(&ctx); + + ctx.corner = LAB_CORNER_TOP_RIGHT; + ctx.fill_color = theme->window_active_title_bg_color, + ctx.border_color = theme->window_active_border_color, + theme->corner_top_right_active_normal = rounded_rect(&ctx); + + ctx.fill_color = theme->window_inactive_title_bg_color, + ctx.border_color = theme->window_inactive_border_color, + theme->corner_top_right_inactive_normal = rounded_rect(&ctx); +} + +/* + * Draw the buffer used to render the edges of window drop-shadows. The buffer + * is 1 pixel tall and `visible_size` pixels wide and can be rotated and scaled for the + * different edges. The buffer is drawn as would be found at the right-hand + * edge of a window. The gradient has a color of `start_color` at its left edge + * fading to clear at its right edge. + */ +static void +shadow_edge_gradient(struct lab_data_buffer *buffer, + int visible_size, int total_size, float start_color[4]) +{ + if (!buffer) { + /* This type of shadow is disabled, do nothing */ + return; + } + + assert(buffer->format == DRM_FORMAT_ARGB8888); + uint8_t *pixels = buffer->data; + + /* Inset portion which is obscured */ + int inset = total_size - visible_size; + + /* Standard deviation normalised against the shadow width, squared */ + double variance = 0.3 * 0.3; + + for (int x = 0; x < visible_size; x++) { + /* + * x normalised against total shadow width. We add on inset here + * because we don't bother drawing inset for the edge shadow + * buffers but still need the pattern to line up with the corner + * shadow buffers which do have inset drawn. + */ + double xn = (double)(x + inset) / (double)total_size; + + /* Gausian dropoff */ + double alpha = exp(-(xn * xn) / variance); + + /* RGBA values are all pre-multiplied */ + pixels[4 * x] = start_color[2] * alpha * 255; + pixels[4 * x + 1] = start_color[1] * alpha * 255; + pixels[4 * x + 2] = start_color[0] * alpha * 255; + pixels[4 * x + 3] = start_color[3] * alpha * 255; + } +} + +/* + * Draw the buffer used to render the corners of window drop-shadows. The + * shadow looks better if the buffer is inset behind the window, so the buffer + * is square with a size of radius+inset. The buffer is drawn for the + * bottom-right corner but can be rotated for other corners. The gradient fades + * from `start_color` at the top-left to clear at the opposite edge. + * + * If the window is translucent we don't want the shadow to be visible through + * it. For the bottom corners of the window this is easy, we just erase the + * square of the buffer which will be behind the window. For the top it's a + * little more complicated because the titlebar can have rounded corners. + * However, the titlebar itself is always opaque so we only have to erase the + * L-shaped area of the buffer which can appear behind the non-titlebar part of + * the window. + */ +static void +shadow_corner_gradient(struct lab_data_buffer *buffer, int visible_size, + int total_size, int titlebar_height, float start_color[4]) +{ + if (!buffer) { + /* This type of shadow is disabled, do nothing */ + return; + } + + assert(buffer->format == DRM_FORMAT_ARGB8888); + uint8_t *pixels = buffer->data; + + /* Standard deviation normalised against the shadow width, squared */ + double variance = 0.3 * 0.3; + + int inset = total_size - visible_size; + + for (int y = 0; y < total_size; y++) { + uint8_t *pixel_row = &pixels[y * buffer->stride]; + for (int x = 0; x < total_size; x++) { + /* x and y normalised against total shadow width */ + double x_norm = (double)(x) / (double)total_size; + double y_norm = (double)(y) / (double)total_size; + /* + * For Gaussian drop-off in 2d you can just calculate + * the outer product of the horizontal and vertical + * profiles. + */ + double gauss_x = exp(-(x_norm * x_norm) / variance); + double gauss_y = exp(-(y_norm * y_norm) / variance); + double alpha = gauss_x * gauss_y; + + /* + * Erase the L-shaped region which could be visible + * through a transparent window but not obscured by the + * titlebar. If inset is smaller than the titlebar + * height then there's nothing to do, this is handled by + * (inset - titlebar_height) being negative. + */ + bool in1 = x < inset && y < inset - titlebar_height; + bool in2 = x < inset - titlebar_height && y < inset; + if (in1 || in2) { + alpha = 0.0; + } + + /* RGBA values are all pre-multiplied */ + pixel_row[4 * x] = start_color[2] * alpha * 255; + pixel_row[4 * x + 1] = start_color[1] * alpha * 255; + pixel_row[4 * x + 2] = start_color[0] * alpha * 255; + pixel_row[4 * x + 3] = start_color[3] * alpha * 255; + } + } +} + +static void +create_shadows(struct theme *theme) +{ + /* Size of shadow visible extending beyond the window */ + int visible_active_size = theme->window_active_shadow_size; + int visible_inactive_size = theme->window_inactive_shadow_size; + /* How far inside the window the shadow inset begins */ + int inset_active = (double)visible_active_size * SSD_SHADOW_INSET; + int inset_inactive = (double)visible_inactive_size * SSD_SHADOW_INSET; + /* Total width including visible and obscured portion */ + int total_active_size = visible_active_size + inset_active; + int total_inactive_size = visible_inactive_size + inset_inactive; + + /* + * Edge shadows don't need to be inset so the buffers are sized just for + * the visible width. Corners are inset so the buffers are larger for + * this. + */ + if (visible_active_size > 0) { + theme->shadow_edge_active = buffer_create_cairo( + visible_active_size, 1, 1.0); + theme->shadow_corner_top_active = buffer_create_cairo( + total_active_size, total_active_size, 1.0); + theme->shadow_corner_bottom_active = buffer_create_cairo( + total_active_size, total_active_size, 1.0); + if (!theme->shadow_corner_top_active + || !theme->shadow_corner_bottom_active + || !theme->shadow_edge_active) { + wlr_log(WLR_ERROR, "Failed to allocate shadow buffer"); + return; + } + } + if (visible_inactive_size > 0) { + theme->shadow_edge_inactive = buffer_create_cairo( + visible_inactive_size, 1, 1.0); + theme->shadow_corner_top_inactive = buffer_create_cairo( + total_inactive_size, total_inactive_size, 1.0); + theme->shadow_corner_bottom_inactive = buffer_create_cairo( + total_inactive_size, total_inactive_size, 1.0); + if (!theme->shadow_corner_top_inactive + || !theme->shadow_corner_bottom_inactive + || !theme->shadow_edge_inactive) { + wlr_log(WLR_ERROR, "Failed to allocate shadow buffer"); + return; + } + } + + shadow_edge_gradient(theme->shadow_edge_active, visible_active_size, + total_active_size, theme->window_active_shadow_color); + shadow_edge_gradient(theme->shadow_edge_inactive, visible_inactive_size, + total_inactive_size, theme->window_inactive_shadow_color); + shadow_corner_gradient(theme->shadow_corner_top_active, + visible_active_size, total_active_size, + theme->title_height, theme->window_active_shadow_color); + shadow_corner_gradient(theme->shadow_corner_bottom_active, + visible_active_size, total_active_size, 0, + theme->window_active_shadow_color); + shadow_corner_gradient(theme->shadow_corner_top_inactive, + visible_inactive_size, total_inactive_size, + theme->title_height, theme->window_inactive_shadow_color); + shadow_corner_gradient(theme->shadow_corner_bottom_inactive, + visible_inactive_size, total_inactive_size, 0, + theme->window_inactive_shadow_color); +} + +static void +fill_colors_with_osd_theme(struct theme *theme, float colors[3][4]) +{ + memcpy(colors[0], theme->osd_bg_color, sizeof(colors[0])); + memcpy(colors[1], theme->osd_label_text_color, sizeof(colors[1])); + memcpy(colors[2], theme->osd_bg_color, sizeof(colors[2])); +} + +static int +get_titlebar_height(struct theme *theme) +{ + int h = MAX(font_height(&rc.font_activewindow), + font_height(&rc.font_inactivewindow)); + if (h < theme->window_button_height) { + h = theme->window_button_height; + } + h += 2 * theme->window_titlebar_padding_height; + return h; +} + +static void +post_processing(struct theme *theme) +{ + theme->title_height = get_titlebar_height(theme); + + theme->menu_item_height = font_height(&rc.font_menuitem) + + 2 * theme->menu_item_padding_y; + + theme->osd_window_switcher_item_height = font_height(&rc.font_osd) + + 2 * theme->osd_window_switcher_item_padding_y + + 2 * theme->osd_window_switcher_item_active_border_width; + + if (rc.corner_radius >= theme->title_height) { + rc.corner_radius = theme->title_height - 1; + } + + int min_button_hover_radius = + MIN(theme->window_button_width, theme->window_button_height) / 2; + if (theme->window_button_hover_bg_corner_radius > min_button_hover_radius) { + theme->window_button_hover_bg_corner_radius = min_button_hover_radius; + } + + if (theme->menu_max_width < theme->menu_min_width) { + wlr_log(WLR_ERROR, + "Adjusting menu.width.max: .max (%d) lower than .min (%d)", + theme->menu_max_width, theme->menu_min_width); + theme->menu_max_width = theme->menu_min_width; + } + + /* Inherit OSD settings if not set */ + if (theme->osd_bg_color[0] == FLT_MIN) { + memcpy(theme->osd_bg_color, + theme->window_active_title_bg_color, + sizeof(theme->osd_bg_color)); + } + if (theme->osd_border_width == INT_MIN) { + theme->osd_border_width = theme->border_width; + } + if (theme->osd_label_text_color[0] == FLT_MIN) { + memcpy(theme->osd_label_text_color, + theme->window_active_label_text_color, + sizeof(theme->osd_label_text_color)); + } + if (theme->osd_border_color[0] == FLT_MIN) { + /* + * As per http://openbox.org/wiki/Help:Themes#osd.border.color + * we should fall back to window_active_border_color but + * that is usually the same as window_active_title_bg_color + * and thus the fallback for osd_bg_color. Which would mean + * they are both the same color and thus the border is invisible. + * + * Instead, we fall back to osd_label_text_color which in turn + * falls back to window_active_label_text_color. + */ + memcpy(theme->osd_border_color, theme->osd_label_text_color, + sizeof(theme->osd_border_color)); + } + if (theme->osd_workspace_switcher_boxes_width == 0) { + theme->osd_workspace_switcher_boxes_height = 0; + } + if (theme->osd_workspace_switcher_boxes_height == 0) { + theme->osd_workspace_switcher_boxes_width = 0; + } + if (theme->osd_window_switcher_width_is_percent) { + theme->osd_window_switcher_width = + MIN(theme->osd_window_switcher_width, 100); + } + if (theme->osd_window_switcher_preview_border_width == INT_MIN) { + theme->osd_window_switcher_preview_border_width = + theme->osd_border_width; + } + if (theme->osd_window_switcher_preview_border_color[0][0] == FLT_MIN) { + fill_colors_with_osd_theme(theme, + theme->osd_window_switcher_preview_border_color); + } + + if (theme->snapping_overlay_region.border_width == INT_MIN) { + theme->snapping_overlay_region.border_width = + theme->osd_border_width; + } + if (theme->snapping_overlay_edge.border_width == INT_MIN) { + theme->snapping_overlay_edge.border_width = + theme->osd_border_width; + } + if (theme->snapping_overlay_region.border_color[0][0] == FLT_MIN) { + fill_colors_with_osd_theme(theme, + theme->snapping_overlay_region.border_color); + } + if (theme->snapping_overlay_edge.border_color[0][0] == FLT_MIN) { + fill_colors_with_osd_theme(theme, + theme->snapping_overlay_edge.border_color); + } +} + +void +theme_init(struct theme *theme, struct server *server, const char *theme_name) +{ + /* + * Set some default values. This is particularly important on + * reconfigure as not all themes set all options + */ + theme_builtin(theme, server); + + /* Read /share/themes/$theme_name/openbox-3/themerc */ + struct wl_list paths; + paths_theme_create(&paths, theme_name, "themerc"); + theme_read(theme, &paths); + paths_destroy(&paths); + + /* Read /labwc/themerc-override */ + paths_config_create(&paths, "themerc-override"); + theme_read(theme, &paths); + paths_destroy(&paths); + + post_processing(theme); + create_corners(theme); + load_buttons(theme); + create_shadows(theme); +} + +void +theme_finish(struct theme *theme) +{ + for (enum ssd_part_type type = LAB_SSD_BUTTON_FIRST; + type <= LAB_SSD_BUTTON_LAST; type++) { + for (uint8_t state_set = 0; state_set <= LAB_BS_ALL; + state_set++) { + zdrop(&theme->window[THEME_INACTIVE] + .buttons[type][state_set]); + zdrop(&theme->window[THEME_ACTIVE] + .buttons[type][state_set]); + } + } + + zdrop(&theme->corner_top_left_active_normal); + zdrop(&theme->corner_top_left_inactive_normal); + zdrop(&theme->corner_top_right_active_normal); + zdrop(&theme->corner_top_right_inactive_normal); + + zdrop(&theme->shadow_corner_top_active); + zdrop(&theme->shadow_corner_bottom_active); + zdrop(&theme->shadow_edge_active); + zdrop(&theme->shadow_corner_top_inactive); + zdrop(&theme->shadow_corner_bottom_inactive); + zdrop(&theme->shadow_edge_inactive); +}