Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: segmented border visuals — 8 gap-separated zone elements with per-zone hover
The border now draws as 8 visual elements matching the interactive
zones: 4 edge bars plus 4 L-shaped corners (two rects each), separated
by a 4px gap so each zone reads as its own handle. Only the hovered
zone highlights, replacing the whole-border highlight. The rounded
single-frame path is gone (the backplate keeps its corner radius); a
shared border_corner_len() keeps visuals and cursor zones aligned.
Underneath, the four full-band rects stay enabled but transparent as
scene hit-test catchers, so the pointer never falls through the visual
gaps and width-0 virtual zones behave as before.
Fixes hover highlight never showing: draw_borders gated the highlight
on wm_requested.ssd, which is false for plain toplevels; the zone side
used the ssd flag too and fell back to the 8px virtual band. Both
sides now key on border width alone.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/cursor.rs | 60 ++++++++----
src/server/window.rs | 265 ++++++++++++++++++++++++++-------------------------
2 files changed, 180 insertions(+), 145 deletions(-)
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 15aa161..a0a76cc 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -53,6 +53,8 @@ pub struct Cursor {
/// on hover transitions. May dangle after a close — validate against
/// `wm.windows` before dereferencing.
pub hovered_border_window: *mut crate::window::Window,
+ /// Which of that window's 8 border zones is highlighted.
+ pub hovered_border_element: Option<crate::window::BorderElement>,
pub right_click_on_bg: bool,
pub right_click_on_border: bool,
pub left_click_on_bg_in_overview: bool,
@@ -103,6 +105,7 @@ impl Default for Cursor {
last_click_time: 0,
last_click_window: std::ptr::null_mut(),
hovered_border_window: std::ptr::null_mut(),
+ hovered_border_element: None,
right_click_on_bg: false,
right_click_on_border: false,
left_click_on_bg_in_overview: false,
@@ -453,25 +456,31 @@ impl Cursor {
}
}
- /// Move the border hover highlight to `target` (null to clear): flips the
- /// `border_hovered` flag on the old and new windows and repaints their
- /// borders. The stored pointer may refer to a closed window, so it is
- /// only dereferenced after checking it is still in `wm.windows`.
- pub unsafe fn set_border_hover(&mut self, target: *mut crate::window::Window) {
- if self.hovered_border_window == target {
+ /// Move the border hover highlight to `target`'s `element` (null/None to
+ /// clear): updates `hovered_border_element` on the old and new windows
+ /// and repaints their borders. The stored pointer may refer to a closed
+ /// window, so it is only dereferenced after checking it is still in
+ /// `wm.windows`.
+ pub unsafe fn set_border_hover(
+ &mut self,
+ target: *mut crate::window::Window,
+ element: Option<crate::window::BorderElement>,
+ ) {
+ if self.hovered_border_window == target && self.hovered_border_element == element {
return;
}
let old = self.hovered_border_window;
- if !old.is_null() {
+ if !old.is_null() && old != target {
let wm = &(*(*self.seat).server).wm;
if wm.windows.iter().any(|&w| w == old) && !(*old).closed {
- (*old).border_hovered = false;
+ (*old).hovered_border_element = None;
(*old).draw_borders();
}
}
self.hovered_border_window = target;
+ self.hovered_border_element = element;
if !target.is_null() {
- (*target).border_hovered = true;
+ (*target).hovered_border_element = element;
(*target).draw_borders();
}
}
@@ -485,13 +494,13 @@ impl Cursor {
let lock_state = (*server).lock_manager.state;
if lock_state != crate::lock_manager::LockState::Unlocked {
if !matches!(result.data, SceneNodeDataVal::LockSurface(_)) {
- self.set_border_hover(std::ptr::null_mut());
+ self.set_border_hover(std::ptr::null_mut(), None);
self.clear_focus();
return;
}
} else {
if matches!(result.data, SceneNodeDataVal::LockSurface(_)) {
- self.set_border_hover(std::ptr::null_mut());
+ self.set_border_hover(std::ptr::null_mut(), None);
self.clear_focus();
return;
}
@@ -512,14 +521,14 @@ impl Cursor {
{
match get_border_zone(window, lx, ly) {
BorderZone::Resize(edges) => {
- self.set_border_hover(window);
+ self.set_border_hover(window, Some(border_element_for_edges(edges)));
ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
let cursor_name = get_resize_cursor_name(edges);
self.set_xcursor(cursor_name.as_ptr() as *const _);
return;
}
BorderZone::Move => {
- self.set_border_hover(window);
+ self.set_border_hover(window, Some(crate::window::BorderElement::Top));
ffi::wlr_seat_pointer_notify_clear_focus((*self.seat).wlr_seat);
self.set_xcursor(b"grab\0".as_ptr() as *const _);
return;
@@ -533,7 +542,7 @@ impl Cursor {
}
_ => {}
}
- self.set_border_hover(std::ptr::null_mut());
+ self.set_border_hover(std::ptr::null_mut(), None);
if is_window && (*server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
self.clear_focus();
@@ -547,7 +556,7 @@ impl Cursor {
}
}
- self.set_border_hover(std::ptr::null_mut());
+ self.set_border_hover(std::ptr::null_mut(), None);
self.clear_focus();
}
@@ -2261,7 +2270,9 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
return BorderZone::None;
}
- let is_virtual_border = !(*window).wm_requested.ssd || (*window).rendering_requested.border.width == 0;
+ // Width-only, matching draw_borders: visible borders are the zones at
+ // their real width regardless of the ssd flag.
+ let is_virtual_border = (*window).rendering_requested.border.width == 0;
let bw = if is_virtual_border {
8.0
} else {
@@ -2287,7 +2298,7 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
// moves the window, everything else resizes. Corner squares of
// `corner_len` (measured from the outer corners along the band)
// resize on both adjacent edges, so the top corners still resize.
- let corner_len = (2.0 * bw).max(16.0);
+ let corner_len = crate::window::border_corner_len(bw);
let dist_left = rx + bw;
let dist_right = (content_w + bw) - rx;
@@ -2326,6 +2337,21 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
BorderZone::None
}
+/// Map a resize zone's edges to the border element that should highlight.
+pub fn border_element_for_edges(edges: crate::window::Edges) -> crate::window::BorderElement {
+ use crate::window::BorderElement::*;
+ match (edges.top, edges.bottom, edges.left, edges.right) {
+ (true, _, true, _) => TopLeft,
+ (true, _, _, true) => TopRight,
+ (_, true, true, _) => BottomLeft,
+ (_, true, _, true) => BottomRight,
+ (_, true, _, _) => Bottom,
+ (_, _, true, _) => Left,
+ (_, _, _, true) => Right,
+ _ => Top,
+ }
+}
+
pub fn get_resize_cursor_name(edges: crate::window::Edges) -> &'static [u8] {
if edges.top && edges.left {
b"nw-resize\0"
diff --git a/src/server/window.rs b/src/server/window.rs
index 5448e75..6a4acfd 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -89,11 +89,54 @@ impl Border {
}
}
+/// One of the 8 interactive border zones. Each draws as its own visual
+/// element (corners as two-rect Ls) and highlights independently on hover.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum BorderElement {
+ Top,
+ Bottom,
+ Left,
+ Right,
+ TopLeft,
+ TopRight,
+ BottomLeft,
+ BottomRight,
+}
+
+/// Length of a corner zone, measured from the outer corner along each band.
+/// Shared by the visual segments (draw_borders) and the pointer zones
+/// (cursor.rs get_border_zone) so they always agree.
+pub fn border_corner_len(bw: f64) -> f64 {
+ (2.0 * bw).max(16.0)
+}
+
+/// Visual gap between border segments, so the 8 zones read as separate
+/// elements. Zones and hit-testing stay continuous across the gaps.
+pub const BORDER_SEGMENT_GAP: i32 = 4;
+
+// Indices into BorderRects.segments: 4 edge bars + 2 L-arm rects per corner.
+const SEG_TOP: usize = 0;
+const SEG_BOTTOM: usize = 1;
+const SEG_LEFT: usize = 2;
+const SEG_RIGHT: usize = 3;
+const SEG_TL_H: usize = 4;
+const SEG_TL_V: usize = 5;
+const SEG_TR_H: usize = 6;
+const SEG_TR_V: usize = 7;
+const SEG_BL_H: usize = 8;
+const SEG_BL_V: usize = 9;
+const SEG_BR_H: usize = 10;
+const SEG_BR_V: usize = 11;
+
pub struct BorderRects {
+ /// Invisible full-band rects kept as scene hit-test catchers, so the
+ /// pointer never falls through the visual gaps between segments.
pub left: *mut ffi::wlr_scene_rect,
pub right: *mut ffi::wlr_scene_rect,
pub top: *mut ffi::wlr_scene_rect,
pub bottom: *mut ffi::wlr_scene_rect,
+ /// The visible zone segments, indexed by the SEG_* constants.
+ pub segments: [*mut ffi::wlr_scene_rect; 12],
}
pub struct ShowWindowMenuRequest {
@@ -208,9 +251,9 @@ pub struct Window {
pub decorations_below_tree: *mut ffi::wlr_scene_tree,
pub surfaces: crate::scene::SaveableSurfaces,
pub border: BorderRects,
- /// Pointer is over the border grab surface (set by cursor.rs); the
- /// border draws in `hover_color` while set.
- pub border_hovered: bool,
+ /// The border zone the pointer is over (set by cursor.rs); that segment
+ /// draws in `hover_color` while set.
+ pub hovered_border_element: Option<BorderElement>,
pub decorations_above: ffi::wl_list,
pub decorations_above_tree: *mut ffi::wlr_scene_tree,
pub popup_tree: *mut ffi::wlr_scene_tree,
@@ -362,6 +405,10 @@ impl Window {
let border_right = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
let border_top = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
let border_bottom = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+ let mut border_segments = [std::ptr::null_mut(); 12];
+ for seg in border_segments.iter_mut() {
+ *seg = ffi::wlr_scene_rect_create(tree, 0, 0, clear_color.as_ptr());
+ }
let decorations_above_tree = ffi::wlr_scene_tree_create(tree);
@@ -383,8 +430,9 @@ impl Window {
right: border_right,
top: border_top,
bottom: border_bottom,
+ segments: border_segments,
},
- border_hovered: false,
+ hovered_border_element: None,
decorations_above: std::mem::zeroed(),
decorations_above_tree,
popup_tree,
@@ -2089,14 +2137,21 @@ impl Window {
ffi::river_scene_rect_set_corner_radius(self.window_background, (border.corner_radius as f64 * self.scale) as i32);
ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, !requested.hidden && self.wm_requested.ssd);
- // Zero configured width keeps the legacy invisible rects; cursor.rs
- // treats the same case as a "virtual" 8px resize zone.
+ // The border draws as 8 zone segments (4 edge bars + 4 two-rect L
+ // corners) with BORDER_SEGMENT_GAP between them; the hovered zone
+ // draws in hover_color. Underneath, the 4 full-band rects stay
+ // enabled but transparent as scene hit-test catchers, so the pointer
+ // never falls through the gaps (and width 0 keeps the legacy
+ // invisible 8px virtual resize zones).
let is_virtual_border = border.width == 0;
if requested.circular {
ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
ffi::wlr_scene_node_set_enabled(self.border.top as *mut ffi::wlr_scene_node, false);
ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
+ for &seg in self.border.segments.iter() {
+ ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
+ }
return;
}
let content = ffi::wlr_box {
@@ -2110,142 +2165,96 @@ impl Window {
let clip_empty = requested.content_clip.width == 0 && requested.content_clip.height == 0;
if clip_empty || ffi::wlr_box_intersection(&mut intersect, &content, &requested.content_clip) {
let border = &requested.border;
- let border_width = if is_virtual_border { 8 } else { border.width };
- // Hover highlights only real SSD borders; virtual rects stay
- // invisible. The backplate keeps the base color either way.
- let color: [f32; 4] = if is_virtual_border {
- [0.0, 0.0, 0.0, 0.0]
- } else if self.border_hovered && self.wm_requested.ssd {
- border.hover_color
- } else {
- border_color
- };
+ let band = if is_virtual_border { 8 } else { border.width as i32 };
+ let transparent = [0.0f32; 4];
- if !is_virtual_border && border.corner_radius > 0 {
- self.draw_rounded_border_frame(&content, border_width as i32, border.corner_radius, &color);
- return;
- }
- // Coming back from the rounded path: the top rect doubles as the
- // frame there and keeps rounded/clipped state otherwise.
+ // The rounded-frame path used to leave radius/clip state on the
+ // top band rect; keep it reset.
ffi::river_scene_rect_set_corner_radius(self.border.top, 0);
ffi::wlr_scene_rect_set_clipped_region(self.border.top, ffi::clipped_region_get_default());
- let mut left = ffi::wlr_box {
- x: -(border_width as i32),
- y: 0,
- width: border_width as i32,
- height: content.height,
- };
- let mut right = ffi::wlr_box {
- x: content.width,
- y: 0,
- width: border_width as i32,
- height: content.height,
- };
- let mut top = ffi::wlr_box {
- x: 0,
- y: -(border_width as i32),
- width: content.width,
- height: border_width as i32,
- };
- let mut bottom = ffi::wlr_box {
- x: 0,
- y: content.height,
- width: content.width,
- height: border_width as i32,
+ let apply = |rect: *mut ffi::wlr_scene_rect, bx: ffi::wlr_box, color: &[f32; 4], enabled: bool| {
+ let mut bx = bx;
+ if enabled && (requested.clip.width != 0 || requested.clip.height != 0) {
+ let mut clip_intersect = std::mem::zeroed();
+ ffi::wlr_box_intersection(&mut clip_intersect, &bx, &requested.clip);
+ bx = clip_intersect;
+ }
+ let enabled = enabled && bx.width > 0 && bx.height > 0;
+ ffi::wlr_scene_node_set_enabled(rect as *mut ffi::wlr_scene_node, enabled);
+ if !enabled {
+ return;
+ }
+ ffi::river_scene_node_set_position_if_changed(
+ rect as *mut ffi::wlr_scene_node,
+ (bx.x as f64 * self.scale) as i32,
+ (bx.y as f64 * self.scale) as i32,
+ );
+ ffi::river_scene_rect_set_size_if_changed(
+ rect,
+ (bx.width as f64 * self.scale) as i32,
+ (bx.height as f64 * self.scale) as i32,
+ );
+ ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
};
- let edge_left = if is_virtual_border { true } else { border.edges.left };
- let edge_right = if is_virtual_border { true } else { border.edges.right };
- let edge_top = if is_virtual_border { true } else { border.edges.top };
- let edge_bottom = if is_virtual_border { true } else { border.edges.bottom };
-
- if edge_top {
- left.y -= border_width as i32;
- left.height += border_width as i32;
- right.y -= border_width as i32;
- right.height += border_width as i32;
- }
- if edge_bottom {
- left.height += border_width as i32;
- right.height += border_width as i32;
+ // Full-band hit catchers: sides span the corners vertically.
+ let b = ffi::wlr_box { x: -band, y: -band, width: band, height: content.height + 2 * band };
+ apply(self.border.left, b, &transparent, true);
+ let b = ffi::wlr_box { x: content.width, y: -band, width: band, height: content.height + 2 * band };
+ apply(self.border.right, b, &transparent, true);
+ let b = ffi::wlr_box { x: 0, y: -band, width: content.width, height: band };
+ apply(self.border.top, b, &transparent, true);
+ let b = ffi::wlr_box { x: 0, y: content.height, width: content.width, height: band };
+ apply(self.border.bottom, b, &transparent, true);
+
+ if is_virtual_border {
+ for &seg in self.border.segments.iter() {
+ ffi::wlr_scene_node_set_enabled(seg as *mut ffi::wlr_scene_node, false);
+ }
+ return;
}
- let mut edges = [
- ("left", &mut left, self.border.left, edge_left),
- ("right", &mut right, self.border.right, edge_right),
- ("top", &mut top, self.border.top, edge_top),
- ("bottom", &mut bottom, self.border.bottom, edge_bottom),
- ];
+ let bw = border.width as i32;
+ let cl = border_corner_len(bw as f64) as i32;
+ let g = BORDER_SEGMENT_GAP;
+ let arm = cl - bw;
+ // Edge bars span between the corner zones, inset by the gap.
+ let bar_x = cl - bw + g;
+ let bar_w = content.width + 2 * bw - 2 * cl - 2 * g;
+ let bar_y = cl - bw + g;
+ let bar_h = content.height + 2 * bw - 2 * cl - 2 * g;
- for (_, edge_box, rect, enabled) in &mut edges {
- if requested.clip.width != 0 || requested.clip.height != 0 {
- let mut clip_intersect = std::mem::zeroed();
- ffi::wlr_box_intersection(&mut clip_intersect, *edge_box, &requested.clip);
- **edge_box = clip_intersect;
+ let color_for = |elem: BorderElement| -> [f32; 4] {
+ if self.hovered_border_element == Some(elem) {
+ border.hover_color
+ } else {
+ border_color
}
- ffi::wlr_scene_node_set_enabled(*rect as *mut ffi::wlr_scene_node, *enabled);
- let scaled_x = ((*edge_box).x as f64 * self.scale) as i32;
- let scaled_y = ((*edge_box).y as f64 * self.scale) as i32;
- let scaled_w = ((*edge_box).width as f64 * self.scale) as i32;
- let scaled_h = ((*edge_box).height as f64 * self.scale) as i32;
- ffi::river_scene_node_set_position_if_changed(*rect as *mut ffi::wlr_scene_node, scaled_x, scaled_y);
- ffi::river_scene_rect_set_size_if_changed(*rect, scaled_w, scaled_h);
- ffi::wlr_scene_rect_set_color(*rect, color.as_ptr());
+ };
+ let e = &border.edges;
+ use BorderElement::*;
+
+ let segs: [(usize, ffi::wlr_box, BorderElement, bool); 12] = [
+ (SEG_TOP, ffi::wlr_box { x: bar_x, y: -bw, width: bar_w, height: bw }, Top, e.top),
+ (SEG_BOTTOM, ffi::wlr_box { x: bar_x, y: content.height, width: bar_w, height: bw }, Bottom, e.bottom),
+ (SEG_LEFT, ffi::wlr_box { x: -bw, y: bar_y, width: bw, height: bar_h }, Left, e.left),
+ (SEG_RIGHT, ffi::wlr_box { x: content.width, y: bar_y, width: bw, height: bar_h }, Right, e.right),
+ (SEG_TL_H, ffi::wlr_box { x: -bw, y: -bw, width: cl, height: bw }, TopLeft, e.top && e.left),
+ (SEG_TL_V, ffi::wlr_box { x: -bw, y: 0, width: bw, height: arm }, TopLeft, e.top && e.left),
+ (SEG_TR_H, ffi::wlr_box { x: content.width + bw - cl, y: -bw, width: cl, height: bw }, TopRight, e.top && e.right),
+ (SEG_TR_V, ffi::wlr_box { x: content.width, y: 0, width: bw, height: arm }, TopRight, e.top && e.right),
+ (SEG_BL_H, ffi::wlr_box { x: -bw, y: content.height, width: cl, height: bw }, BottomLeft, e.bottom && e.left),
+ (SEG_BL_V, ffi::wlr_box { x: -bw, y: content.height - arm, width: bw, height: arm }, BottomLeft, e.bottom && e.left),
+ (SEG_BR_H, ffi::wlr_box { x: content.width + bw - cl, y: content.height, width: cl, height: bw }, BottomRight, e.bottom && e.right),
+ (SEG_BR_V, ffi::wlr_box { x: content.width, y: content.height - arm, width: bw, height: arm }, BottomRight, e.bottom && e.right),
+ ];
+ for (idx, bx, elem, enabled) in segs {
+ apply(self.border.segments[idx], bx, &color_for(elem), enabled);
}
}
}
- /// Rounded border: a single frame rect (the `top` border node) covering
- /// content plus border on all edges, with the content area clipped out.
- /// Per-edge selection doesn't apply here — a rounded frame is all-edges.
- unsafe fn draw_rounded_border_frame(&mut self, content: &ffi::wlr_box, width: i32, corner_radius: i32, color: &[f32; 4]) {
- let requested = &self.rendering_requested;
-
- let mut frame = ffi::wlr_box {
- x: -width,
- y: -width,
- width: content.width + 2 * width,
- height: content.height + 2 * width,
- };
- if requested.clip.width != 0 || requested.clip.height != 0 {
- let mut clip_intersect = std::mem::zeroed();
- ffi::wlr_box_intersection(&mut clip_intersect, &frame, &requested.clip);
- frame = clip_intersect;
- }
-
- let scale = self.scale;
- let s = |v: i32| (v as f64 * scale) as i32;
- let radius_u16 = |v: i32| (v as f64 * scale).round().clamp(0.0, u16::MAX as f64) as u16;
-
- let rect = self.border.top;
- ffi::river_scene_node_set_position_if_changed(rect as *mut ffi::wlr_scene_node, s(frame.x), s(frame.y));
- ffi::river_scene_rect_set_size_if_changed(rect, s(frame.width), s(frame.height));
- ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
- ffi::river_scene_rect_set_corner_radius(rect, s(corner_radius + width));
- // Clipped region is node-relative: the content box, shifted by
- // wherever clipping moved the frame's origin.
- ffi::wlr_scene_rect_set_clipped_region(rect, ffi::clipped_region {
- area: ffi::wlr_box {
- x: s(-frame.x),
- y: s(-frame.y),
- width: s(content.width),
- height: s(content.height),
- },
- corners: ffi::fx_corner_radii {
- top_left: radius_u16(corner_radius),
- top_right: radius_u16(corner_radius),
- bottom_right: radius_u16(corner_radius),
- bottom_left: radius_u16(corner_radius),
- },
- });
-
- ffi::wlr_scene_node_set_enabled(rect as *mut ffi::wlr_scene_node, true);
- ffi::wlr_scene_node_set_enabled(self.border.left as *mut ffi::wlr_scene_node, false);
- ffi::wlr_scene_node_set_enabled(self.border.right as *mut ffi::wlr_scene_node, false);
- ffi::wlr_scene_node_set_enabled(self.border.bottom as *mut ffi::wlr_scene_node, false);
- }
-
#[allow(unused_assignments)]
pub unsafe fn apply_surface_clip(&mut self, a: *const ffi::wlr_box, b: *const ffi::wlr_box) {
let mut surface_clip = std::mem::zeroed::<ffi::wlr_box>();