// SPDX-License-Identifier: GPL-2.0-only /* * Theme engine for labwc * * Copyright (C) Johan Malm 2020-2023 */ #define _POSIX_C_SOURCE 200809L #include "theme.h" #include "config.h" #include #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/string-helpers.h" #include "config/rcxml.h" #include "img/img.h" #include "labwc.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 rounded_corner { ROUNDED_CORNER_TOP_LEFT, ROUNDED_CORNER_TOP_RIGHT }; struct rounded_corner_ctx { struct wlr_box *box; double radius; double line_width; cairo_pattern_t *fill_pattern; float *border_color; enum rounded_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; } } /* Draw rounded-rectangular hover overlay on the button buffer */ static void draw_hover_overlay_on_button(cairo_t *cairo, int w, int h) { /* Overlay (pre-multiplied alpha) */ float overlay_color[4] = { 0.15f, 0.15f, 0.15f, 0.3f}; set_cairo_color(cairo, overlay_color); int r = rc.theme->window_button_hover_bg_corner_radius; cairo_new_sub_path(cairo); cairo_arc(cairo, r, r, r, 180 * deg, 270 * deg); cairo_line_to(cairo, w - r, 0); cairo_arc(cairo, w - r, r, r, -90 * deg, 0 * deg); cairo_line_to(cairo, w, h - r); cairo_arc(cairo, w - r, h - r, r, 0 * deg, 90 * deg); cairo_line_to(cairo, r, h); cairo_arc(cairo, r, h - r, r, 90 * deg, 180 * deg); cairo_close_path(cairo); cairo_fill(cairo); } /* Round the buffer for the leftmost button in the titlebar */ static void round_left_corner_button(cairo_t *cairo, int w, int h) { /* * Position of the topleft corner of the titlebar relative to the * leftmost button */ double x = -rc.theme->window_titlebar_padding_width; double y = -(rc.theme->titlebar_height - rc.theme->window_button_height) / 2; double r = rc.corner_radius - (double)rc.theme->border_width / 2.0; cairo_new_sub_path(cairo); cairo_arc(cairo, x + r, y + r, r, deg * 180, deg * 270); cairo_line_to(cairo, w, y); cairo_line_to(cairo, w, h); cairo_line_to(cairo, x, h); cairo_close_path(cairo); cairo_set_source_rgba(cairo, 1, 1, 1, 1); cairo_set_operator(cairo, CAIRO_OPERATOR_DEST_IN); cairo_fill(cairo); } /* Round the buffer for the rightmost button in the titlebar */ static void round_right_corner_button(cairo_t *cairo, int w, int h) { /* * Horizontally flip the cairo context so we can reuse * round_left_corner_button() for rounding the rightmost button. */ cairo_scale(cairo, -1, 1); cairo_translate(cairo, -w, 0); round_left_corner_button(cairo, w, h); } /* * 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_img *(*button_imgs)[LAB_BS_ALL + 1] = theme->window[active].button_imgs; struct lab_img **img = &button_imgs[b->type][b->state_set]; float *rgba = theme->window[active].button_colors[b->type]; char filename[4096]; assert(!*img); /* PNG */ get_button_filename(filename, sizeof(filename), b->name, active ? "-active.png" : "-inactive.png"); *img = lab_img_load(LAB_IMG_PNG, filename, rgba); #if HAVE_RSVG /* SVG */ if (!*img) { get_button_filename(filename, sizeof(filename), b->name, active ? "-active.svg" : "-inactive.svg"); *img = lab_img_load(LAB_IMG_SVG, filename, rgba); } #endif /* XBM */ if (!*img) { get_button_filename(filename, sizeof(filename), b->name, ".xbm"); *img = lab_img_load(LAB_IMG_XBM, filename, rgba); } /* * XBM (alternative name) * For example max_hover_toggled instead of max_toggled_hover */ if (!*img && b->alt_name) { get_button_filename(filename, sizeof(filename), b->alt_name, ".xbm"); *img = lab_img_load(LAB_IMG_XBM, filename, rgba); } /* * Builtin bitmap * * Applicable to basic buttons such as max, max_toggled and iconify. * There are no bitmap fallbacks for *_hover icons. */ if (!*img && b->fallback_button) { *img = lab_img_load_from_bitmap(b->fallback_button, rgba); } /* * If hover-icons do not exist, add fallbacks by copying the non-hover * variant and then adding an overlay. */ if (!*img && (b->state_set & LAB_BS_HOVERD)) { struct lab_img *non_hover_img = button_imgs[b->type][b->state_set & ~LAB_BS_HOVERD]; *img = lab_img_copy(non_hover_img); lab_img_add_modifier(*img, draw_hover_overlay_on_button); } /* * If the loaded button is at the corner of the titlebar, also create * rounded variants. */ struct lab_img **rounded_img = &button_imgs[b->type][b->state_set | LAB_BS_ROUNDED]; struct title_button *leftmost_button; wl_list_for_each(leftmost_button, &rc.title_buttons_left, link) { if (leftmost_button->type == b->type) { *rounded_img = lab_img_copy(*img); lab_img_add_modifier(*rounded_img, round_left_corner_button); } break; } struct title_button *rightmost_button; wl_list_for_each_reverse(rightmost_button, &rc.title_buttons_right, link) { if (rightmost_button->type == b->type) { *rounded_img = lab_img_copy(*img); lab_img_add_modifier(*rounded_img, round_right_corner_button); } break; } } /* * 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", .fallback_button = (const char[]){ 0x00, 0x21, 0x33, 0x1E, 0x0C, 0x00 }, .type = LAB_SSD_BUTTON_WINDOW_MENU, .state_set = 0, }, { .name = "iconify", .fallback_button = (const char[]){ 0x00, 0x00, 0x00, 0x00, 0x3f, 0x3f }, .type = LAB_SSD_BUTTON_ICONIFY, .state_set = 0, }, { .name = "max", .fallback_button = (const char[]){ 0x3f, 0x3f, 0x21, 0x21, 0x21, 0x3f }, .type = LAB_SSD_BUTTON_MAXIMIZE, .state_set = 0, }, { .name = "max_toggled", .fallback_button = (const char[]){ 0x3e, 0x22, 0x2f, 0x29, 0x39, 0x0f }, .type = LAB_SSD_BUTTON_MAXIMIZE, .state_set = LAB_BS_TOGGLED, }, { .name = "shade", .fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x0c, 0x1e, 0x3f }, .type = LAB_SSD_BUTTON_SHADE, .state_set = 0, }, { .name = "shade_toggled", .fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x3f, 0x1e, 0x0c }, .type = LAB_SSD_BUTTON_SHADE, .state_set = LAB_BS_TOGGLED, }, { .name = "desk", .fallback_button = (const char[]){ 0x33, 0x33, 0x00, 0x00, 0x33, 0x33 }, .type = LAB_SSD_BUTTON_OMNIPRESENT, .state_set = 0, }, { .name = "desk_toggled", .fallback_button = (const char[]){ 0x00, 0x1e, 0x1a, 0x16, 0x1e, 0x00 }, .type = LAB_SSD_BUTTON_OMNIPRESENT, .state_set = LAB_BS_TOGGLED, }, { .name = "close", .fallback_button = (const char[]){ 0x33, 0x3f, 0x1e, 0x1e, 0x3f, 0x33 }, .type = LAB_SSD_BUTTON_CLOSE, .state_set = 0, }, { .name = "menu_hover", .type = LAB_SSD_BUTTON_WINDOW_MENU, .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[0] != '#') { return; } size_t len = strlen(hex); if (len == 4) { /* #fff is shorthand for #f0f0f0, per theme spec */ rgba[0] = (hex_to_dec(hex[1]) * 16) / 255.0; rgba[1] = (hex_to_dec(hex[2]) * 16) / 255.0; rgba[2] = (hex_to_dec(hex[3]) * 16) / 255.0; } else if (len >= 7) { 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; } else { return; } rgba[3] = 1.0; 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 void parse_color(const char *str, float *rgba) { if (str[0] == '#') { parse_hexstr(str, rgba); } else if (!strncasecmp(str, "rgb:", 4)) { size_t len = strlen(str); if (len == 9 && str[5] == '/' && str[7] == '/') { /* rgb:R/G/B */ rgba[0] = (hex_to_dec(str[4]) * 16) / 255.0; rgba[1] = (hex_to_dec(str[6]) * 16) / 255.0; rgba[2] = (hex_to_dec(str[8]) * 16) / 255.0; rgba[3] = 1.0; } else if (len == 12 && str[6] == '/' && str[9] == '/') { /* rgb:RR/GG/BB */ rgba[0] = (hex_to_dec(str[4]) * 16 + hex_to_dec(str[5])) / 255.0; rgba[1] = (hex_to_dec(str[7]) * 16 + hex_to_dec(str[8])) / 255.0; rgba[2] = (hex_to_dec(str[10]) * 16 + hex_to_dec(str[11])) / 255.0; rgba[3] = 1.0; } } else { uint32_t argb = 0; if (lookup_named_color(str, &argb)) { rgba[0] = ((argb >> 16) & 0xFF) / 255.0; rgba[1] = ((argb >> 8) & 0xFF) / 255.0; rgba[2] = (argb & 0xFF) / 255.0; rgba[3] = 1.0; } } } static enum lab_gradient parse_gradient(const char *str) { /* * Parsing of "texture" strings is very loose, following Openbox: * just a case-insensitive match of substrings of interest, with * no regard for ordering nor whitespace. */ char *lower = g_ascii_strdown(str, -1); enum lab_gradient gradient = LAB_GRADIENT_NONE; if (strstr(lower, "gradient")) { if (strstr(lower, "splitvertical")) { gradient = LAB_GRADIENT_SPLITVERTICAL; } else if (strstr(lower, "vertical")) { gradient = LAB_GRADIENT_VERTICAL; } } g_free(lower); return gradient; } 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->window_titlebar_padding_height = 0; theme->window_titlebar_padding_width = 0; parse_hexstr("#aaaaaa", theme->window[THEME_ACTIVE].border_color); parse_hexstr("#aaaaaa", theme->window[THEME_INACTIVE].border_color); parse_hexstr("#ff0000", theme->window_toggled_keybinds_color); theme->window[THEME_ACTIVE].title_bg.gradient = LAB_GRADIENT_NONE; theme->window[THEME_INACTIVE].title_bg.gradient = LAB_GRADIENT_NONE; parse_hexstr("#e1dedb", theme->window[THEME_ACTIVE].title_bg.color); parse_hexstr("#f6f5f4", theme->window[THEME_INACTIVE].title_bg.color); theme->window[THEME_ACTIVE].title_bg.color_split_to[0] = FLT_MIN; theme->window[THEME_INACTIVE].title_bg.color_split_to[0] = FLT_MIN; theme->window[THEME_ACTIVE].title_bg.color_to[0] = FLT_MIN; theme->window[THEME_INACTIVE].title_bg.color_to[0] = FLT_MIN; theme->window[THEME_ACTIVE].title_bg.color_to_split_to[0] = FLT_MIN; theme->window[THEME_INACTIVE].title_bg.color_to_split_to[0] = FLT_MIN; parse_hexstr("#000000", theme->window[THEME_ACTIVE].label_text_color); parse_hexstr("#000000", theme->window[THEME_INACTIVE].label_text_color); theme->window_label_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[THEME_ACTIVE].shadow_size = 60; theme->window[THEME_INACTIVE].shadow_size = 40; parse_hexstr("#00000060", theme->window[THEME_ACTIVE].shadow_color); parse_hexstr("#00000040", theme->window[THEME_INACTIVE].shadow_color); theme->menu_overlap_x = 0; theme->menu_overlap_y = 0; theme->menu_min_width = 20; theme->menu_max_width = 200; theme->menu_border_width = INT_MIN; theme->menu_border_color[0] = FLT_MIN; theme->menu_items_padding_x = 7; theme->menu_items_padding_y = 4; 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_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); theme->menu_title_text_justify = parse_justification("Center"); parse_hexstr("#ffffff", theme->menu_title_text_color); theme->osd_window_switcher_classic.width = 600; theme->osd_window_switcher_classic.width_is_percent = false; theme->osd_window_switcher_classic.padding = 4; theme->osd_window_switcher_classic.item_padding_x = 10; theme->osd_window_switcher_classic.item_padding_y = 1; theme->osd_window_switcher_classic.item_active_border_width = 2; theme->osd_window_switcher_classic.item_icon_size = -1; /* 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; theme->osd_workspace_switcher_boxes_border_width = 2; /* 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; } struct window_switcher_classic_theme *switcher_classic_theme = &theme->osd_window_switcher_classic; /* * 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_INFO, "padding.height is no longer supported"); } if (match_glob(key, "window.active.border.color")) { parse_color(value, theme->window[THEME_ACTIVE].border_color); } if (match_glob(key, "window.inactive.border.color")) { parse_color(value, theme->window[THEME_INACTIVE].border_color); } /* border.color is obsolete, but handled for backward compatibility */ if (match_glob(key, "border.color")) { parse_color(value, theme->window[THEME_ACTIVE].border_color); parse_color(value, theme->window[THEME_INACTIVE].border_color); } if (match_glob(key, "window.active.indicator.toggled-keybind.color")) { parse_color(value, theme->window_toggled_keybinds_color); } if (match_glob(key, "window.active.title.bg")) { theme->window[THEME_ACTIVE].title_bg.gradient = parse_gradient(value); } if (match_glob(key, "window.inactive.title.bg")) { theme->window[THEME_INACTIVE].title_bg.gradient = parse_gradient(value); } if (match_glob(key, "window.active.title.bg.color")) { parse_color(value, theme->window[THEME_ACTIVE].title_bg.color); } if (match_glob(key, "window.inactive.title.bg.color")) { parse_color(value, theme->window[THEME_INACTIVE].title_bg.color); } if (match_glob(key, "window.active.title.bg.color.splitTo")) { parse_color(value, theme->window[THEME_ACTIVE].title_bg.color_split_to); } if (match_glob(key, "window.inactive.title.bg.color.splitTo")) { parse_color(value, theme->window[THEME_INACTIVE].title_bg.color_split_to); } if (match_glob(key, "window.active.title.bg.colorTo")) { parse_color(value, theme->window[THEME_ACTIVE].title_bg.color_to); } if (match_glob(key, "window.inactive.title.bg.colorTo")) { parse_color(value, theme->window[THEME_INACTIVE].title_bg.color_to); } if (match_glob(key, "window.active.title.bg.colorTo.splitTo")) { parse_color(value, theme->window[THEME_ACTIVE].title_bg.color_to_split_to); } if (match_glob(key, "window.inactive.title.bg.colorTo.splitTo")) { parse_color(value, theme->window[THEME_INACTIVE].title_bg.color_to_split_to); } if (match_glob(key, "window.active.label.text.color")) { parse_color(value, theme->window[THEME_ACTIVE].label_text_color); } if (match_glob(key, "window.inactive.label.text.color")) { parse_color(value, theme->window[THEME_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_color(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_color(value, theme->window[THEME_INACTIVE].button_colors[type]); } } /* individual buttons */ if (match_glob(key, "window.active.button.menu.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_WINDOW_MENU]); parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_WINDOW_ICON]); } if (match_glob(key, "window.active.button.iconify.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_ICONIFY]); } if (match_glob(key, "window.active.button.max.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_MAXIMIZE]); } if (match_glob(key, "window.active.button.shade.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_SHADE]); } if (match_glob(key, "window.active.button.desk.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_OMNIPRESENT]); } if (match_glob(key, "window.active.button.close.unpressed.image.color")) { parse_color(value, theme->window[THEME_ACTIVE] .button_colors[LAB_SSD_BUTTON_CLOSE]); } if (match_glob(key, "window.inactive.button.menu.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_WINDOW_MENU]); parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_WINDOW_ICON]); } if (match_glob(key, "window.inactive.button.iconify.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_ICONIFY]); } if (match_glob(key, "window.inactive.button.max.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_MAXIMIZE]); } if (match_glob(key, "window.inactive.button.shade.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_SHADE]); } if (match_glob(key, "window.inactive.button.desk.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_OMNIPRESENT]); } if (match_glob(key, "window.inactive.button.close.unpressed.image.color")) { parse_color(value, theme->window[THEME_INACTIVE] .button_colors[LAB_SSD_BUTTON_CLOSE]); } /* window drop-shadows */ if (match_glob(key, "window.active.shadow.size")) { theme->window[THEME_ACTIVE].shadow_size = get_int_if_positive( value, "window.active.shadow.size"); } if (match_glob(key, "window.inactive.shadow.size")) { theme->window[THEME_INACTIVE].shadow_size = get_int_if_positive( value, "window.inactive.shadow.size"); } if (match_glob(key, "window.active.shadow.color")) { parse_color(value, theme->window[THEME_ACTIVE].shadow_color); } if (match_glob(key, "window.inactive.shadow.color")) { parse_color(value, theme->window[THEME_INACTIVE].shadow_color); } if (match_glob(key, "menu.overlap.x")) { theme->menu_overlap_x = atoi(value); } if (match_glob(key, "menu.overlap.y")) { theme->menu_overlap_y = atoi(value); } 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.border.width")) { theme->menu_border_width = get_int_if_positive( value, "menu.border.width"); } if (match_glob(key, "menu.border.color")) { parse_color(value, theme->menu_border_color); } if (match_glob(key, "menu.items.padding.x")) { theme->menu_items_padding_x = get_int_if_positive( value, "menu.items.padding.x"); } if (match_glob(key, "menu.items.padding.y")) { theme->menu_items_padding_y = get_int_if_positive( value, "menu.items.padding.y"); } if (match_glob(key, "menu.items.bg.color")) { parse_color(value, theme->menu_items_bg_color); } if (match_glob(key, "menu.items.text.color")) { parse_color(value, theme->menu_items_text_color); } if (match_glob(key, "menu.items.active.bg.color")) { parse_color(value, theme->menu_items_active_bg_color); } if (match_glob(key, "menu.items.active.text.color")) { parse_color(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_color(value, theme->menu_separator_color); } if (match_glob(key, "menu.title.bg.color")) { parse_color(value, theme->menu_title_bg_color); } if (match_glob(key, "menu.title.text.justify")) { theme->menu_title_text_justify = parse_justification(value); } if (match_glob(key, "menu.title.text.color")) { parse_color(value, theme->menu_title_text_color); } if (match_glob(key, "osd.bg.color")) { parse_color(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_color(value, theme->osd_border_color); } /* classic window switcher */ if (match_glob(key, "osd.window-switcher.style-classic.width") || match_glob(key, "osd.window-switcher.width")) { if (strrchr(value, '%')) { switcher_classic_theme->width_is_percent = true; } else { switcher_classic_theme->width_is_percent = false; } switcher_classic_theme->width = get_int_if_positive(value, "osd.window-switcher.style-classic.width"); } if (match_glob(key, "osd.window-switcher.style-classic.padding") || match_glob(key, "osd.window-switcher.padding")) { switcher_classic_theme->padding = get_int_if_positive(value, "osd.window-switcher.style-classic.padding"); } if (match_glob(key, "osd.window-switcher.style-classic.item.padding.x") || match_glob(key, "osd.window-switcher.item.padding.x")) { switcher_classic_theme->item_padding_x = get_int_if_positive(value, "osd.window-switcher.style-classic.item.padding.x"); } if (match_glob(key, "osd.window-switcher.style-classic.item.padding.y") || match_glob(key, "osd.window-switcher.item.padding.y")) { switcher_classic_theme->item_padding_y = get_int_if_positive(value, "osd.window-switcher.style-classic.item.padding.y"); } if (match_glob(key, "osd.window-switcher.style-classic.item.active.border.width") || match_glob(key, "osd.window-switcher.item.active.border.width")) { switcher_classic_theme->item_active_border_width = get_int_if_positive(value, "osd.window-switcher.style-classic.item.active.border.width"); } if (match_glob(key, "osd.window-switcher.style-classic.item.icon.size") || match_glob(key, "osd.window-switcher.item.icon.size")) { switcher_classic_theme->item_icon_size = get_int_if_positive(value, "osd.window-switcher.style-classic.item.icon.size"); } 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.workspace-switcher.boxes.border.width")) { theme->osd_workspace_switcher_boxes_border_width = get_int_if_positive( value, "osd.workspace-switcher.boxes.border.width"); } if (match_glob(key, "osd.label.text.color")) { parse_color(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_color(value, theme->snapping_overlay_region.bg_color); } if (match_glob(key, "snapping.overlay.edge.bg.color")) { parse_color(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_color(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) { 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_surface_t *surf = buffer->surface; cairo_t *cairo = cairo_create(surf); /* 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 ROUNDED_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 ROUNDED_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; } cairo_close_path(cairo); cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE); /* * We need to offset the fill pattern vertically by the border * width to line up with the rest of the titlebar. This is done * by applying a transformation matrix to the pattern temporarily. * It would be better to copy the pattern, but cairo does not * provide a simple way to this. */ cairo_matrix_t matrix; cairo_matrix_init_translate(&matrix, 0, -ctx->line_width); cairo_pattern_set_matrix(ctx->fill_pattern, &matrix); cairo_set_source(cairo, ctx->fill_pattern); cairo_fill_preserve(cairo); cairo_stroke(cairo); /* Reset the fill pattern transformation matrix afterward */ cairo_matrix_init_identity(&matrix); cairo_pattern_set_matrix(ctx->fill_pattern, &matrix); /* * 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 ROUNDED_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 ROUNDED_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; } 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 ROUNDED_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 ROUNDED_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; } cairo_stroke(cairo); out: cairo_surface_flush(surf); cairo_destroy(cairo); return buffer; } static void add_color_stop_rgba_premult(cairo_pattern_t *pattern, float offset, const float c[4]) { float alpha = c[3]; if (alpha == 0.0f) { cairo_pattern_add_color_stop_rgba(pattern, offset, 0, 0, 0, 0); } else { cairo_pattern_add_color_stop_rgba(pattern, offset, c[0] / alpha, c[1] / alpha, c[2] / alpha, alpha); } } static cairo_pattern_t * create_titlebar_pattern(const struct theme_background *bg, int height) { cairo_pattern_t *pattern; switch (bg->gradient) { case LAB_GRADIENT_VERTICAL: pattern = cairo_pattern_create_linear(0, 0, 0, height); add_color_stop_rgba_premult(pattern, 0, bg->color); add_color_stop_rgba_premult(pattern, 1, bg->color_to); break; case LAB_GRADIENT_SPLITVERTICAL: pattern = cairo_pattern_create_linear(0, 0, 0, height); add_color_stop_rgba_premult(pattern, 0, bg->color_split_to); add_color_stop_rgba_premult(pattern, 0.5, bg->color); add_color_stop_rgba_premult(pattern, 0.5, bg->color_to); add_color_stop_rgba_premult(pattern, 1, bg->color_to_split_to); break; case LAB_GRADIENT_NONE: default: pattern = color_to_pattern(bg->color); break; } return pattern; } static struct lab_data_buffer * create_titlebar_fill(cairo_pattern_t *pattern, int height) { /* create 1px wide buffer to be stretched horizontally */ struct lab_data_buffer *fill = buffer_create_cairo(1, height, 1); cairo_t *cairo = cairo_create(fill->surface); cairo_set_source(cairo, pattern); cairo_paint(cairo); cairo_surface_flush(fill->surface); cairo_destroy(cairo); return fill; } static void create_backgrounds(struct theme *theme) { for (int active = THEME_INACTIVE; active <= THEME_ACTIVE; active++) { theme->window[active].titlebar_pattern = create_titlebar_pattern( &theme->window[active].title_bg, theme->titlebar_height); theme->window[active].titlebar_fill = create_titlebar_fill( theme->window[active].titlebar_pattern, theme->titlebar_height); } } 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->titlebar_height + theme->border_width, }; for (int active = THEME_INACTIVE; active <= THEME_ACTIVE; active++) { struct rounded_corner_ctx ctx = { .box = &box, .radius = rc.corner_radius, .line_width = theme->border_width, .fill_pattern = theme->window[active].titlebar_pattern, .border_color = theme->window[active].border_color, .corner = ROUNDED_CORNER_TOP_LEFT, }; theme->window[active].corner_top_left_normal = rounded_rect(&ctx); ctx.corner = ROUNDED_CORNER_TOP_RIGHT; theme->window[active].corner_top_right_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; /* Gaussian 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_shadow(struct theme *theme, int active) { /* Size of shadow visible extending beyond the window */ int visible_size = theme->window[active].shadow_size; /* How far inside the window the shadow inset begins */ int inset = (double)visible_size * SSD_SHADOW_INSET; /* Total width including visible and obscured portion */ int total_size = visible_size + inset; /* * 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_size > 0) { theme->window[active].shadow_edge = buffer_create_cairo( visible_size, 1, 1.0); theme->window[active].shadow_corner_top = buffer_create_cairo( total_size, total_size, 1.0); theme->window[active].shadow_corner_bottom = buffer_create_cairo( total_size, total_size, 1.0); if (!theme->window[active].shadow_corner_top || !theme->window[active].shadow_corner_bottom || !theme->window[active].shadow_edge) { wlr_log(WLR_ERROR, "Failed to allocate shadow buffer"); return; } } shadow_edge_gradient(theme->window[active].shadow_edge, visible_size, total_size, theme->window[active].shadow_color); shadow_corner_gradient(theme->window[active].shadow_corner_top, visible_size, total_size, theme->titlebar_height, theme->window[active].shadow_color); shadow_corner_gradient(theme->window[active].shadow_corner_bottom, visible_size, total_size, 0, theme->window[active].shadow_color); } static void create_shadows(struct theme *theme) { create_shadow(theme, THEME_INACTIVE); create_shadow(theme, THEME_ACTIVE); } static void copy_color_scaled(float dest[4], const float src[4], float scale) { /* RGB values are premultiplied so must not exceed alpha */ dest[0] = fminf(src[0] * scale, src[3]); dest[1] = fminf(src[1] * scale, src[3]); dest[2] = fminf(src[2] * scale, src[3]); dest[3] = src[3]; /* alpha */ } static void fill_background_colors(struct theme_background *bg) { /* color.splitTo is color * 5/4, per Openbox theme spec */ if (bg->color_split_to[0] == FLT_MIN) { copy_color_scaled(bg->color_split_to, bg->color, 1.25f); } /* colorTo has no default in Openbox; just re-use "color" */ if (bg->color_to[0] == FLT_MIN) { memcpy(bg->color_to, bg->color, sizeof(bg->color_to)); } /* colorTo.splitTo is colorTo * 17/16, per Openbox theme spec */ if (bg->color_to_split_to[0] == FLT_MIN) { copy_color_scaled(bg->color_to_split_to, bg->color_to, 1.0625f); } } 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) { struct window_switcher_classic_theme *switcher_classic_theme = &theme->osd_window_switcher_classic; theme->titlebar_height = get_titlebar_height(theme); fill_background_colors(&theme->window[THEME_INACTIVE].title_bg); fill_background_colors(&theme->window[THEME_ACTIVE].title_bg); theme->menu_item_height = font_height(&rc.font_menuitem) + 2 * theme->menu_items_padding_y; theme->menu_header_height = font_height(&rc.font_menuheader) + 2 * theme->menu_items_padding_y; int osd_font_height = font_height(&rc.font_osd); if (switcher_classic_theme->item_icon_size <= 0) { switcher_classic_theme->item_icon_size = osd_font_height; } int osd_field_height = MAX(osd_font_height, switcher_classic_theme->item_icon_size); switcher_classic_theme->item_height = osd_field_height + 2 * switcher_classic_theme->item_padding_y + 2 * switcher_classic_theme->item_active_border_width; if (rc.corner_radius >= theme->titlebar_height) { rc.corner_radius = theme->titlebar_height - 1; } if (rc.resize_corner_range < 0) { rc.resize_corner_range = theme->titlebar_height / 2; } 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; } if (theme->menu_border_width == INT_MIN) { theme->menu_border_width = theme->border_width; } if (theme->menu_border_color[0] == FLT_MIN) { memcpy(theme->menu_border_color, theme->window[THEME_ACTIVE].border_color, sizeof(theme->menu_border_color)); } /* Inherit OSD settings if not set */ if (theme->osd_bg_color[0] == FLT_MIN) { memcpy(theme->osd_bg_color, theme->window[THEME_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[THEME_ACTIVE].label_text_color, sizeof(theme->osd_label_text_color)); } if (theme->osd_border_color[0] == FLT_MIN) { /* * As per https://openbox.org/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 (switcher_classic_theme->width_is_percent) { switcher_classic_theme->width = MIN(switcher_classic_theme->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); struct wl_list paths; if (theme_name) { /* * Read * - /share/themes/$theme_name/labwc/themerc * - /share/themes/$theme_name/openbox-3/themerc */ 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_backgrounds(theme); create_corners(theme); load_buttons(theme); create_shadows(theme); } static void destroy_img(struct lab_img **img) { lab_img_destroy(*img); *img = NULL; } 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 = LAB_BS_DEFAULT; state_set <= LAB_BS_ALL; state_set++) { destroy_img(&theme->window[THEME_INACTIVE] .button_imgs[type][state_set]); destroy_img(&theme->window[THEME_ACTIVE] .button_imgs[type][state_set]); } } for (int active = THEME_INACTIVE; active <= THEME_ACTIVE; active++) { zfree_pattern(theme->window[active].titlebar_pattern); zdrop(&theme->window[active].titlebar_fill); zdrop(&theme->window[active].corner_top_left_normal); zdrop(&theme->window[active].corner_top_right_normal); zdrop(&theme->window[active].shadow_corner_top); zdrop(&theme->window[active].shadow_corner_bottom); zdrop(&theme->window[active].shadow_edge); } }