feat: implement Openbox-style bottom window handle and grips

Add full handle/grip assembly to the bottom of SSD window frames,
following the Openbox themerc specification for geometry and theming.
Theme parsing:
- Parse window.handle.width (handle bar height, default 6)
- Parse window.grip.width (corner grip width, default 20)
- Parse window.[active|inactive].handle.bg with Solid/Gradient support
- Parse window.[active|inactive].grip.bg (inherits from handle if unset)
- Pre-render 1px-wide fill buffers and cairo patterns for handle/grip
Scene graph (new ssd-handle.c):
- Handle assembly replaces bottom border when active, with its own
  left/right/top borders and three-segment bottom border
- Grips at left/right corners for diagonal resize (sw/se-resize)
- Center handle for vertical resize (s-resize)
- Vertical separator lines between grips and handle using border color
- Per Openbox spec, handle_width is content-only height with borders
  drawn around it (total assembly height = 2*border_width + handle_width)
Interactive visual states (grips only):
- Hover: 20% black overlay on grip content area
- Pressed: 40% black overlay with 1px inset shadow (dark top/left,
  light bottom/right) for a pushed-in 3D effect
- Dragging: 20% overlay with inset shadow maintained
- Global hover tracking (server.hovered_handle_ssd/element) ensures
  proper cleanup when cursor moves across views or to desktop
Decoration toggle cycle (ToggleDecorations action):
- New LAB_SSD_MODE_BORDER_HANDLE between BORDER and FULL
- keepBorder=true: full -> border+handle -> border -> none -> full
- keepBorder=false: full -> none -> full (unchanged)
Node types and input:
- New LAB_NODE_HANDLE, LAB_NODE_GRIP_LEFT, LAB_NODE_GRIP_RIGHT
- Integrated into LAB_NODE_BORDER/BORDER_BOTTOM containment so
  existing Border context mousebinds (Resize) work automatically
- Handle/grip descriptors resolved directly in get_cursor_context()
  bypassing ssd_get_resizing_type() for precise cursor shapes
Visibility rules:
- Hidden when maximized, shaded, or handle_width is 0
- Hidden in LAB_SSD_MODE_BORDER and LAB_SSD_MODE_NONE states
- Bottom border in ssd-border.c disabled when handle is active
Documentation:
- labwc-theme.5.scd: document all handle/grip theme properties
- labwc-actions.5.scd: update ToggleDecorations to 4-state cycle
- docs/themerc: add handle/grip default values
This commit is contained in:
stormshadow 2026-05-14 06:23:49 +05:30
parent 4af693a7fd
commit ba5a0b9829
19 changed files with 1132 additions and 17 deletions

View file

@ -217,13 +217,14 @@ Actions are used in menus and keyboard/mouse bindings.
*<action name="ToggleDecorations" />*
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.
*<action name="ToggleFullscreen" />*
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|<monitor_name>]
Whether the client is on a monitor relative to the to

View file

@ -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

View file

@ -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:
#

View file

@ -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,

View file

@ -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,
};

View file

@ -229,6 +229,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;

View file

@ -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 */

View file

@ -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);

View file

@ -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 */

View file

@ -562,6 +562,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);

View file

@ -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;
}

View file

@ -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:

View file

@ -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);
}

View file

@ -5,5 +5,6 @@ labwc_sources += files(
'ssd-titlebar.c',
'ssd-border.c',
'ssd-extents.c',
'ssd-handle.c',
'ssd-shadow.c',
)

View file

@ -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);
}
}

607
src/ssd/ssd-handle.c Normal file
View file

@ -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 <assert.h>
#include <wlr/render/pixman.h>
#include <wlr/types/wlr_scene.h>
#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;
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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);
}