diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index 2677e348..7b0b822b 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -216,13 +216,14 @@ Actions are used in menus and keyboard/mouse bindings. ** Toggle decorations of focused window. - This is a 3-state action which can be executed multiple times: - - Only the titlebar will be hidden, borders and resize area are kept + This is a 4-state action which can be executed multiple times: + - Titlebar is hidden; borders and handle/grip remain visible + - Handle/grip is also hidden; only borders remain - Remaining decorations will be disabled - Decorations will be shown normally - By disabling the theme configuration 'keepBorder' the first step - will be removed and the action only toggles between on and off. + By disabling the theme configuration 'keepBorder' the cycle reduces + to toggling between full decorations and no decorations. ** Toggle fullscreen state of focused window. @@ -526,9 +527,10 @@ Actions that execute other actions. Used in keyboard/mouse bindings. Whether the client is tiled (snapped) to the indicated region. The indicated region may be a glob. - *decoration* [full|border|none] + *decoration* [full|border-handle|border|none] Whether the client has full server-side decorations, - borders only, or no server-side decorations. + borders with handle/grip, borders only, or no + server-side decorations. *monitor* [current|left|right|] Whether the client is on a monitor relative to the to diff --git a/docs/labwc-theme.5.scd b/docs/labwc-theme.5.scd index dce48bdf..53694c52 100644 --- a/docs/labwc-theme.5.scd +++ b/docs/labwc-theme.5.scd @@ -113,6 +113,33 @@ window.*.title.bg.colorTo.splitTo: #557485 Line width (integer) of border drawn around window frames. Default is 1. +*window.handle.width* + Height (integer) of the bottom handle bar in pixels. Despite the name, + this controls the vertical size of the handle (Openbox naming + convention). When set to 0, the handle/grip assembly is disabled and + the standard bottom border is used instead. Default is 6. + +*window.grip.width* + Width (integer) of each corner grip in the bottom handle bar, in + pixels. Default is 20. + +*window.active.handle.bg* / *window.inactive.handle.bg* + Texture for the handle background. Supports Solid, Gradient Vertical, + and Gradient SplitVertical. Default is *Solid* with color #a0a0a0 + (active) / #c0c0c0 (inactive). + +*window.active.handle.bg.color* (and .colorTo, .color.splitTo, .colorTo.splitTo) + Colors for the handle background gradient stops. See the texture + documentation above for details. + +*window.active.grip.bg* / *window.inactive.grip.bg* + Texture for the left and right grip backgrounds. If not set, the grip + inherits from the handle background. + +*window.active.grip.bg.color* (and .colorTo, .color.splitTo, .colorTo.splitTo) + Colors for the grip background gradient stops. If not set, the grip + inherits from the handle background colors. + *window.titlebar.padding.width* Horizontal titlebar padding size, in pixels, between border and first button on the left/right. @@ -561,7 +588,19 @@ following icons should be added: # DEFINITIONS -The handle is the window edge decoration at the bottom of the window. +The handle is the window edge decoration at the bottom of the window. It +consists of a center handle bar (for vertical resizing) and two grips at the +left and right corners (for diagonal resizing). The handle/grip assembly +replaces the standard bottom border when enabled (window.handle.width > 0) +and the current decoration state includes the handle. Vertical separator +lines of border.width thickness are drawn between the grips and the center +handle using the window border color. + +When hovering over a grip element, it dims by 20%. When clicked (pressed +but not yet moved), it dims by 40% with an inset shadow effect giving a +"pushed in" appearance. During drag-resize, the dimming reduces to 20% +while maintaining the inset shadow. The center handle does not receive +these visual effects. # SEE ALSO diff --git a/docs/themerc b/docs/themerc index 83ea726b..58bf0a88 100644 --- a/docs/themerc +++ b/docs/themerc @@ -58,6 +58,19 @@ window.inactive.shadow.size: 40 window.active.shadow.color: #00000060 window.inactive.shadow.color: #00000040 +# window bottom handle and grips +window.handle.width: 6 +window.grip.width: 20 +window.active.handle.bg: Solid +window.active.handle.bg.color: #a0a0a0 +window.inactive.handle.bg: Solid +window.inactive.handle.bg.color: #c0c0c0 +# Grips inherit from handle if not set explicitly +# window.active.grip.bg: Solid +# window.active.grip.bg.color: #a0a0a0 +# window.inactive.grip.bg: Solid +# window.inactive.grip.bg.color: #c0c0c0 + # Note that "menu", "iconify", "max", "close" buttons colors can be defined # individually by inserting the type after the button node, for example: # diff --git a/include/common/node-type.h b/include/common/node-type.h index 52fff3b0..a2a2519a 100644 --- a/include/common/node-type.h +++ b/include/common/node-type.h @@ -43,6 +43,10 @@ enum lab_node_type { LAB_NODE_BORDER_LEFT, LAB_NODE_BORDER, + LAB_NODE_HANDLE, + LAB_NODE_GRIP_LEFT, + LAB_NODE_GRIP_RIGHT, + LAB_NODE_CLIENT, LAB_NODE_FRAME, LAB_NODE_ROOT, diff --git a/include/config/types.h b/include/config/types.h index 3b1fa5e8..827f78bc 100644 --- a/include/config/types.h +++ b/include/config/types.h @@ -38,6 +38,7 @@ enum lab_rotation { enum lab_ssd_mode { LAB_SSD_MODE_NONE = 0, LAB_SSD_MODE_BORDER, + LAB_SSD_MODE_BORDER_HANDLE, LAB_SSD_MODE_FULL, LAB_SSD_MODE_INVALID, }; diff --git a/include/labwc.h b/include/labwc.h index 87a42198..0da2a55f 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -236,6 +236,10 @@ struct server { struct ssd_button *hovered_button; + /* Track which handle/grip element is hovered (for cross-view cleanup) */ + struct ssd *hovered_handle_ssd; + int hovered_handle_element; /* -1 = none */ + /* Tree for all non-layer xdg/xwayland-shell surfaces */ struct wlr_scene_tree *workspace_tree; diff --git a/include/ssd-internal.h b/include/ssd-internal.h index 600b9076..9a026273 100644 --- a/include/ssd-internal.h +++ b/include/ssd-internal.h @@ -12,6 +12,35 @@ struct ssd_state_title_width { bool truncated; }; +/* Interaction state for handle and grip elements */ +enum ssd_handle_state { + SSD_HANDLE_STATE_NORMAL = 0, + SSD_HANDLE_STATE_HOVER, + SSD_HANDLE_STATE_PRESSED, /* clicked but not yet moved */ + SSD_HANDLE_STATE_DRAGGING, /* clicked and moved */ +}; + +/* Indices into ssd_handle_scene.element_states[] */ +#define SSD_HANDLE_ELEMENT_GRIP_LEFT 0 +#define SSD_HANDLE_ELEMENT_CENTER 1 +#define SSD_HANDLE_ELEMENT_GRIP_RIGHT 2 +#define SSD_HANDLE_ELEMENT_COUNT 3 + +/* Indices into ssd_handle_subtree.rects[] for border/separator rects */ +enum handle_rect_idx { + HRECT_BORDER_LEFT, + HRECT_BORDER_RIGHT, + HRECT_BORDER_BOTTOM_LEFT, + HRECT_BORDER_BOTTOM_CENTER, + HRECT_BORDER_BOTTOM_RIGHT, + HRECT_BORDER_TOP_LEFT, + HRECT_BORDER_TOP_CENTER, + HRECT_BORDER_TOP_RIGHT, + HRECT_SEPARATOR_LEFT, + HRECT_SEPARATOR_RIGHT, + HRECT_COUNT, +}; + /* * The scene-graph of SSD looks like below. The parentheses indicate the * type of each node (enum lab_node_type, stored in the node_descriptor @@ -50,6 +79,14 @@ struct ssd_state_title_width { * +--extents * +--top * +--... + * +--handle (optional, when handle_width > 0) + * +--inactive + * | +--textures[3] (grip_left, handle, grip_right wlr_scene_buffers) + * | +--rects[HRECT_COUNT] (borders and separators, all border_color) + * | +--overlay[3] (per-element hover/pressed dimming) + * | +--inset shadow rects (per-element top, left, bottom, right) + * +--active + * +--... */ struct ssd { struct view *view; @@ -127,6 +164,27 @@ struct ssd { } subtrees[2]; /* indexed by enum ssd_active_state */ } shadow; + /* Bottom handle/grip assembly (optional, created when handle_width > 0) */ + struct ssd_handle_scene { + struct wlr_scene_tree *tree; + struct ssd_handle_subtree { + struct wlr_scene_tree *tree; + /* Per-element texture buffers (1px wide, stretched) */ + struct wlr_scene_buffer *textures[SSD_HANDLE_ELEMENT_COUNT]; + /* Border and separator rects (all border_color) */ + struct wlr_scene_rect *rects[HRECT_COUNT]; + /* Per-element overlays for hover/pressed dimming */ + struct wlr_scene_rect *overlay[SSD_HANDLE_ELEMENT_COUNT]; + /* Inset shadow rects (per-element, for pressed/dragging) */ + struct wlr_scene_rect *inset_top[SSD_HANDLE_ELEMENT_COUNT]; + struct wlr_scene_rect *inset_left[SSD_HANDLE_ELEMENT_COUNT]; + struct wlr_scene_rect *inset_bottom[SSD_HANDLE_ELEMENT_COUNT]; + struct wlr_scene_rect *inset_right[SSD_HANDLE_ELEMENT_COUNT]; + } subtrees[2]; /* indexed by enum ssd_active_state */ + /* Interaction state per element */ + enum ssd_handle_state element_states[SSD_HANDLE_ELEMENT_COUNT]; + } handle; + /* * Space between the extremities of the view's wlr_surface * and the max extents of the server-side decorations. @@ -186,4 +244,11 @@ void ssd_shadow_create(struct ssd *ssd); void ssd_shadow_update(struct ssd *ssd); void ssd_shadow_destroy(struct ssd *ssd); +void ssd_handle_create(struct ssd *ssd); +void ssd_handle_update(struct ssd *ssd); +void ssd_handle_destroy(struct ssd *ssd); +void ssd_handle_set_element_state(struct ssd *ssd, int element, + enum ssd_handle_state state); +void ssd_handle_clear_all_states(struct ssd *ssd); + #endif /* LABWC_SSD_INTERNAL_H */ diff --git a/include/ssd.h b/include/ssd.h index 9c617474..650a4917 100644 --- a/include/ssd.h +++ b/include/ssd.h @@ -50,8 +50,10 @@ void ssd_set_titlebar(struct ssd *ssd, bool enabled); void ssd_enable_keybind_inhibit_indicator(struct ssd *ssd, bool enable); void ssd_enable_shade(struct ssd *ssd, bool enable); +void ssd_set_handle(struct ssd *ssd, bool enabled); void ssd_update_hovered_button(struct wlr_scene_node *node); +void ssd_update_hovered_handle(struct wlr_scene_node *node); void ssd_button_free(struct ssd_button *button); diff --git a/include/theme.h b/include/theme.h index 2fb499a9..6f771715 100644 --- a/include/theme.h +++ b/include/theme.h @@ -61,6 +61,16 @@ struct theme_background { struct theme { int border_width; + /* + * Height of the bottom handle bar (Openbox calls this + * "window.handle.width" but it is the vertical size). + * Set to 0 to disable the handle/grip assembly. + */ + int handle_width; + + /* Width of each corner grip in the handle bar */ + int grip_width; + /* * the space between title bar border and * buttons on the left/right/top @@ -130,6 +140,16 @@ struct theme { struct lab_data_buffer *shadow_corner_top; struct lab_data_buffer *shadow_corner_bottom; struct lab_data_buffer *shadow_edge; + + /* handle and grip backgrounds (Openbox-compatible) */ + struct theme_background handle_bg; + struct theme_background grip_bg; + + /* Pre-rendered handle/grip fill buffers and patterns */ + cairo_pattern_t *handle_pattern; + cairo_pattern_t *grip_pattern; + struct lab_data_buffer *handle_fill; + struct lab_data_buffer *grip_fill; } window[2]; /* Derived from font sizes */ diff --git a/include/view.h b/include/view.h index 46943ab2..2c6da746 100644 --- a/include/view.h +++ b/include/view.h @@ -569,6 +569,7 @@ bool view_is_tiled_and_notify_tiled(struct view *view); bool view_is_floating(struct view *view); void view_move_to_workspace(struct view *view, struct workspace *workspace); bool view_titlebar_visible(struct view *view); +bool view_handle_visible(struct view *view); void view_set_ssd_mode(struct view *view, enum lab_ssd_mode mode); void view_set_decorations(struct view *view, enum lab_ssd_mode mode, bool force_ssd); void view_toggle_fullscreen(struct view *view); diff --git a/src/common/node-type.c b/src/common/node-type.c index 67958dd4..d619083f 100644 --- a/src/common/node-type.c +++ b/src/common/node-type.c @@ -42,6 +42,12 @@ node_type_parse(const char *context) return LAB_NODE_BORDER_BOTTOM; } else if (!strcasecmp(context, "Left")) { return LAB_NODE_BORDER_LEFT; + } else if (!strcasecmp(context, "Handle")) { + return LAB_NODE_HANDLE; + } else if (!strcasecmp(context, "BLGrip")) { + return LAB_NODE_GRIP_LEFT; + } else if (!strcasecmp(context, "BRGrip")) { + return LAB_NODE_GRIP_RIGHT; } else if (!strcasecmp(context, "Frame")) { return LAB_NODE_FRAME; } else if (!strcasecmp(context, "Client")) { @@ -80,9 +86,16 @@ node_type_contains(enum lab_node_type whole, enum lab_node_type part) return part >= LAB_NODE_BUTTON_FIRST && part <= LAB_NODE_CLIENT; } + if (whole == LAB_NODE_HANDLE) { + return part == LAB_NODE_GRIP_LEFT + || part == LAB_NODE_GRIP_RIGHT; + } if (whole == LAB_NODE_BORDER) { - return part >= LAB_NODE_CORNER_TOP_LEFT - && part <= LAB_NODE_BORDER_LEFT; + return (part >= LAB_NODE_CORNER_TOP_LEFT + && part <= LAB_NODE_BORDER_LEFT) + || part == LAB_NODE_HANDLE + || part == LAB_NODE_GRIP_LEFT + || part == LAB_NODE_GRIP_RIGHT; } if (whole == LAB_NODE_BORDER_TOP) { return part == LAB_NODE_CORNER_TOP_LEFT @@ -94,7 +107,10 @@ node_type_contains(enum lab_node_type whole, enum lab_node_type part) } if (whole == LAB_NODE_BORDER_BOTTOM) { return part == LAB_NODE_CORNER_BOTTOM_RIGHT - || part == LAB_NODE_CORNER_BOTTOM_LEFT; + || part == LAB_NODE_CORNER_BOTTOM_LEFT + || part == LAB_NODE_HANDLE + || part == LAB_NODE_GRIP_LEFT + || part == LAB_NODE_GRIP_RIGHT; } if (whole == LAB_NODE_BORDER_LEFT) { return part == LAB_NODE_CORNER_TOP_LEFT @@ -123,6 +139,12 @@ node_type_to_edges(enum lab_node_type type) return LAB_EDGES_BOTTOM_RIGHT; case LAB_NODE_CORNER_BOTTOM_LEFT: return LAB_EDGES_BOTTOM_LEFT; + case LAB_NODE_HANDLE: + return LAB_EDGE_BOTTOM; + case LAB_NODE_GRIP_LEFT: + return LAB_EDGES_BOTTOM_LEFT; + case LAB_NODE_GRIP_RIGHT: + return LAB_EDGES_BOTTOM_RIGHT; default: return LAB_EDGE_NONE; } diff --git a/src/desktop.c b/src/desktop.c index 57ef9e3c..4f103db9 100644 --- a/src/desktop.c +++ b/src/desktop.c @@ -390,6 +390,19 @@ get_cursor_context(void) ret.node = node; ret.type = LAB_NODE_CYCLE_OSD_ITEM; return ret; + case LAB_NODE_HANDLE: + case LAB_NODE_GRIP_LEFT: + case LAB_NODE_GRIP_RIGHT: + /* + * Handle/grip nodes have precise types + * assigned via node_descriptor; use them + * directly without ssd_get_resizing_type(). + */ + ret.node = node; + ret.view = desc->view; + assert(ret.view); + ret.type = desc->type; + return ret; case LAB_NODE_BUTTON_FIRST...LAB_NODE_BUTTON_LAST: case LAB_NODE_SSD_ROOT: case LAB_NODE_TITLE: diff --git a/src/input/cursor.c b/src/input/cursor.c index e8ac7d96..16bc2921 100644 --- a/src/input/cursor.c +++ b/src/input/cursor.c @@ -35,6 +35,7 @@ #include "resistance.h" #include "resize-outlines.h" #include "ssd.h" +#include "ssd-internal.h" #include "view.h" #include "xwayland.h" @@ -315,6 +316,24 @@ process_cursor_resize(uint32_t time) static struct view *last_resize_view = NULL; assert(server.grabbed_view); + + /* + * Transition handle/grip from PRESSED to DRAGGING on + * first resize motion. + */ + struct ssd *ssd = server.grabbed_view->ssd; + if (ssd && ssd->handle.tree) { + for (int i = 0; i < SSD_HANDLE_ELEMENT_COUNT; i++) { + if (i == SSD_HANDLE_ELEMENT_CENTER) { + continue; + } + if (ssd->handle.element_states[i] + == SSD_HANDLE_STATE_PRESSED) { + ssd_handle_set_element_state(ssd, i, + SSD_HANDLE_STATE_DRAGGING); + } + } + } if (server.grabbed_view == last_resize_view) { int32_t refresh = 0; if (output_is_usable(last_resize_view->output)) { @@ -544,6 +563,7 @@ cursor_update_common(const struct cursor_context *ctx, struct wlr_seat *wlr_seat = seat->wlr_seat; ssd_update_hovered_button(ctx->node); + ssd_update_hovered_handle(ctx->node); if (server.input_mode != LAB_INPUT_STATE_PASSTHROUGH) { /* @@ -1150,6 +1170,28 @@ cursor_process_button_press(struct seat *seat, uint32_t button, uint32_t time_ms interactive_set_grab_context(&ctx); } + /* + * Set pressed visual state on grip elements (not center handle). + * This runs before action processing so the visual + * feedback appears immediately on click. + */ + if (ctx.view && ctx.view->ssd && ctx.view->ssd->handle.tree) { + switch (ctx.type) { + case LAB_NODE_GRIP_LEFT: + ssd_handle_set_element_state(ctx.view->ssd, + SSD_HANDLE_ELEMENT_GRIP_LEFT, + SSD_HANDLE_STATE_PRESSED); + break; + case LAB_NODE_GRIP_RIGHT: + ssd_handle_set_element_state(ctx.view->ssd, + SSD_HANDLE_ELEMENT_GRIP_RIGHT, + SSD_HANDLE_STATE_PRESSED); + break; + default: + break; + } + } + if (server.input_mode == LAB_INPUT_STATE_MENU) { /* * If menu was already opened on press, set a very small value @@ -1272,11 +1314,21 @@ cursor_finish_button_release(struct seat *seat, uint32_t button) if (resize_outlines_enabled(server.grabbed_view)) { resize_outlines_finish(server.grabbed_view); } + /* + * Clear any handle/grip pressed/dragging state + * when finishing interactive resize. + */ + ssd_handle_clear_all_states( + server.grabbed_view ? server.grabbed_view->ssd : NULL); /* Exit interactive move/resize mode */ interactive_finish(server.grabbed_view); return true; } else if (server.grabbed_view) { - /* Button was released without starting move/resize */ + /* + * Button was released without starting move/resize. + * Clear any handle/grip pressed state. + */ + ssd_handle_clear_all_states(server.grabbed_view->ssd); interactive_cancel(server.grabbed_view); } diff --git a/src/ssd/meson.build b/src/ssd/meson.build index a316409b..7c5685e8 100644 --- a/src/ssd/meson.build +++ b/src/ssd/meson.build @@ -5,5 +5,6 @@ labwc_sources += files( 'ssd-titlebar.c', 'ssd-border.c', 'ssd-extents.c', + 'ssd-handle.c', 'ssd-shadow.c', ) diff --git a/src/ssd/ssd-border.c b/src/ssd/ssd-border.c index c2c99f8d..670e7b78 100644 --- a/src/ssd/ssd-border.c +++ b/src/ssd/ssd-border.c @@ -59,6 +59,18 @@ ssd_border_create(struct ssd *ssd) wlr_scene_node_set_enabled(&ssd->border.tree->node, false); } + /* + * When the handle assembly is active, hide the bottom border + * because the handle provides its own bottom border visuals. + */ + if (ssd->handle.tree && ssd->handle.tree->node.enabled) { + FOR_EACH_ACTIVE_STATE(active) { + wlr_scene_node_set_enabled( + &ssd->border.subtrees[active].bottom->node, + false); + } + } + if (view->current.width > 0 && view->current.height > 0) { /* * The SSD is recreated by a Reconfigure request @@ -152,6 +164,15 @@ ssd_border_update(struct ssd *ssd) top_width, theme->border_width); wlr_scene_node_set_position(&subtree->top->node, top_x, -(ssd->titlebar.height + theme->border_width)); + + /* + * Hide the bottom border when the handle assembly + * is active (handle draws its own borders). + */ + bool handle_active = ssd->handle.tree + && ssd->handle.tree->node.enabled; + wlr_scene_node_set_enabled( + &subtree->bottom->node, !handle_active); } } diff --git a/src/ssd/ssd-handle.c b/src/ssd/ssd-handle.c new file mode 100644 index 00000000..f1156ff9 --- /dev/null +++ b/src/ssd/ssd-handle.c @@ -0,0 +1,607 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* + * SSD handle and grip assembly for the bottom of the window frame. + * + * Implements Openbox-compatible handle/grip decorations with interactive + * visual states (hover, pressed, dragging). + */ + +#include +#include +#include +#include "buffer.h" +#include "common/macros.h" +#include "common/scene-helpers.h" +#include "config/rcxml.h" +#include "labwc.h" +#include "node.h" +#include "ssd.h" +#include "ssd-internal.h" +#include "theme.h" +#include "view.h" + +/* Inset shadow line width for pressed/dragging state */ +#define INSET_LINE_WIDTH 1 + +/* Overlay colors (premultiplied RGBA) */ +static const float overlay_hover[4] = { 0.0f, 0.0f, 0.0f, 0.20f }; +static const float overlay_pressed[4] = { 0.0f, 0.0f, 0.0f, 0.40f }; +static const float overlay_none[4] = { 0.0f, 0.0f, 0.0f, 0.0f }; + +/* Inset shadow colors (premultiplied) */ +static const float inset_dark[4] = { 0.0f, 0.0f, 0.0f, 0.30f }; +static const float inset_light[4] = { 0.10f, 0.10f, 0.10f, 0.10f }; + +/* Map element index -> node type for hit detection */ +static const enum lab_node_type elem_node_types[SSD_HANDLE_ELEMENT_COUNT] = { + [SSD_HANDLE_ELEMENT_GRIP_LEFT] = LAB_NODE_GRIP_LEFT, + [SSD_HANDLE_ELEMENT_CENTER] = LAB_NODE_HANDLE, + [SSD_HANDLE_ELEMENT_GRIP_RIGHT] = LAB_NODE_GRIP_RIGHT, +}; + +/* Spec for a border/separator rect: geometry + node descriptor type */ +struct rect_spec { + int x, y, w, h; + enum lab_node_type desc_type; +}; + +/* Per-element geometry (shared by textures, overlays, insets) */ +struct elem_geometry { + int x, y, w, h; +}; + +/* + * Compute the layout metrics for the handle/grip assembly. + * All coordinates are relative to the handle tree origin, which is + * positioned at (-border_width, effective_height) relative to the + * SSD root. + */ +struct handle_layout { + int bw; /* border_width */ + int hw; /* handle_width (height of handle content) */ + int full_width; /* total width including side borders */ + int grip_w; /* width of each grip */ + int handle_w; /* width of center handle (between grips) */ + int content_h; /* height of textured content (= hw, per Openbox spec) */ + int total_h; /* total assembly height: 2*bw + hw */ + /* X positions (relative to handle tree) */ + int grip_left_x; + int handle_x; + int grip_right_x; + /* Y position of content (below top border line) */ + int content_y; +}; + +static struct handle_layout +compute_layout(struct theme *theme, int view_width) +{ + struct handle_layout l; + l.bw = theme->border_width; + l.hw = theme->handle_width; + l.full_width = view_width + 2 * l.bw; + l.grip_w = theme->grip_width; + + /* Ensure grips don't overflow the available width */ + if (2 * l.grip_w + 4 * l.bw > l.full_width) { + l.grip_w = MAX((l.full_width - 4 * l.bw) / 2, 0); + } + + /* + * Center handle width: total minus outer borders (2*bw), + * grips (2*grip_w), and inner separators (2*bw). + */ + l.handle_w = MAX(l.full_width - 4 * l.bw - 2 * l.grip_w, 0); + + /* X coordinates accounting for separators between grips and handle */ + l.grip_left_x = l.bw; + l.handle_x = l.bw + l.grip_w + l.bw; + l.grip_right_x = l.bw + l.grip_w + l.bw + l.handle_w + l.bw; + + /* Content Y starts after top border line */ + l.content_y = l.bw; + + /* + * Per Openbox spec, handle_width is the content-only height. + * Borders are drawn around it, not subtracted from it. + */ + l.content_h = l.hw; + l.total_h = 2 * l.bw + l.hw; + + return l; +} + +static void +compute_rect_specs(const struct handle_layout *l, + struct rect_spec specs[HRECT_COUNT]) +{ + specs[HRECT_BORDER_LEFT] = (struct rect_spec){ + 0, 0, l->bw, l->total_h, LAB_NODE_GRIP_LEFT }; + specs[HRECT_BORDER_RIGHT] = (struct rect_spec){ + l->full_width - l->bw, 0, l->bw, l->total_h, + LAB_NODE_GRIP_RIGHT }; + specs[HRECT_BORDER_BOTTOM_LEFT] = (struct rect_spec){ + 0, l->bw + l->hw, l->bw + l->grip_w + l->bw, l->bw, + LAB_NODE_GRIP_LEFT }; + specs[HRECT_BORDER_BOTTOM_CENTER] = (struct rect_spec){ + l->handle_x, l->bw + l->hw, l->handle_w, l->bw, + LAB_NODE_HANDLE }; + specs[HRECT_BORDER_BOTTOM_RIGHT] = (struct rect_spec){ + l->grip_right_x - l->bw, l->bw + l->hw, + l->bw + l->grip_w + l->bw, l->bw, LAB_NODE_GRIP_RIGHT }; + specs[HRECT_BORDER_TOP_LEFT] = (struct rect_spec){ + l->bw, 0, l->grip_w + l->bw, l->bw, + LAB_NODE_GRIP_LEFT }; + specs[HRECT_BORDER_TOP_CENTER] = (struct rect_spec){ + l->handle_x, 0, l->handle_w, l->bw, + LAB_NODE_HANDLE }; + specs[HRECT_BORDER_TOP_RIGHT] = (struct rect_spec){ + l->grip_right_x - l->bw, 0, l->grip_w + l->bw, l->bw, + LAB_NODE_GRIP_RIGHT }; + specs[HRECT_SEPARATOR_LEFT] = (struct rect_spec){ + l->bw + l->grip_w, l->content_y, l->bw, l->content_h, + LAB_NODE_GRIP_LEFT }; + specs[HRECT_SEPARATOR_RIGHT] = (struct rect_spec){ + l->grip_right_x - l->bw, l->content_y, l->bw, l->content_h, + LAB_NODE_GRIP_RIGHT }; +} + +static void +compute_elem_geometry(const struct handle_layout *l, + struct elem_geometry elems[SSD_HANDLE_ELEMENT_COUNT]) +{ + elems[SSD_HANDLE_ELEMENT_GRIP_LEFT] = (struct elem_geometry){ + l->grip_left_x, l->content_y, l->grip_w, l->content_h }; + elems[SSD_HANDLE_ELEMENT_CENTER] = (struct elem_geometry){ + l->handle_x, l->content_y, l->handle_w, l->content_h }; + elems[SSD_HANDLE_ELEMENT_GRIP_RIGHT] = (struct elem_geometry){ + l->grip_right_x, l->content_y, l->grip_w, l->content_h }; +} + +static struct wlr_scene_buffer * +create_handle_buffer(struct wlr_scene_tree *parent, + struct wlr_buffer *buf, int x, int y, int w, int h) +{ + struct wlr_scene_buffer *sbuf = + lab_wlr_scene_buffer_create(parent, buf); + wlr_scene_node_set_position(&sbuf->node, x, y); + wlr_scene_buffer_set_dest_size(sbuf, w, h); + if (wlr_renderer_is_pixman(server.renderer)) { + wlr_scene_buffer_set_filter_mode( + sbuf, WLR_SCALE_FILTER_NEAREST); + } + return sbuf; +} + +static void +create_inset_rects(struct wlr_scene_tree *parent, + struct ssd_handle_subtree *subtree, int idx, + int x, int y, int w, int h) +{ + /* Top inset (dark) */ + subtree->inset_top[idx] = lab_wlr_scene_rect_create( + parent, w, INSET_LINE_WIDTH, inset_dark); + wlr_scene_node_set_position( + &subtree->inset_top[idx]->node, x, y); + wlr_scene_node_set_enabled( + &subtree->inset_top[idx]->node, false); + + /* Left inset (dark) */ + subtree->inset_left[idx] = lab_wlr_scene_rect_create( + parent, INSET_LINE_WIDTH, h, inset_dark); + wlr_scene_node_set_position( + &subtree->inset_left[idx]->node, x, y); + wlr_scene_node_set_enabled( + &subtree->inset_left[idx]->node, false); + + /* Bottom inset (light highlight) */ + subtree->inset_bottom[idx] = lab_wlr_scene_rect_create( + parent, w, INSET_LINE_WIDTH, inset_light); + wlr_scene_node_set_position( + &subtree->inset_bottom[idx]->node, x, y + h - INSET_LINE_WIDTH); + wlr_scene_node_set_enabled( + &subtree->inset_bottom[idx]->node, false); + + /* Right inset (light highlight) */ + subtree->inset_right[idx] = lab_wlr_scene_rect_create( + parent, INSET_LINE_WIDTH, h, inset_light); + wlr_scene_node_set_position( + &subtree->inset_right[idx]->node, + x + w - INSET_LINE_WIDTH, y); + wlr_scene_node_set_enabled( + &subtree->inset_right[idx]->node, false); +} + +void +ssd_handle_create(struct ssd *ssd) +{ + assert(ssd); + struct view *view = ssd->view; + struct theme *theme = rc.theme; + + if (theme->handle_width <= 0) { + ssd->handle.tree = NULL; + return; + } + + int width = view->current.width; + struct handle_layout l = compute_layout(theme, width); + + ssd->handle.tree = lab_wlr_scene_tree_create(ssd->tree); + wlr_scene_node_set_position(&ssd->handle.tree->node, + -theme->border_width, + view_effective_height(view, /* use_pending */ false)); + + /* Initialize element states */ + for (int i = 0; i < SSD_HANDLE_ELEMENT_COUNT; i++) { + ssd->handle.element_states[i] = SSD_HANDLE_STATE_NORMAL; + } + + enum ssd_active_state active; + FOR_EACH_ACTIVE_STATE(active) { + struct ssd_handle_subtree *subtree = + &ssd->handle.subtrees[active]; + subtree->tree = lab_wlr_scene_tree_create(ssd->handle.tree); + struct wlr_scene_tree *parent = subtree->tree; + wlr_scene_node_set_enabled(&parent->node, active); + float *border_color = theme->window[active].border_color; + + /* Border and separator rects */ + struct rect_spec rspecs[HRECT_COUNT]; + compute_rect_specs(&l, rspecs); + for (int i = 0; i < HRECT_COUNT; i++) { + subtree->rects[i] = lab_wlr_scene_rect_create( + parent, rspecs[i].w, rspecs[i].h, + border_color); + wlr_scene_node_set_position( + &subtree->rects[i]->node, + rspecs[i].x, rspecs[i].y); + node_descriptor_create(&subtree->rects[i]->node, + rspecs[i].desc_type, view, NULL); + } + + /* Per-element geometry (shared by textures, overlays, insets) */ + struct elem_geometry elems[SSD_HANDLE_ELEMENT_COUNT]; + compute_elem_geometry(&l, elems); + struct wlr_buffer *bufs[SSD_HANDLE_ELEMENT_COUNT] = { + &theme->window[active].grip_fill->base, + &theme->window[active].handle_fill->base, + &theme->window[active].grip_fill->base, + }; + + for (int i = 0; i < SSD_HANDLE_ELEMENT_COUNT; i++) { + /* Texture buffer (1px wide, stretched to fill) */ + subtree->textures[i] = create_handle_buffer( + parent, bufs[i], elems[i].x, elems[i].y, + elems[i].w, elems[i].h); + node_descriptor_create( + &subtree->textures[i]->node, + elem_node_types[i], view, NULL); + + /* Overlay rect for hover/pressed dimming */ + subtree->overlay[i] = lab_wlr_scene_rect_create( + parent, elems[i].w, elems[i].h, + overlay_none); + wlr_scene_node_set_position( + &subtree->overlay[i]->node, + elems[i].x, elems[i].y); + wlr_scene_node_set_enabled( + &subtree->overlay[i]->node, false); + node_descriptor_create( + &subtree->overlay[i]->node, + elem_node_types[i], view, NULL); + + /* Inset shadow rects for pressed/dragging state */ + create_inset_rects(parent, subtree, i, + elems[i].x, elems[i].y, + elems[i].w, elems[i].h); + } + } + + if (view->maximized == VIEW_AXIS_BOTH) { + wlr_scene_node_set_enabled( + &ssd->handle.tree->node, false); + } +} + +void +ssd_handle_update(struct ssd *ssd) +{ + assert(ssd); + + if (!ssd->handle.tree) { + return; + } + + struct view *view = ssd->view; + struct theme *theme = rc.theme; + + bool should_show = view_handle_visible(view) + && view->maximized != VIEW_AXIS_BOTH; + + if (!should_show) { + if (ssd->handle.tree->node.enabled) { + wlr_scene_node_set_enabled( + &ssd->handle.tree->node, false); + } + return; + } else if (!ssd->handle.tree->node.enabled) { + wlr_scene_node_set_enabled( + &ssd->handle.tree->node, true); + } + + int width = view->current.width; + int height = view_effective_height(view, /* use_pending */ false); + struct handle_layout l = compute_layout(theme, width); + + /* Update position: handle sits below the client area */ + wlr_scene_node_set_position(&ssd->handle.tree->node, + -theme->border_width, height); + + enum ssd_active_state active; + FOR_EACH_ACTIVE_STATE(active) { + struct ssd_handle_subtree *subtree = + &ssd->handle.subtrees[active]; + + float *border_color = theme->window[active].border_color; + + /* Update border and separator rects */ + struct rect_spec rspecs[HRECT_COUNT]; + compute_rect_specs(&l, rspecs); + for (int i = 0; i < HRECT_COUNT; i++) { + wlr_scene_rect_set_size(subtree->rects[i], + rspecs[i].w, rspecs[i].h); + wlr_scene_node_set_position( + &subtree->rects[i]->node, + rspecs[i].x, rspecs[i].y); + wlr_scene_rect_set_color(subtree->rects[i], + border_color); + } + + /* Update textures, overlays, and insets per element */ + struct elem_geometry elems[SSD_HANDLE_ELEMENT_COUNT]; + compute_elem_geometry(&l, elems); + + for (int i = 0; i < SSD_HANDLE_ELEMENT_COUNT; i++) { + int ex = elems[i].x; + int ey = elems[i].y; + int ew = elems[i].w; + int eh = elems[i].h; + + wlr_scene_node_set_position( + &subtree->textures[i]->node, ex, ey); + wlr_scene_buffer_set_dest_size( + subtree->textures[i], ew, eh); + + wlr_scene_rect_set_size( + subtree->overlay[i], ew, eh); + wlr_scene_node_set_position( + &subtree->overlay[i]->node, ex, ey); + + wlr_scene_rect_set_size( + subtree->inset_top[i], + ew, INSET_LINE_WIDTH); + wlr_scene_node_set_position( + &subtree->inset_top[i]->node, ex, ey); + + wlr_scene_rect_set_size( + subtree->inset_left[i], + INSET_LINE_WIDTH, eh); + wlr_scene_node_set_position( + &subtree->inset_left[i]->node, ex, ey); + + wlr_scene_rect_set_size( + subtree->inset_bottom[i], + ew, INSET_LINE_WIDTH); + wlr_scene_node_set_position( + &subtree->inset_bottom[i]->node, + ex, ey + eh - INSET_LINE_WIDTH); + + wlr_scene_rect_set_size( + subtree->inset_right[i], + INSET_LINE_WIDTH, eh); + wlr_scene_node_set_position( + &subtree->inset_right[i]->node, + ex + ew - INSET_LINE_WIDTH, ey); + } + } +} + +void +ssd_handle_destroy(struct ssd *ssd) +{ + assert(ssd); + + if (!ssd->handle.tree) { + return; + } + + wlr_scene_node_destroy(&ssd->handle.tree->node); + ssd->handle = (struct ssd_handle_scene){0}; +} + +/* + * Update the visual state of a single handle/grip element. + * This toggles the overlay and inset shadow rects on both active + * and inactive subtrees (only the enabled one is visible). + */ +void +ssd_handle_set_element_state(struct ssd *ssd, int element, + enum ssd_handle_state state) +{ + if (!ssd || !ssd->handle.tree || element < 0 + || element >= SSD_HANDLE_ELEMENT_COUNT) { + return; + } + + if (ssd->handle.element_states[element] == state) { + return; + } + ssd->handle.element_states[element] = state; + + enum ssd_active_state active; + FOR_EACH_ACTIVE_STATE(active) { + struct ssd_handle_subtree *subtree = + &ssd->handle.subtrees[active]; + struct wlr_scene_rect *overlay = subtree->overlay[element]; + + bool show_overlay = false; + bool show_insets = false; + + switch (state) { + case SSD_HANDLE_STATE_NORMAL: + break; + case SSD_HANDLE_STATE_HOVER: + wlr_scene_rect_set_color(overlay, overlay_hover); + show_overlay = true; + break; + case SSD_HANDLE_STATE_PRESSED: + wlr_scene_rect_set_color(overlay, overlay_pressed); + show_overlay = true; + show_insets = true; + break; + case SSD_HANDLE_STATE_DRAGGING: + wlr_scene_rect_set_color(overlay, overlay_hover); + show_overlay = true; + show_insets = true; + break; + } + + wlr_scene_node_set_enabled(&overlay->node, show_overlay); + wlr_scene_node_set_enabled( + &subtree->inset_top[element]->node, show_insets); + wlr_scene_node_set_enabled( + &subtree->inset_left[element]->node, show_insets); + wlr_scene_node_set_enabled( + &subtree->inset_bottom[element]->node, show_insets); + wlr_scene_node_set_enabled( + &subtree->inset_right[element]->node, show_insets); + } +} + +void +ssd_handle_clear_all_states(struct ssd *ssd) +{ + if (!ssd || !ssd->handle.tree) { + return; + } + for (int i = 0; i < SSD_HANDLE_ELEMENT_COUNT; i++) { + ssd_handle_set_element_state(ssd, i, + SSD_HANDLE_STATE_NORMAL); + } +} + +/* + * Determine which handle element (if any) a scene node belongs to, + * and resolve the owning view/SSD. + * + * Returns the element index (SSD_HANDLE_ELEMENT_*) or -1 if the + * node is not part of any handle. If found, *out_ssd is set to + * the owning SSD. + */ +static int +handle_element_from_node(struct wlr_scene_node *node, struct ssd **out_ssd) +{ + *out_ssd = NULL; + if (!node) { + return -1; + } + + /* + * Walk up the node tree to find a node_descriptor with + * a handle/grip type. + */ + struct wlr_scene_node *n = node; + int element = -1; + while (n) { + if (n->data) { + struct node_descriptor *desc = n->data; + switch (desc->type) { + case LAB_NODE_GRIP_LEFT: + element = SSD_HANDLE_ELEMENT_GRIP_LEFT; + break; + case LAB_NODE_HANDLE: + /* + * Center handle is identified for correct cursor + * shape but visual hover/pressed effects are + * intentionally limited to corner grips only. + */ + element = SSD_HANDLE_ELEMENT_CENTER; + break; + case LAB_NODE_GRIP_RIGHT: + element = SSD_HANDLE_ELEMENT_GRIP_RIGHT; + break; + default: + break; + } + if (element >= 0) { + struct view *view = desc->view; + if (view && view->ssd + && view->ssd->handle.tree) { + *out_ssd = view->ssd; + } + return element; + } + } + n = n->parent ? &n->parent->node : NULL; + } + return -1; +} + +/* + * Update hover state based on the node under the cursor. + * Uses global server.hovered_handle_ssd / hovered_handle_element + * to track state across views (mirrors ssd_update_hovered_button + * pattern). + * + * Called from cursor_update_common() in cursor.c. + */ +void +ssd_update_hovered_handle(struct wlr_scene_node *node) +{ + struct ssd *new_ssd = NULL; + int new_element = -1; + int raw_element; + + raw_element = handle_element_from_node(node, &new_ssd); + + if (new_ssd && raw_element >= 0) { + /* + * Visual hover/pressed effects apply to corner grips + * only. The center handle still changes the cursor + * shape via node_type_to_edges() but receives no + * dimming overlay. + */ + if (raw_element != SSD_HANDLE_ELEMENT_CENTER) { + new_element = raw_element; + } + } + + struct ssd *old_ssd = server.hovered_handle_ssd; + int old_element = server.hovered_handle_element; + + /* Same element on same SSD -- nothing to do */ + if (old_ssd == new_ssd && old_element == new_element) { + return; + } + + /* Clear hover on old element */ + if (old_ssd && old_ssd->handle.tree && old_element >= 0) { + if (old_ssd->handle.element_states[old_element] + == SSD_HANDLE_STATE_HOVER) { + ssd_handle_set_element_state(old_ssd, + old_element, SSD_HANDLE_STATE_NORMAL); + } + } + + /* Set hover on new element */ + if (new_ssd && new_element >= 0) { + if (new_ssd->handle.element_states[new_element] + == SSD_HANDLE_STATE_NORMAL) { + ssd_handle_set_element_state(new_ssd, + new_element, SSD_HANDLE_STATE_HOVER); + } + } + + server.hovered_handle_ssd = new_ssd; + server.hovered_handle_element = new_element; +} diff --git a/src/ssd/ssd.c b/src/ssd/ssd.c index e2ab6375..13d9e3c1 100644 --- a/src/ssd/ssd.c +++ b/src/ssd/ssd.c @@ -59,6 +59,18 @@ ssd_thickness(struct view *view) if (!view_titlebar_visible(view)) { thickness.top -= theme->titlebar_height; } + + /* + * When the handle is visible, the bottom thickness is the + * full handle assembly: top separator (bw) + content (hw) + * + bottom border (bw). Per Openbox spec, handle_width is + * the content-only height with borders drawn around it. + */ + if (view_handle_visible(view)) { + thickness.bottom = 2 * theme->border_width + + theme->handle_width; + } + return thickness; } @@ -122,14 +134,34 @@ ssd_get_resizing_type(const struct ssd *ssd, struct wlr_cursor *cursor) return LAB_NODE_CORNER_TOP_LEFT; } else if (top && right) { return LAB_NODE_CORNER_TOP_RIGHT; - } else if (bottom && left) { - return LAB_NODE_CORNER_BOTTOM_LEFT; - } else if (bottom && right) { - return LAB_NODE_CORNER_BOTTOM_RIGHT; + } else if (bottom) { + /* + * When the handle is visible, the grip columns define + * the effective corner zones for the bottom edge. + * Expand the corner width so that the extents below + * the grips produce diagonal resize types matching the + * grip layout above them. + */ + if (view_handle_visible(view)) { + struct theme *theme = rc.theme; + int grip_col = theme->grip_width + theme->border_width; + int wide = MAX(corner_width, grip_col); + if (cursor->x < view_box.x + wide) { + return LAB_NODE_CORNER_BOTTOM_LEFT; + } else if (cursor->x > view_box.x + + view_box.width - wide) { + return LAB_NODE_CORNER_BOTTOM_RIGHT; + } + } else { + if (left) { + return LAB_NODE_CORNER_BOTTOM_LEFT; + } else if (right) { + return LAB_NODE_CORNER_BOTTOM_RIGHT; + } + } + return LAB_NODE_BORDER_BOTTOM; } else if (top) { return LAB_NODE_BORDER_TOP; - } else if (bottom) { - return LAB_NODE_BORDER_BOTTOM; } else if (left) { return LAB_NODE_BORDER_LEFT; } else if (right) { @@ -167,10 +199,14 @@ ssd_create(struct view *view, bool active) */ ssd_titlebar_create(ssd); ssd_border_create(ssd); + ssd_handle_create(ssd); if (!view_titlebar_visible(view)) { /* Ensure we keep the old state on Reconfigure or when exiting fullscreen */ ssd_set_titlebar(ssd, false); } + if (!view_handle_visible(view)) { + ssd_set_handle(ssd, false); + } ssd->margin = ssd_thickness(view); ssd_set_active(ssd, active); ssd_enable_keybind_inhibit_indicator(ssd, view->inhibits_keybinds); @@ -234,6 +270,7 @@ ssd_update_geometry(struct ssd *ssd) * maximizedDecoration=none */ ssd_set_titlebar(ssd, view_titlebar_visible(view)); + ssd_set_handle(ssd, view_handle_visible(view)); if (update_extents) { ssd_extents_update(ssd); @@ -242,6 +279,7 @@ ssd_update_geometry(struct ssd *ssd) if (update_area || state_changed) { ssd_titlebar_update(ssd); ssd_border_update(ssd); + ssd_handle_update(ssd); ssd_shadow_update(ssd); } @@ -264,6 +302,22 @@ ssd_set_titlebar(struct ssd *ssd, bool enabled) ssd->margin = ssd_thickness(ssd->view); } +void +ssd_set_handle(struct ssd *ssd, bool enabled) +{ + if (!ssd || !ssd->handle.tree) { + return; + } + if (ssd->handle.tree->node.enabled == enabled) { + return; + } + wlr_scene_node_set_enabled(&ssd->handle.tree->node, enabled); + ssd_border_update(ssd); + ssd_extents_update(ssd); + ssd_shadow_update(ssd); + ssd->margin = ssd_thickness(ssd->view); +} + void ssd_destroy(struct ssd *ssd) { @@ -277,10 +331,15 @@ ssd_destroy(struct ssd *ssd) server.hovered_button->node) == view) { server.hovered_button = NULL; } + if (server.hovered_handle_ssd == ssd) { + server.hovered_handle_ssd = NULL; + server.hovered_handle_element = -1; + } /* Destroy subcomponents */ ssd_titlebar_destroy(ssd); ssd_border_destroy(ssd); + ssd_handle_destroy(ssd); ssd_extents_destroy(ssd); ssd_shadow_destroy(ssd); wlr_scene_node_destroy(&ssd->tree->node); @@ -298,6 +357,8 @@ ssd_mode_parse(const char *mode) return LAB_SSD_MODE_NONE; } else if (!strcasecmp(mode, "border")) { return LAB_SSD_MODE_BORDER; + } else if (!strcasecmp(mode, "border-handle")) { + return LAB_SSD_MODE_BORDER_HANDLE; } else if (!strcasecmp(mode, "full")) { return LAB_SSD_MODE_FULL; } else { @@ -324,6 +385,11 @@ ssd_set_active(struct ssd *ssd, bool active) &ssd->shadow.subtrees[active_state].tree->node, active == active_state); } + if (ssd->handle.tree) { + wlr_scene_node_set_enabled( + &ssd->handle.subtrees[active_state].tree->node, + active == active_state); + } } } @@ -335,6 +401,7 @@ ssd_enable_shade(struct ssd *ssd, bool enable) } ssd_titlebar_update(ssd); ssd_border_update(ssd); + ssd_handle_update(ssd); wlr_scene_node_set_enabled(&ssd->extents.tree->node, !enable); ssd_shadow_update(ssd); } @@ -385,5 +452,13 @@ ssd_debug_get_node_name(const struct ssd *ssd, struct wlr_scene_node *node) if (node == &ssd->extents.tree->node) { return "extents"; } + if (ssd->handle.tree) { + if (node == &ssd->handle.subtrees[SSD_ACTIVE].tree->node) { + return "handle.active"; + } + if (node == &ssd->handle.subtrees[SSD_INACTIVE].tree->node) { + return "handle.inactive"; + } + } return NULL; } diff --git a/src/theme.c b/src/theme.c index 71e577ce..54652cea 100644 --- a/src/theme.c +++ b/src/theme.c @@ -532,6 +532,8 @@ static void theme_builtin(struct theme *theme) { theme->border_width = 1; + theme->handle_width = 6; + theme->grip_width = 20; theme->window_titlebar_padding_height = 0; theme->window_titlebar_padding_width = 0; @@ -551,6 +553,30 @@ theme_builtin(struct theme *theme) theme->window[SSD_ACTIVE].title_bg.color_to_split_to[0] = FLT_MIN; theme->window[SSD_INACTIVE].title_bg.color_to_split_to[0] = FLT_MIN; + /* handle background defaults */ + theme->window[SSD_ACTIVE].handle_bg.gradient = LAB_GRADIENT_NONE; + theme->window[SSD_INACTIVE].handle_bg.gradient = LAB_GRADIENT_NONE; + parse_hexstr("#a0a0a0", theme->window[SSD_ACTIVE].handle_bg.color); + parse_hexstr("#c0c0c0", theme->window[SSD_INACTIVE].handle_bg.color); + theme->window[SSD_ACTIVE].handle_bg.color_split_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].handle_bg.color_split_to[0] = FLT_MIN; + theme->window[SSD_ACTIVE].handle_bg.color_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].handle_bg.color_to[0] = FLT_MIN; + theme->window[SSD_ACTIVE].handle_bg.color_to_split_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].handle_bg.color_to_split_to[0] = FLT_MIN; + + /* grip background defaults (FLT_MIN sentinel = inherit from handle) */ + theme->window[SSD_ACTIVE].grip_bg.gradient = LAB_GRADIENT_NONE; + theme->window[SSD_INACTIVE].grip_bg.gradient = LAB_GRADIENT_NONE; + theme->window[SSD_ACTIVE].grip_bg.color[0] = FLT_MIN; + theme->window[SSD_INACTIVE].grip_bg.color[0] = FLT_MIN; + theme->window[SSD_ACTIVE].grip_bg.color_split_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].grip_bg.color_split_to[0] = FLT_MIN; + theme->window[SSD_ACTIVE].grip_bg.color_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].grip_bg.color_to[0] = FLT_MIN; + theme->window[SSD_ACTIVE].grip_bg.color_to_split_to[0] = FLT_MIN; + theme->window[SSD_INACTIVE].grip_bg.color_to_split_to[0] = FLT_MIN; + parse_hexstr("#000000", theme->window[SSD_ACTIVE].label_text_color); parse_hexstr("#000000", theme->window[SSD_INACTIVE].label_text_color); theme->window_label_text_justify = parse_justification("Center"); @@ -757,6 +783,82 @@ entry(struct theme *theme, const char *key, const char *value) parse_color(value, theme->window[SSD_INACTIVE].title_bg.color_to_split_to); } + /* handle width (Openbox "window.handle.width" is actually the height) */ + if (match_glob(key, "window.handle.width")) { + theme->handle_width = get_int_if_positive( + value, "window.handle.width"); + } + + /* grip width */ + if (match_glob(key, "window.grip.width")) { + theme->grip_width = get_int_if_positive( + value, "window.grip.width"); + } + + /* handle background */ + if (match_glob(key, "window.active.handle.bg")) { + theme->window[SSD_ACTIVE].handle_bg.gradient = parse_gradient(value); + } + if (match_glob(key, "window.inactive.handle.bg")) { + theme->window[SSD_INACTIVE].handle_bg.gradient = parse_gradient(value); + } + if (match_glob(key, "window.active.handle.bg.color")) { + parse_color(value, theme->window[SSD_ACTIVE].handle_bg.color); + } + if (match_glob(key, "window.inactive.handle.bg.color")) { + parse_color(value, theme->window[SSD_INACTIVE].handle_bg.color); + } + if (match_glob(key, "window.active.handle.bg.color.splitTo")) { + parse_color(value, theme->window[SSD_ACTIVE].handle_bg.color_split_to); + } + if (match_glob(key, "window.inactive.handle.bg.color.splitTo")) { + parse_color(value, theme->window[SSD_INACTIVE].handle_bg.color_split_to); + } + if (match_glob(key, "window.active.handle.bg.colorTo")) { + parse_color(value, theme->window[SSD_ACTIVE].handle_bg.color_to); + } + if (match_glob(key, "window.inactive.handle.bg.colorTo")) { + parse_color(value, theme->window[SSD_INACTIVE].handle_bg.color_to); + } + if (match_glob(key, "window.active.handle.bg.colorTo.splitTo")) { + parse_color(value, theme->window[SSD_ACTIVE].handle_bg.color_to_split_to); + } + if (match_glob(key, "window.inactive.handle.bg.colorTo.splitTo")) { + parse_color(value, theme->window[SSD_INACTIVE].handle_bg.color_to_split_to); + } + + /* grip background */ + if (match_glob(key, "window.active.grip.bg")) { + theme->window[SSD_ACTIVE].grip_bg.gradient = parse_gradient(value); + } + if (match_glob(key, "window.inactive.grip.bg")) { + theme->window[SSD_INACTIVE].grip_bg.gradient = parse_gradient(value); + } + if (match_glob(key, "window.active.grip.bg.color")) { + parse_color(value, theme->window[SSD_ACTIVE].grip_bg.color); + } + if (match_glob(key, "window.inactive.grip.bg.color")) { + parse_color(value, theme->window[SSD_INACTIVE].grip_bg.color); + } + if (match_glob(key, "window.active.grip.bg.color.splitTo")) { + parse_color(value, theme->window[SSD_ACTIVE].grip_bg.color_split_to); + } + if (match_glob(key, "window.inactive.grip.bg.color.splitTo")) { + parse_color(value, theme->window[SSD_INACTIVE].grip_bg.color_split_to); + } + if (match_glob(key, "window.active.grip.bg.colorTo")) { + parse_color(value, theme->window[SSD_ACTIVE].grip_bg.color_to); + } + if (match_glob(key, "window.inactive.grip.bg.colorTo")) { + parse_color(value, theme->window[SSD_INACTIVE].grip_bg.color_to); + } + if (match_glob(key, "window.active.grip.bg.colorTo.splitTo")) { + parse_color(value, theme->window[SSD_ACTIVE].grip_bg.color_to_split_to); + } + if (match_glob(key, "window.inactive.grip.bg.colorTo.splitTo")) { + parse_color(value, theme->window[SSD_INACTIVE].grip_bg.color_to_split_to); + } + if (match_glob(key, "window.active.label.text.color")) { parse_color(value, theme->window[SSD_ACTIVE].label_text_color); } @@ -1410,6 +1512,28 @@ create_backgrounds(struct theme *theme) theme->window[active].titlebar_pattern, theme->titlebar_height); } + + /* Create handle and grip fill buffers if handle is enabled */ + if (theme->handle_width > 0) { + FOR_EACH_ACTIVE_STATE(active) { + theme->window[active].handle_pattern = + create_titlebar_pattern( + &theme->window[active].handle_bg, + theme->handle_width); + theme->window[active].handle_fill = + create_titlebar_fill( + theme->window[active].handle_pattern, + theme->handle_width); + theme->window[active].grip_pattern = + create_titlebar_pattern( + &theme->window[active].grip_bg, + theme->handle_width); + theme->window[active].grip_fill = + create_titlebar_fill( + theme->window[active].grip_pattern, + theme->handle_width); + } + } } static void @@ -1686,6 +1810,35 @@ post_processing(struct theme *theme) fill_background_colors(&theme->window[SSD_INACTIVE].title_bg); fill_background_colors(&theme->window[SSD_ACTIVE].title_bg); + /* Clamp handle_width to a sensible maximum */ + if (theme->handle_width > 100) { + wlr_log(WLR_ERROR, + "window.handle.width clamped from %d to 100", + theme->handle_width); + theme->handle_width = 100; + } + + /* Clamp grip_width minimum to 1 */ + if (theme->grip_width < 1) { + theme->grip_width = 1; + } + + /* Fill handle gradient color defaults */ + fill_background_colors(&theme->window[SSD_INACTIVE].handle_bg); + fill_background_colors(&theme->window[SSD_ACTIVE].handle_bg); + + /* Inherit grip from handle if grip color was not explicitly set */ + enum ssd_active_state active; + FOR_EACH_ACTIVE_STATE(active) { + struct theme_background *grip = &theme->window[active].grip_bg; + struct theme_background *handle = &theme->window[active].handle_bg; + if (grip->color[0] == FLT_MIN) { + memcpy(grip, handle, sizeof(*grip)); + } else { + fill_background_colors(grip); + } + } + theme->menu_item_height = font_height(&rc.font_menuitem) + 2 * theme->menu_items_padding_y; @@ -1877,5 +2030,9 @@ theme_finish(struct theme *theme) zdrop(&theme->window[active].shadow_corner_top); zdrop(&theme->window[active].shadow_corner_bottom); zdrop(&theme->window[active].shadow_edge); + zfree_pattern(theme->window[active].handle_pattern); + zfree_pattern(theme->window[active].grip_pattern); + zdrop(&theme->window[active].handle_fill); + zdrop(&theme->window[active].grip_fill); } } diff --git a/src/view.c b/src/view.c index 21005f20..9d443099 100644 --- a/src/view.c +++ b/src/view.c @@ -1531,6 +1531,8 @@ view_toggle_decorations(struct view *view) assert(view); if (rc.ssd_keep_border && view->ssd_mode == LAB_SSD_MODE_FULL) { + view_set_ssd_mode(view, LAB_SSD_MODE_BORDER_HANDLE); + } else if (view->ssd_mode == LAB_SSD_MODE_BORDER_HANDLE) { view_set_ssd_mode(view, LAB_SSD_MODE_BORDER); } else if (view->ssd_mode != LAB_SSD_MODE_NONE) { view_set_ssd_mode(view, LAB_SSD_MODE_NONE); @@ -1621,6 +1623,19 @@ view_titlebar_visible(struct view *view) return view->ssd_mode == LAB_SSD_MODE_FULL; } +bool +view_handle_visible(struct view *view) +{ + if (rc.theme->handle_width <= 0) { + return false; + } + if (view->shaded) { + return false; + } + return view->ssd_mode == LAB_SSD_MODE_FULL + || view->ssd_mode == LAB_SSD_MODE_BORDER_HANDLE; +} + void view_set_ssd_mode(struct view *view, enum lab_ssd_mode mode) { @@ -1640,6 +1655,7 @@ view_set_ssd_mode(struct view *view, enum lab_ssd_mode mode) if (mode) { decorate(view); ssd_set_titlebar(view->ssd, view_titlebar_visible(view)); + ssd_set_handle(view->ssd, view_handle_visible(view)); } else { undecorate(view); }