labwc/src/theme.c
tokyo4j 9cd64d2a8f osd-thumbnail: update default colors of selected window item
Previously, the default values of
`osd.window-switcher.style-thumbnail.item.active.{bg,border}.color`
were blue. But they caused the selected window title in the window
switcher to be unreadable due to duplicated colors of the text and
background with Openbox themes like Numix.

Instead, this commit updates them to follow other themes configurations.
The default border color of the selected window item is now
`osd.label.text.color` with 50% opacity and the background is
`osd.label.text.color` with 15% opacity.

For subpixel antialiasing to work, the background color is calculated by
manually blending `osd.label.text.color` and `osd.bg.color`, rather than
just updating the alpha with 50% or 15%.
2025-10-08 00:40:56 +09:00

1857 lines
60 KiB
C

// 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 <assert.h>
#include <cairo.h>
#include <drm_fourcc.h>
#include <glib.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wlr/util/box.h>
#include <wlr/util/log.h>
#include <wlr/render/pixman.h>
#include <strings.h>
#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 lab_node_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_HOVERED)) {
struct lab_img *non_hover_img =
button_imgs[b->type][b->state_set & ~LAB_BS_HOVERED];
*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];
if (rc.nr_title_buttons_left > 0
&& b->type == rc.title_buttons_left[0]) {
*rounded_img = lab_img_copy(*img);
lab_img_add_modifier(*rounded_img, round_left_corner_button);
}
if (rc.nr_title_buttons_right > 0
&& b->type == rc.title_buttons_right
[rc.nr_title_buttons_right - 1]) {
*rounded_img = lab_img_copy(*img);
lab_img_add_modifier(*rounded_img, round_right_corner_button);
}
}
/*
* 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_NODE_BUTTON_WINDOW_MENU,
.state_set = 0,
}, {
.name = "iconify",
.fallback_button = (const char[]){ 0x00, 0x00, 0x00, 0x00, 0x3f, 0x3f },
.type = LAB_NODE_BUTTON_ICONIFY,
.state_set = 0,
}, {
.name = "max",
.fallback_button = (const char[]){ 0x3f, 0x3f, 0x21, 0x21, 0x21, 0x3f },
.type = LAB_NODE_BUTTON_MAXIMIZE,
.state_set = 0,
}, {
.name = "max_toggled",
.fallback_button = (const char[]){ 0x3e, 0x22, 0x2f, 0x29, 0x39, 0x0f },
.type = LAB_NODE_BUTTON_MAXIMIZE,
.state_set = LAB_BS_TOGGLED,
}, {
.name = "shade",
.fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x0c, 0x1e, 0x3f },
.type = LAB_NODE_BUTTON_SHADE,
.state_set = 0,
}, {
.name = "shade_toggled",
.fallback_button = (const char[]){ 0x3f, 0x3f, 0x00, 0x3f, 0x1e, 0x0c },
.type = LAB_NODE_BUTTON_SHADE,
.state_set = LAB_BS_TOGGLED,
}, {
.name = "desk",
.fallback_button = (const char[]){ 0x33, 0x33, 0x00, 0x00, 0x33, 0x33 },
.type = LAB_NODE_BUTTON_OMNIPRESENT,
.state_set = 0,
}, {
.name = "desk_toggled",
.fallback_button = (const char[]){ 0x00, 0x1e, 0x1a, 0x16, 0x1e, 0x00 },
.type = LAB_NODE_BUTTON_OMNIPRESENT,
.state_set = LAB_BS_TOGGLED,
}, {
.name = "close",
.fallback_button = (const char[]){ 0x33, 0x3f, 0x1e, 0x1e, 0x3f, 0x33 },
.type = LAB_NODE_BUTTON_CLOSE,
.state_set = 0,
}, {
.name = "menu_hover",
.type = LAB_NODE_BUTTON_WINDOW_MENU,
.state_set = LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "iconify_hover",
.type = LAB_NODE_BUTTON_ICONIFY,
.state_set = LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "max_hover",
.type = LAB_NODE_BUTTON_MAXIMIZE,
.state_set = LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "max_toggled_hover",
.alt_name = "max_hover_toggled",
.type = LAB_NODE_BUTTON_MAXIMIZE,
.state_set = LAB_BS_TOGGLED | LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "shade_hover",
.type = LAB_NODE_BUTTON_SHADE,
.state_set = LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "shade_toggled_hover",
.alt_name = "shade_hover_toggled",
.type = LAB_NODE_BUTTON_SHADE,
.state_set = LAB_BS_TOGGLED | LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "desk_hover",
/* no fallback (non-hover variant is used instead) */
.type = LAB_NODE_BUTTON_OMNIPRESENT,
.state_set = LAB_BS_HOVERED,
}, {
.name = "desk_toggled_hover",
.alt_name = "desk_hover_toggled",
.type = LAB_NODE_BUTTON_OMNIPRESENT,
.state_set = LAB_BS_TOGGLED | LAB_BS_HOVERED,
/* no fallback (non-hover variant is used instead) */
}, {
.name = "close_hover",
.type = LAB_NODE_BUTTON_CLOSE,
.state_set = LAB_BS_HOVERED,
/* 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 lab_node_type type = LAB_NODE_BUTTON_FIRST;
type <= LAB_NODE_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;
theme->osd_window_switcher_thumbnail.max_width = 80;
theme->osd_window_switcher_thumbnail.max_width_is_percent = true;
theme->osd_window_switcher_thumbnail.padding = 4;
theme->osd_window_switcher_thumbnail.item_width = 300;
theme->osd_window_switcher_thumbnail.item_height = 250;
theme->osd_window_switcher_thumbnail.item_padding = 10;
theme->osd_window_switcher_thumbnail.item_active_border_width = 2;
theme->osd_window_switcher_thumbnail.item_active_border_color[0] = FLT_MIN;
theme->osd_window_switcher_thumbnail.item_active_bg_color[0] = FLT_MIN;
theme->osd_window_switcher_thumbnail.item_icon_size = 60;
/* 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;
struct window_switcher_thumbnail_theme *switcher_thumb_theme =
&theme->osd_window_switcher_thumbnail;
/*
* 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 lab_node_type type = LAB_NODE_BUTTON_FIRST;
type <= LAB_NODE_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 lab_node_type type = LAB_NODE_BUTTON_FIRST;
type <= LAB_NODE_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_NODE_BUTTON_WINDOW_MENU]);
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_WINDOW_ICON]);
}
if (match_glob(key, "window.active.button.iconify.unpressed.image.color")) {
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_ICONIFY]);
}
if (match_glob(key, "window.active.button.max.unpressed.image.color")) {
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_MAXIMIZE]);
}
if (match_glob(key, "window.active.button.shade.unpressed.image.color")) {
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_SHADE]);
}
if (match_glob(key, "window.active.button.desk.unpressed.image.color")) {
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_OMNIPRESENT]);
}
if (match_glob(key, "window.active.button.close.unpressed.image.color")) {
parse_color(value, theme->window[THEME_ACTIVE]
.button_colors[LAB_NODE_BUTTON_CLOSE]);
}
if (match_glob(key, "window.inactive.button.menu.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_WINDOW_MENU]);
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_WINDOW_ICON]);
}
if (match_glob(key, "window.inactive.button.iconify.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_ICONIFY]);
}
if (match_glob(key, "window.inactive.button.max.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_MAXIMIZE]);
}
if (match_glob(key, "window.inactive.button.shade.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_SHADE]);
}
if (match_glob(key, "window.inactive.button.desk.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_BUTTON_OMNIPRESENT]);
}
if (match_glob(key, "window.inactive.button.close.unpressed.image.color")) {
parse_color(value, theme->window[THEME_INACTIVE]
.button_colors[LAB_NODE_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");
}
/* thumbnail window switcher */
if (match_glob(key, "osd.window-switcher.style-thumbnail.width.max")) {
if (strrchr(value, '%')) {
switcher_thumb_theme->max_width_is_percent = true;
} else {
switcher_thumb_theme->max_width_is_percent = false;
}
switcher_thumb_theme->max_width = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.width.max");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.padding")) {
switcher_thumb_theme->padding = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.padding");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.width")) {
switcher_thumb_theme->item_width = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.item.width");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.height")) {
switcher_thumb_theme->item_height = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.item.height");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.padding")) {
switcher_thumb_theme->item_padding = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.item.padding");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.active.border.width")) {
switcher_thumb_theme->item_active_border_width = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.item.active.border.width");
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.active.border.color")) {
parse_color(value, switcher_thumb_theme->item_active_border_color);
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.active.bg.color")) {
parse_color(value, switcher_thumb_theme->item_active_bg_color);
}
if (match_glob(key, "osd.window-switcher.style-thumbnail.item.icon.size")) {
switcher_thumb_theme->item_icon_size = get_int_if_positive(
value, "osd.window-switcher.style-thumbnail.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;
}
/* Blend foreground color (with new alpha) with background color */
static void
blend_color_with_bg(float *dst, float *fg, float fg_a, float *bg)
{
/* Guard against zero division */
if (fg[3] <= 0.0f) {
memset(dst, 0, sizeof(float) * 4);
return;
}
/* Redo premultiplication to update foreground alpha */
float new_fg[4] = {
fg[0] / fg[3] * fg_a,
fg[1] / fg[3] * fg_a,
fg[2] / fg[3] * fg_a,
fg_a,
};
/* Blend colors */
dst[0] = new_fg[0] + bg[0] * (1.0f - new_fg[3]);
dst[1] = new_fg[1] + bg[1] * (1.0f - new_fg[3]);
dst[2] = new_fg[2] + bg[2] * (1.0f - new_fg[3]);
dst[3] = new_fg[3] + bg[3] * (1.0f - new_fg[3]);
}
static void
post_processing(struct theme *theme)
{
struct window_switcher_classic_theme *switcher_classic_theme =
&theme->osd_window_switcher_classic;
struct window_switcher_thumbnail_theme *switcher_thumb_theme =
&theme->osd_window_switcher_thumbnail;
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);
switcher_thumb_theme->title_height = osd_font_height;
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 (switcher_thumb_theme->item_active_border_color[0] == FLT_MIN) {
blend_color_with_bg(switcher_thumb_theme->item_active_border_color,
theme->osd_label_text_color, 0.50, theme->osd_bg_color);
}
if (switcher_thumb_theme->item_active_bg_color[0] == FLT_MIN) {
blend_color_with_bg(switcher_thumb_theme->item_active_bg_color,
theme->osd_label_text_color, 0.15, theme->osd_bg_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
* - <data-dir>/share/themes/$theme_name/labwc/themerc
* - <data-dir>/share/themes/$theme_name/openbox-3/themerc
*/
paths_theme_create(&paths, theme_name, "themerc");
theme_read(theme, &paths);
paths_destroy(&paths);
}
/* Read <config-dir>/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 lab_node_type type = LAB_NODE_BUTTON_FIRST;
type <= LAB_NODE_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);
}
}