diff --git a/docs/window-animations-plan.md b/docs/window-animations-plan.md index 0cb05965..82b10a4e 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, tile-to-float, and float-to-tile are deferred - until after tiled reflow and spawn-in are validated. +- Floating command-driven moves are deferred until after tiled reflow, spawn-in, + and float/tile transitions are validated. - 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. @@ -100,6 +100,8 @@ Tests: - unchanged in-flight windows keep their original timeline - 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 +- pointer/header double-click unfloat bypasses the command-animation gate ## Phase 2: Retained Texture Freezing @@ -110,6 +112,8 @@ Initial retained-record implementation status: - Tiled animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees. - Spawn-in animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees for both tiled windows and floating child contents. +- Tile-to-float and float-to-tile transitions retain GPU/dmabuf-backed child + contents while the presentation geometry changes. - Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the existing buffer release/sync path remains authoritative. - Single-pixel buffers can be retained as color records. diff --git a/src/compositor.rs b/src/compositor.rs index d8fb4027..93600f27 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -363,6 +363,7 @@ fn start_compositor2( animations: Default::default(), layout_animations_requested: Default::default(), layout_animations_active: Default::default(), + layout_animation_curve_override: Default::default(), suppress_animations_for_next_layout: Default::default(), toplevels: Default::default(), const_40hz_latch: Default::default(), diff --git a/src/config/handler.rs b/src/config/handler.rs index 0e9436c5..384137c7 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1990,9 +1990,11 @@ impl ConfigProxyHandler { } fn handle_set_seat_floating(&self, seat: Seat, floating: bool) -> Result<(), CphError> { - let seat = self.get_seat(seat)?; - seat.set_floating(floating); - Ok(()) + self.state.with_linear_layout_animations(|| { + let seat = self.get_seat(seat)?; + seat.set_floating(floating); + Ok(()) + }) } fn handle_get_window_floating(&self, window: Window) -> Result<(), CphError> { @@ -2004,9 +2006,11 @@ impl ConfigProxyHandler { } fn handle_set_window_floating(&self, window: Window, floating: bool) -> Result<(), CphError> { - let window = self.get_window(window)?; - toplevel_set_floating(&self.state, window, floating); - Ok(()) + self.state.with_linear_layout_animations(|| { + let window = self.get_window(window)?; + toplevel_set_floating(&self.state, window, floating); + Ok(()) + }) } fn handle_add_pollable(self: &Rc, fd: i32) -> Result<(), CphError> { diff --git a/src/state.rs b/src/state.rs index 8bb78826..a94c8a37 100644 --- a/src/state.rs +++ b/src/state.rs @@ -270,6 +270,7 @@ pub struct State { pub animations: AnimationState, pub layout_animations_requested: Cell, pub layout_animations_active: Cell, + pub layout_animation_curve_override: Cell>, pub suppress_animations_for_next_layout: Cell, pub toplevels: CopyHashMap>, pub const_40hz_latch: EventSource, @@ -854,7 +855,7 @@ impl State { mut height: i32, workspace: &Rc, abs_pos: Option<(i32, i32)>, - ) { + ) -> Rc { width += 2 * self.theme.sizes.border_width.get(); height += 2 * self.theme.sizes.border_width.get() + self.theme.title_plus_underline_height(); @@ -885,8 +886,9 @@ impl State { } Rect::new_sized_saturating(x1, y1, width, height) }; - FloatNode::new(self, workspace, position, node.clone()); + let float = FloatNode::new(self, workspace, position, node.clone()); self.focus_after_map(node, self.seat_queue.last().as_deref()); + float } fn focus_after_map(&self, node: Rc, seat: Option<&Rc>) { @@ -1125,6 +1127,7 @@ impl State { self.animations.clear(); self.layout_animations_requested.set(false); self.layout_animations_active.set(false); + self.layout_animation_curve_override.set(None); self.suppress_animations_for_next_layout.set(false); self.render_ctx_watchers.clear(); self.workspace_watchers.clear(); @@ -1478,6 +1481,31 @@ impl State { old: Rect, new: Rect, retained: Option>, + ) { + let curve = self + .layout_animation_curve_override + .get() + .unwrap_or_else(|| self.animations.curve.get()); + self.queue_layout_animation(node_id, old, new, retained, curve); + } + + pub fn queue_linear_layout_animation( + self: &Rc, + node_id: NodeId, + old: Rect, + new: Rect, + retained: Option>, + ) { + self.queue_layout_animation(node_id, old, new, retained, AnimationCurve::Linear); + } + + fn queue_layout_animation( + self: &Rc, + node_id: NodeId, + old: Rect, + new: Rect, + retained: Option>, + curve: AnimationCurve, ) { if !self.animations.enabled.get() || !self.layout_animations_active.get() @@ -1506,7 +1534,7 @@ impl State { retained, now, self.animations.duration_ms.get(), - self.animations.curve.get(), + curve, ); if started { self.damage(expand_damage_rect( @@ -1570,6 +1598,19 @@ impl State { res } + pub fn with_linear_layout_animations(&self, f: impl FnOnce() -> T) -> T { + let prev_requested = self.layout_animations_requested.replace(true); + let prev_active = self.layout_animations_active.replace(true); + let prev_curve = self + .layout_animation_curve_override + .replace(Some(AnimationCurve::Linear)); + let res = f(); + self.layout_animations_requested.set(prev_requested); + self.layout_animations_active.set(prev_active); + self.layout_animation_curve_override.set(prev_curve); + res + } + fn ensure_animation_tick(self: &Rc) { if self.animations.tick_is_active() { return; diff --git a/src/tree/toplevel.rs b/src/tree/toplevel.rs index 499fedeb..dc9927f9 100644 --- a/src/tree/toplevel.rs +++ b/src/tree/toplevel.rs @@ -1096,6 +1096,26 @@ pub fn toplevel_create_split(state: &Rc, tl: Rc, axis: } } +fn float_outer_for_body(state: &State, body: Rect) -> Rect { + let bw = state.theme.sizes.border_width.get(); + Rect::new_sized_saturating( + body.x1() - bw, + body.y1() - bw, + body.width() + 2 * bw, + body.height() + 2 * bw, + ) +} + +fn float_body_for_outer(state: &State, outer: Rect) -> Rect { + let bw = state.theme.sizes.border_width.get(); + Rect::new_sized_saturating( + outer.x1() + bw, + outer.y1() + bw, + outer.width() - 2 * bw, + outer.height() - 2 * bw, + ) +} + pub fn toplevel_set_floating(state: &Rc, tl: Rc, floating: bool) { let data = tl.tl_data(); if data.is_fullscreen.get() { @@ -1112,9 +1132,20 @@ pub fn toplevel_set_floating(state: &Rc, tl: Rc, floati parent.cnode_remove_child2(&*tl, true); state.map_tiled(tl); } else if let Some(ws) = data.workspace.get() { + let node_id = data.node_id; + let old_body = + state + .animations + .visual_rect(node_id, tl.node_absolute_position(), state.now_nsec()); + let old_outer = float_outer_for_body(state, old_body); + let retained = tl.tl_animation_snapshot(); parent.cnode_remove_child2(&*tl, true); let (width, height) = data.float_size(&ws); - state.map_floating(tl, width, height, &ws, None); + let floater = state.map_floating(tl, width, height, &ws, None); + let new_outer = floater.position.get(); + let new_body = float_body_for_outer(state, new_outer); + state.queue_linear_layout_animation(floater.node_id(), old_outer, new_outer, None); + state.queue_linear_layout_animation(node_id, old_body, new_body, retained); } }