desktop grid client
git clone https://git.lucas.co/cce-grid.git
fix(items): handles on the hovered image only
While adjust mode is on, only the item under the pointer (`hover_item`)
draws its corner discs — the compositor shows its ring on the hovered
window the same way — instead of every item at once. The off-screen
move cce-ui synthesizes on pointer leave clears it, so the handles drop
the moment the pointer is on the background or a window; a grabbed item
keeps them through its raise.
Verified in a shadow in overview: the image's discs appear when hovered
and vanish on the background, with no window ring either way.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 9 ++++++---
src/main.rs | 48 ++++++++++++++++++++++++++++++++++++++----------
2 files changed, 44 insertions(+), 13 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index a630494..5b4f6f6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -81,9 +81,12 @@ grown by a drop while it is open.
window-adjust mode — overview, or Super held — comes and goes) is the one
thing this client subscribes to (`spawn_adjust_listener`, a plain std
thread on the status socket, reconnecting with backoff): it never holds
-keyboard focus, so it cannot read Super for itself. While on, every item
-draws four corner discs (`handle_discs`) in the same `style.surface.border`
-colours and `handle_width` as the windows' handles, the hovered one lit;
+keyboard focus, so it cannot read Super for itself. While on, the item
+UNDER THE POINTER (`hover_item` — the compositor shows its ring on the
+hovered window the same way) draws four corner discs (`handle_discs`) in
+the same `style.surface.border` colours and `handle_width` as the windows'
+handles, the hovered one lit; a leave arrives as the off-screen move cce-ui
+synthesizes and clears it;
a press on a disc starts a `Resize`, which scales the image
PROPORTIONALLY (the mean of the two edge ratios the drag asks for),
anchored on the opposite corner, and saves the sidecar on release. A press
diff --git a/src/main.rs b/src/main.rs
index 6a2a81d..75956dd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -150,8 +150,14 @@ struct GridApp {
/// held in VIRTUAL units so the drag survives a pan or zoom mid-gesture.
dragging: Option<Drag>,
/// The compositor's window-adjust mode (`adjust` status topic): while
- /// on, every item shows its corner handles and a press on one resizes.
+ /// on, the item under the pointer shows its corner handles and a press
+ /// on one resizes.
adjust: bool,
+ /// The item under the pointer (body or handle) — the one that shows its
+ /// handles, like the compositor's ring on the hovered window. Cleared by
+ /// the off-screen move cce-ui synthesizes on pointer leave, so it drops
+ /// the moment the pointer is on the background or a window.
+ hover_item: Option<usize>,
/// The corner handle under the pointer, drawn in the hover colour.
hover: Option<(usize, Corner)>,
/// An in-flight corner resize, delta-driven like `Drag`.
@@ -496,12 +502,12 @@ impl GridApp {
continue;
}
pc.image(id, rect, 1.0);
- // Window-adjust mode: the four corner handles, in the same
- // colours as the windows' handles, the hovered one lit. Sized in
- // virtual units, so they scale with the canvas rather than
- // holding a screen size the way the compositor's do — this
+ // Window-adjust mode: the hovered item's four corner handles, in
+ // the same colours as the windows' handles, the hovered one lit.
+ // Sized in virtual units, so they scale with the canvas rather
+ // than holding a screen size the way the compositor's do — this
// client never learns the camera zoom.
- if self.adjust {
+ if self.adjust && self.hover_item == Some(index) {
let (r, discs) = handle_discs(item, st.handle_width);
for (corner, cx, cy) in discs {
let color = if self.hover == Some((index, corner)) {
@@ -517,14 +523,25 @@ impl GridApp {
}
impl GridApp {
- /// The corner handle under a virtual-canvas point, topmost item first,
- /// with a unit of slack around the disc's antialiased rim.
+ /// The topmost item under a virtual-canvas point.
+ fn item_at(&self, vx: f64, vy: f64) -> Option<usize> {
+ self.items
+ .iter()
+ .rposition(|(i, _)| vx >= i.x && vx < i.x + i.w && vy >= i.y && vy < i.y + i.h)
+ }
+
+ /// The corner handle under a virtual-canvas point, with a unit of slack
+ /// around the disc's antialiased rim. Only the hovered item's handles
+ /// are up, so only its discs can be hit.
fn corner_at(&self, vx: f64, vy: f64) -> Option<(usize, Corner)> {
if !self.adjust {
return None;
}
let diameter = style().handle_width;
for (index, (item, _)) in self.items.iter().enumerate().rev() {
+ if self.hover_item != Some(index) {
+ continue;
+ }
let (r, discs) = handle_discs(item, diameter);
let reach = (r + 1.0) * (r + 1.0);
for (corner, cx, cy) in discs {
@@ -557,6 +574,7 @@ impl Application for GridApp {
sender: _sender,
dragging: None,
adjust: false,
+ hover_item: None,
hover: None,
resizing: None,
applied_relief: None,
@@ -612,6 +630,7 @@ impl Application for GridApp {
}
self.resizing = None;
self.hover = None;
+ self.hover_item = None;
let (item, id) = self.items.remove(pos);
if let Some(id) = id {
cce_ui::vk::free_image(id);
@@ -825,9 +844,16 @@ impl Application for GridApp {
}
let Some(drag) = self.dragging.as_mut() else {
- // Idle motion: light the handle under the pointer.
+ // Idle motion: the item under the pointer gets the handles, and
+ // the handle under it lights. A leave arrives as an off-screen
+ // position and clears both.
let vx = p.x + pos.x as f64 / s;
let vy = p.y + pos.y as f64 / s;
+ let item = self.item_at(vx, vy);
+ if item != self.hover_item {
+ self.hover_item = item;
+ *needs_rebuild = true;
+ }
let hover = self.corner_at(vx, vy);
if hover != self.hover {
self.hover = hover;
@@ -918,9 +944,11 @@ impl Application for GridApp {
vx >= i.x && vx < i.x + i.w && vy >= i.y && vy < i.y + i.h
})?;
// Raise it: the one you grabbed should be the one you see,
- // and the next press should find it first.
+ // and the next press should find it first. The handles
+ // follow it to its new index.
let item = self.items.remove(hit);
self.items.push(item);
+ self.hover_item = Some(self.items.len() - 1);
self.dragging = Some(Drag {
index: self.items.len() - 1,
last_pos: (pos.x, pos.y),