Add spawn-in window animations
This commit is contained in:
parent
7575f851fe
commit
18ffaef64d
6 changed files with 157 additions and 12 deletions
|
|
@ -12,8 +12,10 @@ be handled deliberately.
|
||||||
- Pointer drag and resize initiated by the mouse or tablet do not animate.
|
- Pointer drag and resize initiated by the mouse or tablet do not animate.
|
||||||
- Linear animations restart only for windows whose destination changes. Other
|
- Linear animations restart only for windows whose destination changes. Other
|
||||||
in-flight windows keep their existing timelines.
|
in-flight windows keep their existing timelines.
|
||||||
- Spawn-in uses scale and position. Spawn-out requires retained visual content
|
- Spawn-in uses scale and position for newly mapped tiled and floating app
|
||||||
and is deferred until the freezing layer exists.
|
windows. Layer-shell, overlay, override-redirect, and fullscreen surfaces do
|
||||||
|
not use this path. Spawn-out requires retained visual content after the live
|
||||||
|
node is gone and remains deferred.
|
||||||
- Command-driven tile-to-float and float-to-tile transitions may animate.
|
- Command-driven tile-to-float and float-to-tile transitions may animate.
|
||||||
Protocol drag/drop paths do not.
|
Protocol drag/drop paths do not.
|
||||||
- The no-overlap multiphase system is a separate phase after the linear path is
|
- The no-overlap multiphase system is a separate phase after the linear path is
|
||||||
|
|
@ -80,8 +82,8 @@ Implementation shape:
|
||||||
Initial scope:
|
Initial scope:
|
||||||
|
|
||||||
- Tiled reflow animation.
|
- Tiled reflow animation.
|
||||||
- Floating command-driven moves, tile-to-float, float-to-tile, and spawn-in are
|
- Floating command-driven moves, tile-to-float, and float-to-tile are deferred
|
||||||
deferred until after tiled reflow is validated.
|
until after tiled reflow and spawn-in are validated.
|
||||||
- Cross-output and cross-scale movements snap for now.
|
- Cross-output and cross-scale movements snap for now.
|
||||||
- Linear mode may overlap windows during swaps. That is expected for the classic
|
- Linear mode may overlap windows during swaps. That is expected for the classic
|
||||||
interpolation mode; no-overlap is Phase 3.
|
interpolation mode; no-overlap is Phase 3.
|
||||||
|
|
@ -89,7 +91,6 @@ Initial scope:
|
||||||
deferred, but animated windows must still be clipped to their presentation
|
deferred, but animated windows must still be clipped to their presentation
|
||||||
bounds and must preserve the existing stretch behavior for undersized contents.
|
bounds and must preserve the existing stretch behavior for undersized contents.
|
||||||
- No spawn-out.
|
- No spawn-out.
|
||||||
- No content freezing.
|
|
||||||
- No multiphase no-overlap planner.
|
- No multiphase no-overlap planner.
|
||||||
|
|
||||||
Tests:
|
Tests:
|
||||||
|
|
@ -107,6 +108,8 @@ Goal: freeze visual contents during movement and enable spawn-out.
|
||||||
Initial retained-record implementation status:
|
Initial retained-record implementation status:
|
||||||
|
|
||||||
- Tiled animation can retain GPU/dmabuf-backed XDG and Xwayland surface trees.
|
- 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.
|
||||||
- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the
|
- Retained records hold both `GfxTexture` and `SurfaceBuffer` references so the
|
||||||
existing buffer release/sync path remains authoritative.
|
existing buffer release/sync path remains authoritative.
|
||||||
- Single-pixel buffers can be retained as color records.
|
- Single-pixel buffers can be retained as color records.
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ use {
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_DURATION_MS: u32 = 160;
|
const DEFAULT_DURATION_MS: u32 = 160;
|
||||||
|
const SPAWN_IN_INITIAL_SCALE_NUMERATOR: i32 = 4;
|
||||||
|
const SPAWN_IN_INITIAL_SCALE_DENOMINATOR: i32 = 5;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum AnimationCurve {
|
pub enum AnimationCurve {
|
||||||
|
|
@ -224,6 +226,26 @@ impl AnimationState {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_spawn_in(
|
||||||
|
&self,
|
||||||
|
node_id: NodeId,
|
||||||
|
target: Rect,
|
||||||
|
retained: Option<Rc<RetainedToplevel>>,
|
||||||
|
now_nsec: u64,
|
||||||
|
duration_ms: u32,
|
||||||
|
) -> bool {
|
||||||
|
let start = spawn_in_start_rect(target);
|
||||||
|
self.set_target(
|
||||||
|
node_id,
|
||||||
|
start,
|
||||||
|
target,
|
||||||
|
retained,
|
||||||
|
now_nsec,
|
||||||
|
duration_ms,
|
||||||
|
AnimationCurve::Linear,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
|
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
|
||||||
let windows = self.windows.borrow();
|
let windows = self.windows.borrow();
|
||||||
match windows.get(&node_id) {
|
match windows.get(&node_id) {
|
||||||
|
|
@ -350,6 +372,23 @@ impl LatchListener for AnimationTick {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
|
||||||
|
fn scaled_dimension(value: i32) -> i32 {
|
||||||
|
let scaled = (value as i64 * SPAWN_IN_INITIAL_SCALE_NUMERATOR as i64
|
||||||
|
/ SPAWN_IN_INITIAL_SCALE_DENOMINATOR as i64) as i32;
|
||||||
|
scaled.clamp(1, value.max(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = scaled_dimension(target.width());
|
||||||
|
let height = scaled_dimension(target.height());
|
||||||
|
Rect::new_sized_saturating(
|
||||||
|
target.x1() + (target.width() - width) / 2,
|
||||||
|
target.y1() + (target.height() - height) / 2,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
|
fn lerp_rect(from: Rect, to: Rect, t: f64) -> Rect {
|
||||||
fn lerp(from: i32, to: i32, t: f64) -> i32 {
|
fn lerp(from: i32, to: i32, t: f64) -> i32 {
|
||||||
(from as f64 + (to as f64 - from as f64) * t).round() as i32
|
(from as f64 + (to as f64 - from as f64) * t).round() as i32
|
||||||
|
|
@ -437,4 +476,25 @@ mod tests {
|
||||||
Rect::new_sized_saturating(125, 0, 100, 100)
|
Rect::new_sized_saturating(125, 0, 100, 100)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_in_start_rect_is_centered_and_non_empty() {
|
||||||
|
let target = Rect::new_sized_saturating(10, 20, 100, 50);
|
||||||
|
assert_eq!(
|
||||||
|
spawn_in_start_rect(target),
|
||||||
|
Rect::new_sized_saturating(20, 25, 80, 40)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_in_uses_linear_curve() {
|
||||||
|
let state = AnimationState::default();
|
||||||
|
let id = NodeId(1);
|
||||||
|
let target = Rect::new_sized_saturating(10, 20, 100, 50);
|
||||||
|
assert!(state.set_spawn_in(id, target, None, 0, 160));
|
||||||
|
assert_eq!(
|
||||||
|
state.visual_rect(id, target, 80_000_000),
|
||||||
|
Rect::new_sized_saturating(15, 23, 90, 45)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use {
|
||||||
state::State,
|
state::State,
|
||||||
theme::{Color, CornerRadius},
|
theme::{Color, CornerRadius},
|
||||||
tree::{
|
tree::{
|
||||||
ContainerNode, DisplayNode, FloatNode, OutputNode, PlaceholderNode, ToplevelData,
|
ContainerNode, DisplayNode, FloatNode, Node, OutputNode, PlaceholderNode, ToplevelData,
|
||||||
ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
ToplevelNode, ToplevelNodeBase, WorkspaceNode, tab_bar::TabBar,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -207,8 +207,13 @@ impl Renderer<'_> {
|
||||||
if stacked.node_visible() {
|
if stacked.node_visible() {
|
||||||
self.base.sync();
|
self.base.sync();
|
||||||
let pos = stacked.node_absolute_position();
|
let pos = stacked.node_absolute_position();
|
||||||
if pos.intersects(&opos) {
|
let visual = self.state.animations.visual_rect(
|
||||||
let (x, y) = opos.translate(pos.x1(), pos.y1());
|
stacked.node_id(),
|
||||||
|
pos,
|
||||||
|
self.state.now_nsec(),
|
||||||
|
);
|
||||||
|
if visual.intersects(&opos) {
|
||||||
|
let (x, y) = opos.translate(visual.x1(), visual.y1());
|
||||||
stacked.node_render(self, x, y, None);
|
stacked.node_render(self, x, y, None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -983,6 +988,10 @@ impl Renderer<'_> {
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let pos = floating.position.get();
|
let pos = floating.position.get();
|
||||||
|
let visual =
|
||||||
|
self.state
|
||||||
|
.animations
|
||||||
|
.visual_rect(floating.node_id(), pos, self.state.now_nsec());
|
||||||
let theme = &self.state.theme;
|
let theme = &self.state.theme;
|
||||||
let bw = theme.sizes.border_width.get();
|
let bw = theme.sizes.border_width.get();
|
||||||
let bc = if floating.active.get() {
|
let bc = if floating.active.get() {
|
||||||
|
|
@ -991,16 +1000,26 @@ impl Renderer<'_> {
|
||||||
theme.colors.border.get()
|
theme.colors.border.get()
|
||||||
};
|
};
|
||||||
let cr = theme.corner_radius.get();
|
let cr = theme.corner_radius.get();
|
||||||
let outer = Rect::new_sized_saturating(0, 0, pos.width(), pos.height());
|
let outer = Rect::new_sized_saturating(0, 0, visual.width(), visual.height());
|
||||||
self.render_rounded_frame(outer, &bc, cr, bw, x, y);
|
self.render_rounded_frame(outer, &bc, cr, bw, x, y);
|
||||||
let body =
|
let body = Rect::new_sized_saturating(
|
||||||
Rect::new_sized_saturating(x + bw, y + bw, pos.width() - 2 * bw, pos.height() - 2 * bw);
|
x + bw,
|
||||||
|
y + bw,
|
||||||
|
visual.width() - 2 * bw,
|
||||||
|
visual.height() - 2 * bw,
|
||||||
|
);
|
||||||
let scissor_body = self.base.scale_rect(body);
|
let scissor_body = self.base.scale_rect(body);
|
||||||
|
self.stretch = if pos.width() != visual.width() || pos.height() != visual.height() {
|
||||||
|
Some(self.base.scale_point(body.width(), body.height()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
if !cr.is_zero() {
|
if !cr.is_zero() {
|
||||||
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
|
let inner_cr = self.scale_corner_radius(cr.expanded_by(-(bw as f32)));
|
||||||
self.corner_radius = Some(inner_cr);
|
self.corner_radius = Some(inner_cr);
|
||||||
}
|
}
|
||||||
child.node_render(self, body.x1(), body.y1(), Some(&scissor_body));
|
self.render_child_or_snapshot(&child, body.x1(), body.y1(), Some(&scissor_body));
|
||||||
|
self.stretch = None;
|
||||||
self.corner_radius = None;
|
self.corner_radius = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
28
src/state.rs
28
src/state.rs
|
|
@ -4,6 +4,7 @@ use {
|
||||||
allocator::BufferObject,
|
allocator::BufferObject,
|
||||||
animation::{
|
animation::{
|
||||||
AnimationCurve, AnimationState, AnimationTick, RetainedToplevel, expand_damage_rect,
|
AnimationCurve, AnimationState, AnimationTick, RetainedToplevel, expand_damage_rect,
|
||||||
|
spawn_in_start_rect,
|
||||||
},
|
},
|
||||||
async_engine::{AsyncEngine, SpawnedFuture},
|
async_engine::{AsyncEngine, SpawnedFuture},
|
||||||
backend::{
|
backend::{
|
||||||
|
|
@ -1516,6 +1517,33 @@ impl State {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn queue_spawn_in_animation(
|
||||||
|
self: &Rc<Self>,
|
||||||
|
node_id: NodeId,
|
||||||
|
target: Rect,
|
||||||
|
retained: Option<Rc<RetainedToplevel>>,
|
||||||
|
) {
|
||||||
|
if !self.animations.enabled.get() || target.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let start = spawn_in_start_rect(target);
|
||||||
|
let now = self.now_nsec();
|
||||||
|
let started = self.animations.set_spawn_in(
|
||||||
|
node_id,
|
||||||
|
target,
|
||||||
|
retained,
|
||||||
|
now,
|
||||||
|
self.animations.duration_ms.get(),
|
||||||
|
);
|
||||||
|
if started {
|
||||||
|
self.damage(expand_damage_rect(
|
||||||
|
start.union(target),
|
||||||
|
self.theme.sizes.border_width.get().max(0),
|
||||||
|
));
|
||||||
|
self.ensure_animation_tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_animations_enabled(&self, enabled: bool) {
|
pub fn set_animations_enabled(&self, enabled: bool) {
|
||||||
if self.animations.enabled.replace(enabled) && !enabled {
|
if self.animations.enabled.replace(enabled) && !enabled {
|
||||||
self.animations.clear();
|
self.animations.clear();
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,14 @@ impl FloatNode {
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
let pos = self.position.get();
|
let pos = self.position.get();
|
||||||
|
let spawn_in_pending = {
|
||||||
|
let data = child.tl_data();
|
||||||
|
data.spawn_in_pending.get() && data.kind.is_app_window() && !data.is_fullscreen.get()
|
||||||
|
};
|
||||||
|
if spawn_in_pending && self.visible.get() {
|
||||||
|
self.state
|
||||||
|
.queue_spawn_in_animation(self.id.into(), pos, None);
|
||||||
|
}
|
||||||
let theme = &self.state.theme;
|
let theme = &self.state.theme;
|
||||||
let bw = theme.sizes.border_width.get();
|
let bw = theme.sizes.border_width.get();
|
||||||
let cpos = Rect::new_sized_saturating(
|
let cpos = Rect::new_sized_saturating(
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
||||||
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
|
let parent_was_none = data.parent.set(Some(parent.clone())).is_none();
|
||||||
if parent_was_none {
|
if parent_was_none {
|
||||||
data.mapped_during_iteration.set(data.state.eng.iteration());
|
data.mapped_during_iteration.set(data.state.eng.iteration());
|
||||||
|
data.spawn_in_pending.set(data.kind.is_app_window());
|
||||||
data.property_changed(TL_CHANGED_NEW);
|
data.property_changed(TL_CHANGED_NEW);
|
||||||
}
|
}
|
||||||
let was_floating = data.parent_is_float.get();
|
let was_floating = data.parent_is_float.get();
|
||||||
|
|
@ -185,6 +186,7 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
||||||
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
|
||||||
let data = self.tl_data();
|
let data = self.tl_data();
|
||||||
let prev = data.desired_extents.replace(*rect);
|
let prev = data.desired_extents.replace(*rect);
|
||||||
|
let spawn_in_pending = data.spawn_in_pending.get();
|
||||||
let parent_is_mono = data
|
let parent_is_mono = data
|
||||||
.parent
|
.parent
|
||||||
.get()
|
.get()
|
||||||
|
|
@ -205,6 +207,22 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
|
||||||
self.tl_animation_snapshot(),
|
self.tl_animation_snapshot(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if spawn_in_pending
|
||||||
|
&& !rect.is_empty()
|
||||||
|
&& data.visible.get()
|
||||||
|
&& !data.is_fullscreen.get()
|
||||||
|
&& data.kind.is_app_window()
|
||||||
|
&& !self.node_is_container()
|
||||||
|
{
|
||||||
|
data.state.clone().queue_spawn_in_animation(
|
||||||
|
data.node_id,
|
||||||
|
*rect,
|
||||||
|
self.tl_animation_snapshot(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if spawn_in_pending && !rect.is_empty() {
|
||||||
|
data.spawn_in_pending.set(false);
|
||||||
|
}
|
||||||
if prev.size() != rect.size() {
|
if prev.size() != rect.size() {
|
||||||
for sc in data.jay_screencasts.lock().values() {
|
for sc in data.jay_screencasts.lock().values() {
|
||||||
sc.schedule_realloc_or_reconfigure();
|
sc.schedule_realloc_or_reconfigure();
|
||||||
|
|
@ -403,6 +421,13 @@ impl ToplevelType {
|
||||||
ToplevelType::XWindow { .. } => window::X_WINDOW,
|
ToplevelType::XWindow { .. } => window::X_WINDOW,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_app_window(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
ToplevelType::XdgToplevel(_) | ToplevelType::XWindow(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ToplevelData {
|
pub struct ToplevelData {
|
||||||
|
|
@ -425,6 +450,7 @@ pub struct ToplevelData {
|
||||||
pub title: RefCell<String>,
|
pub title: RefCell<String>,
|
||||||
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
|
pub parent: CloneCell<Option<Rc<dyn ContainingNode>>>,
|
||||||
pub mapped_during_iteration: Cell<u64>,
|
pub mapped_during_iteration: Cell<u64>,
|
||||||
|
pub spawn_in_pending: Cell<bool>,
|
||||||
pub pos: Cell<Rect>,
|
pub pos: Cell<Rect>,
|
||||||
pub desired_extents: Cell<Rect>,
|
pub desired_extents: Cell<Rect>,
|
||||||
pub seat_state: NodeSeatState,
|
pub seat_state: NodeSeatState,
|
||||||
|
|
@ -488,6 +514,7 @@ impl ToplevelData {
|
||||||
title: RefCell::new(title),
|
title: RefCell::new(title),
|
||||||
parent: Default::default(),
|
parent: Default::default(),
|
||||||
mapped_during_iteration: Cell::new(0),
|
mapped_during_iteration: Cell::new(0),
|
||||||
|
spawn_in_pending: Cell::new(false),
|
||||||
pos: Default::default(),
|
pos: Default::default(),
|
||||||
desired_extents: Default::default(),
|
desired_extents: Default::default(),
|
||||||
seat_state: Default::default(),
|
seat_state: Default::default(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue