From d0cc5dc3c7dcc2d4f91591c0682eaead7d286cc2 Mon Sep 17 00:00:00 2001 From: atagen Date: Thu, 21 May 2026 16:51:50 +1000 Subject: [PATCH] Animate command-driven floating changes --- docs/window-animations-plan.md | 6 ++-- src/config/handler.rs | 4 ++- src/ifs/wl_seat.rs | 3 ++ src/tree/float.rs | 65 ++++++++++++++++++++++++++-------- 4 files changed, 60 insertions(+), 18 deletions(-) diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index 82b10a4e..ad5bd329 100644 --- a/docs/window-animations-plan.md +++ b/docs/window-animations-plan.md @@ -82,8 +82,8 @@ Implementation shape: Initial scope: - Tiled reflow animation. -- Floating command-driven moves are deferred until after tiled reflow, spawn-in, - and float/tile transitions are validated. +- Floating command-driven moves and resizes are animated. Pointer and tablet + drag/resize paths still snap directly to the live cursor position. - Cross-output and cross-scale movements snap for now. - Linear mode may overlap windows during swaps. That is expected for the classic interpolation mode; no-overlap is Phase 3. @@ -101,6 +101,8 @@ Tests: - drag-driven floating movement bypasses animation - damage includes old, current, and final rects - command-driven tile-to-float and float-to-tile transitions use linear motion +- command-driven floating moves and resizes animate without affecting pointer + drag/resize behavior - pointer/header double-click unfloat bypasses the command-animation gate ## Phase 2: Retained Texture Freezing diff --git a/src/config/handler.rs b/src/config/handler.rs index 384137c7..138b4416 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -668,7 +668,9 @@ impl ConfigProxyHandler { fn handle_window_move(&self, window: Window, direction: Direction) -> Result<(), CphError> { self.state.with_layout_animations(|| { let window = self.get_window(window)?; - if let Some(c) = toplevel_parent_container(&*window) { + if let Some(float) = window.tl_data().float.get() { + float.move_by_direction(direction.into()); + } else if let Some(c) = toplevel_parent_container(&*window) { c.move_child(window, direction.into()); } Ok(()) diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 5fba889c..74ff4eda 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -936,6 +936,9 @@ impl WlSeatGlobal { { c.move_child(tl, direction); self.maybe_schedule_warp_mouse_to_focus(); + } else if let Some(float) = data.float.get() { + float.move_by_direction(direction); + self.maybe_schedule_warp_mouse_to_focus(); } } diff --git a/src/tree/float.rs b/src/tree/float.rs index 747c275e..f2f96681 100644 --- a/src/tree/float.rs +++ b/src/tree/float.rs @@ -31,6 +31,9 @@ use { }; tree_id!(FloatNodeId); + +const COMMAND_MOVE_DELTA: i32 = 100; + pub struct FloatNode { pub id: FloatNodeId, pub state: Rc, @@ -371,6 +374,51 @@ impl FloatNode { y2 += y1 - pos.y1(); } let new_pos = Rect::new_saturating(x1, y1, x2, y2); + self.set_position(new_pos); + } + + pub fn move_by_direction(self: &Rc, direction: Direction) { + let (dx, dy) = match direction { + Direction::Left => (-COMMAND_MOVE_DELTA, 0), + Direction::Down => (0, COMMAND_MOVE_DELTA), + Direction::Up => (0, -COMMAND_MOVE_DELTA), + Direction::Right => (COMMAND_MOVE_DELTA, 0), + Direction::Unspecified => return, + }; + self.set_position(self.position.get().move_(dx, dy)); + } + + fn body_for_outer(&self, outer: Rect) -> Rect { + let bw = self.state.theme.sizes.border_width.get(); + Rect::new_sized_saturating( + outer.x1() + bw, + outer.y1() + bw, + outer.width() - 2 * bw, + outer.height() - 2 * bw, + ) + } + + fn queue_position_animation(&self, old_pos: Rect, new_pos: Rect) { + self.state + .clone() + .queue_tiled_animation(self.id.into(), old_pos, new_pos, None); + let Some(child) = self.child.get() else { + return; + }; + self.state.clone().queue_tiled_animation( + child.node_id(), + self.body_for_outer(old_pos), + self.body_for_outer(new_pos), + child.tl_animation_snapshot(), + ); + } + + fn set_position(self: &Rc, new_pos: Rect) { + let pos = self.position.get(); + if new_pos == pos { + return; + } + self.queue_position_animation(pos, new_pos); self.position.set(new_pos); if self.visible.get() { self.state.damage(pos); @@ -799,13 +847,7 @@ impl ContainingNode for FloatNode { let bw = theme.sizes.border_width.get(); let (x, y) = (x - bw, y - bw); let pos = self.position.get(); - if pos.position() != (x, y) { - let new_pos = pos.at_point(x, y); - self.position.set(new_pos); - self.state.damage(pos); - self.state.damage(new_pos); - self.schedule_layout(); - } + self.set_position(pos.at_point(x, y)); } fn cnode_resize_child( @@ -836,14 +878,7 @@ impl ContainingNode for FloatNode { y2 = (v + bw).max(y1 + bw + bw); } let new_pos = Rect::new_saturating(x1, y1, x2, y2); - if new_pos != pos { - self.position.set(new_pos); - if self.visible.get() { - self.state.damage(pos); - self.state.damage(new_pos); - } - self.schedule_layout(); - } + self.set_position(new_pos); } fn cnode_pinned(&self) -> bool {