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

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