1
0
Fork 0
forked from wry/wry

Run planned multiphase layout animations

This commit is contained in:
atagen 2026-05-21 18:37:00 +10:00
parent b50e8d5683
commit 13722429b4
2 changed files with 274 additions and 9 deletions

View file

@ -131,6 +131,7 @@ pub struct AnimationState {
pub duration_ms: Cell<u32>,
pub curve: Cell<AnimationCurve>,
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>,
exits: RefCell<Vec<ExitAnimation>>,
tick: CloneCell<Option<Rc<AnimationTick>>>,
}
@ -263,6 +264,7 @@ impl Default for AnimationState {
duration_ms: Cell::new(DEFAULT_DURATION_MS),
curve: Cell::new(AnimationCurve::from_config(3)),
windows: Default::default(),
phased: Default::default(),
exits: Default::default(),
tick: Default::default(),
}
@ -272,6 +274,7 @@ impl Default for AnimationState {
impl AnimationState {
pub fn clear(&self) {
self.windows.borrow_mut().clear();
self.phased.borrow_mut().clear();
self.exits.borrow_mut().clear();
if let Some(tick) = self.tick.take() {
tick.detach();
@ -290,20 +293,39 @@ impl AnimationState {
) -> bool {
if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().remove(&node_id);
return false;
}
let duration_nsec = duration_ms as u64 * 1_000_000;
let mut windows = self.windows.borrow_mut();
let (from, retained) = match windows.get(&node_id) {
Some(anim) if anim.to == new => return false,
Some(anim) => (anim.rect_at(now_nsec), anim.retained.clone().or(retained)),
None => (old, retained),
};
let mut from = old;
let mut retained = retained;
{
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id) {
if anim.final_rect == new {
return false;
}
from = anim.rect_at(now_nsec);
retained = anim.retained.clone().or(retained);
}
}
{
let windows = self.windows.borrow();
if let Some(anim) = windows.get(&node_id) {
if anim.to == new {
return false;
}
from = anim.rect_at(now_nsec);
retained = anim.retained.clone().or(retained);
}
}
if from == new {
windows.remove(&node_id);
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().remove(&node_id);
return false;
}
windows.insert(
self.phased.borrow_mut().remove(&node_id);
self.windows.borrow_mut().insert(
node_id,
WindowAnimation {
from,
@ -318,6 +340,47 @@ impl AnimationState {
true
}
pub fn set_phased_target(
&self,
node_id: NodeId,
phases: Vec<(Rect, Rect)>,
retained: Option<Rc<RetainedToplevel>>,
now_nsec: u64,
duration_ms: u32,
curve: AnimationCurve,
) -> bool {
if phases.is_empty() || duration_ms == 0 {
return false;
}
let Some((from, _)) = phases.first().copied() else {
return false;
};
let Some((_, final_rect)) = phases.last().copied() else {
return false;
};
if from.is_empty() || final_rect.is_empty() || from == final_rect {
return false;
}
let segments: Vec<_> = phases
.into_iter()
.map(|(from, to)| PhasedSegment { from, to })
.collect();
self.windows.borrow_mut().remove(&node_id);
self.phased.borrow_mut().insert(
node_id,
PhasedWindowAnimation {
segments,
start_nsec: now_nsec,
duration_nsec: duration_ms as u64 * 1_000_000,
curve,
last_damage: from,
final_rect,
retained,
},
);
true
}
pub fn set_spawn_in(
&self,
node_id: NodeId,
@ -375,6 +438,13 @@ impl AnimationState {
}
pub fn visual_rect(&self, node_id: NodeId, layout: Rect, now_nsec: u64) -> Rect {
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id)
&& !anim.done(now_nsec)
{
return anim.rect_at(now_nsec);
}
drop(phased);
let windows = self.windows.borrow();
match windows.get(&node_id) {
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
@ -387,6 +457,13 @@ impl AnimationState {
node_id: NodeId,
now_nsec: u64,
) -> Option<Rc<RetainedToplevel>> {
let phased = self.phased.borrow();
if let Some(anim) = phased.get(&node_id)
&& !anim.done(now_nsec)
{
return anim.retained.clone();
}
drop(phased);
let windows = self.windows.borrow();
match windows.get(&node_id) {
Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
@ -427,6 +504,18 @@ impl AnimationState {
any_active |= active;
active
});
self.phased.borrow_mut().retain(|_, anim| {
let current = anim.rect_at(now_nsec);
let damage = anim.last_damage.union(current).union(anim.final_rect);
damages.push(expand_damage_rect(
damage,
state.theme.sizes.border_width.get().max(0),
));
anim.last_damage = current;
let active = !anim.done(now_nsec);
any_active |= active;
active
});
self.exits.borrow_mut().retain_mut(|exit| {
let current = exit.rect_at(now_nsec);
let damage = exit.last_damage.union(current).union(exit.to);
@ -485,6 +574,45 @@ impl WindowAnimation {
}
}
struct PhasedWindowAnimation {
segments: Vec<PhasedSegment>,
start_nsec: u64,
duration_nsec: u64,
curve: AnimationCurve,
last_damage: Rect,
final_rect: Rect,
retained: Option<Rc<RetainedToplevel>>,
}
struct PhasedSegment {
from: Rect,
to: Rect,
}
impl PhasedWindowAnimation {
fn done(&self, now_nsec: u64) -> bool {
let total_duration = self
.duration_nsec
.saturating_mul(self.segments.len() as u64);
now_nsec.saturating_sub(self.start_nsec) >= total_duration
}
fn rect_at(&self, now_nsec: u64) -> Rect {
if self.duration_nsec == 0 {
return self.final_rect;
}
let elapsed = now_nsec.saturating_sub(self.start_nsec);
let phase = (elapsed / self.duration_nsec) as usize;
let Some(segment) = self.segments.get(phase) else {
return self.final_rect;
};
let phase_elapsed = elapsed % self.duration_nsec;
let t = (phase_elapsed as f64 / self.duration_nsec as f64).clamp(0.0, 1.0);
let t = self.curve.sample(t);
lerp_rect(segment.from, segment.to, t)
}
}
struct ExitAnimation {
from: Rect,
to: Rect,
@ -722,6 +850,53 @@ mod tests {
assert!(state.exit_frames(160_000_000).is_empty());
}
#[test]
fn phased_animation_uses_full_duration_per_phase() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
assert_eq!(state.visual_rect(id, c, 0), a);
assert_eq!(state.visual_rect(id, c, 50_000_000), lerp_rect(a, b, 0.5));
assert_eq!(state.visual_rect(id, c, 100_000_000), b);
assert_eq!(state.visual_rect(id, c, 150_000_000), lerp_rect(b, c, 0.5));
assert_eq!(state.visual_rect(id, c, 200_000_000), c);
}
#[test]
fn linear_retarget_interrupts_phased_animation_from_current_rect() {
let state = AnimationState::default();
let id = NodeId(1);
let a = Rect::new_sized_saturating(0, 0, 100, 100);
let b = Rect::new_sized_saturating(0, 0, 100, 50);
let c = Rect::new_sized_saturating(100, 0, 100, 50);
let d = Rect::new_sized_saturating(100, 100, 100, 50);
assert!(state.set_phased_target(
id,
vec![(a, b), (b, c)],
None,
0,
100,
AnimationCurve::Linear
));
let current = lerp_rect(a, b, 0.5);
assert!(state.set_target(id, a, d, None, 50_000_000, 100, AnimationCurve::Linear));
assert_eq!(state.visual_rect(id, d, 50_000_000), current);
assert_eq!(
state.visual_rect(id, d, 100_000_000),
lerp_rect(current, d, 0.5)
);
}
#[test]
fn unchanged_target_does_not_restart() {
let state = AnimationState::default();

View file

@ -4,7 +4,9 @@ use {
allocator::BufferObject,
animation::{
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
expand_damage_rect, spawn_in_start_rect,
expand_damage_rect,
multiphase::{MultiphaseRequest, MultiphaseWindow, plan_no_overlap},
spawn_in_start_rect,
},
async_engine::{AsyncEngine, SpawnedFuture},
backend::{
@ -157,6 +159,7 @@ use {
uapi::{OwnedFd, c},
};
#[derive(Clone)]
pub(crate) struct LayoutAnimationCandidate {
node_id: NodeId,
old: Rect,
@ -1583,11 +1586,98 @@ impl State {
return;
};
let now = self.now_nsec();
if self.start_multiphase_layout_animation(&candidates, now) {
return;
}
for candidate in candidates {
self.start_layout_animation_candidate(candidate, now);
}
}
fn start_multiphase_layout_animation(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
now_nsec: u64,
) -> bool {
if candidates.len() < 2 {
return false;
}
let windows: Vec<_> = candidates
.iter()
.map(|candidate| MultiphaseWindow {
node_id: candidate.node_id,
from: self
.animations
.visual_rect(candidate.node_id, candidate.old, now_nsec),
to: candidate.new,
})
.collect();
let Some(first) = windows.first() else {
return false;
};
let mut bounds = first.from.union(first.to);
for window in &windows[1..] {
bounds = bounds.union(window.from).union(window.to);
}
let Ok(plan) = plan_no_overlap(&MultiphaseRequest { bounds, windows }) else {
return false;
};
if plan.phases.is_empty() {
return false;
}
let mut entries = vec![];
for candidate in candidates {
let mut current =
self.animations
.visual_rect(candidate.node_id, candidate.old, now_nsec);
let mut damage = current.union(candidate.new);
let mut phases = vec![];
for phase in &plan.phases {
match phase
.steps
.iter()
.find(|step| step.node_id == candidate.node_id)
{
Some(step) => {
phases.push((step.from, step.to));
damage = damage.union(step.from).union(step.to);
current = step.to;
}
None => phases.push((current, current)),
}
}
if current != candidate.new {
return false;
}
let retained = self
.animations
.retained_snapshot(candidate.node_id, now_nsec)
.or_else(|| candidate.retained.clone());
entries.push((candidate.clone(), phases, damage, retained));
}
let mut started_any = false;
for (candidate, phases, damage, retained) in entries {
if self.animations.set_phased_target(
candidate.node_id,
phases,
retained,
now_nsec,
self.animations.duration_ms.get(),
candidate.curve,
) {
started_any = true;
self.damage(expand_damage_rect(
damage,
self.theme.sizes.border_width.get().max(0),
));
}
}
if started_any {
self.ensure_animation_tick();
}
started_any
}
pub fn queue_spawn_in_animation(
self: &Rc<Self>,
node_id: NodeId,