Run planned multiphase layout animations
This commit is contained in:
parent
b50e8d5683
commit
13722429b4
2 changed files with 274 additions and 9 deletions
191
src/animation.rs
191
src/animation.rs
|
|
@ -131,6 +131,7 @@ pub struct AnimationState {
|
||||||
pub duration_ms: Cell<u32>,
|
pub duration_ms: Cell<u32>,
|
||||||
pub curve: Cell<AnimationCurve>,
|
pub curve: Cell<AnimationCurve>,
|
||||||
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
|
windows: RefCell<AHashMap<NodeId, WindowAnimation>>,
|
||||||
|
phased: RefCell<AHashMap<NodeId, PhasedWindowAnimation>>,
|
||||||
exits: RefCell<Vec<ExitAnimation>>,
|
exits: RefCell<Vec<ExitAnimation>>,
|
||||||
tick: CloneCell<Option<Rc<AnimationTick>>>,
|
tick: CloneCell<Option<Rc<AnimationTick>>>,
|
||||||
}
|
}
|
||||||
|
|
@ -263,6 +264,7 @@ impl Default for AnimationState {
|
||||||
duration_ms: Cell::new(DEFAULT_DURATION_MS),
|
duration_ms: Cell::new(DEFAULT_DURATION_MS),
|
||||||
curve: Cell::new(AnimationCurve::from_config(3)),
|
curve: Cell::new(AnimationCurve::from_config(3)),
|
||||||
windows: Default::default(),
|
windows: Default::default(),
|
||||||
|
phased: Default::default(),
|
||||||
exits: Default::default(),
|
exits: Default::default(),
|
||||||
tick: Default::default(),
|
tick: Default::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -272,6 +274,7 @@ impl Default for AnimationState {
|
||||||
impl AnimationState {
|
impl AnimationState {
|
||||||
pub fn clear(&self) {
|
pub fn clear(&self) {
|
||||||
self.windows.borrow_mut().clear();
|
self.windows.borrow_mut().clear();
|
||||||
|
self.phased.borrow_mut().clear();
|
||||||
self.exits.borrow_mut().clear();
|
self.exits.borrow_mut().clear();
|
||||||
if let Some(tick) = self.tick.take() {
|
if let Some(tick) = self.tick.take() {
|
||||||
tick.detach();
|
tick.detach();
|
||||||
|
|
@ -290,20 +293,39 @@ impl AnimationState {
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
|
if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
|
||||||
self.windows.borrow_mut().remove(&node_id);
|
self.windows.borrow_mut().remove(&node_id);
|
||||||
|
self.phased.borrow_mut().remove(&node_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let duration_nsec = duration_ms as u64 * 1_000_000;
|
let duration_nsec = duration_ms as u64 * 1_000_000;
|
||||||
let mut windows = self.windows.borrow_mut();
|
let mut from = old;
|
||||||
let (from, retained) = match windows.get(&node_id) {
|
let mut retained = retained;
|
||||||
Some(anim) if anim.to == new => return false,
|
{
|
||||||
Some(anim) => (anim.rect_at(now_nsec), anim.retained.clone().or(retained)),
|
let phased = self.phased.borrow();
|
||||||
None => (old, retained),
|
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 {
|
if from == new {
|
||||||
windows.remove(&node_id);
|
self.windows.borrow_mut().remove(&node_id);
|
||||||
|
self.phased.borrow_mut().remove(&node_id);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
windows.insert(
|
self.phased.borrow_mut().remove(&node_id);
|
||||||
|
self.windows.borrow_mut().insert(
|
||||||
node_id,
|
node_id,
|
||||||
WindowAnimation {
|
WindowAnimation {
|
||||||
from,
|
from,
|
||||||
|
|
@ -318,6 +340,47 @@ impl AnimationState {
|
||||||
true
|
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(
|
pub fn set_spawn_in(
|
||||||
&self,
|
&self,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
|
|
@ -375,6 +438,13 @@ impl AnimationState {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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();
|
let windows = self.windows.borrow();
|
||||||
match windows.get(&node_id) {
|
match windows.get(&node_id) {
|
||||||
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
|
Some(anim) if !anim.done(now_nsec) => anim.rect_at(now_nsec),
|
||||||
|
|
@ -387,6 +457,13 @@ impl AnimationState {
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
now_nsec: u64,
|
now_nsec: u64,
|
||||||
) -> Option<Rc<RetainedToplevel>> {
|
) -> 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();
|
let windows = self.windows.borrow();
|
||||||
match windows.get(&node_id) {
|
match windows.get(&node_id) {
|
||||||
Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
|
Some(anim) if !anim.done(now_nsec) => anim.retained.clone(),
|
||||||
|
|
@ -427,6 +504,18 @@ impl AnimationState {
|
||||||
any_active |= active;
|
any_active |= 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| {
|
self.exits.borrow_mut().retain_mut(|exit| {
|
||||||
let current = exit.rect_at(now_nsec);
|
let current = exit.rect_at(now_nsec);
|
||||||
let damage = exit.last_damage.union(current).union(exit.to);
|
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 {
|
struct ExitAnimation {
|
||||||
from: Rect,
|
from: Rect,
|
||||||
to: Rect,
|
to: Rect,
|
||||||
|
|
@ -722,6 +850,53 @@ mod tests {
|
||||||
assert!(state.exit_frames(160_000_000).is_empty());
|
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]
|
#[test]
|
||||||
fn unchanged_target_does_not_restart() {
|
fn unchanged_target_does_not_restart() {
|
||||||
let state = AnimationState::default();
|
let state = AnimationState::default();
|
||||||
|
|
|
||||||
92
src/state.rs
92
src/state.rs
|
|
@ -4,7 +4,9 @@ use {
|
||||||
allocator::BufferObject,
|
allocator::BufferObject,
|
||||||
animation::{
|
animation::{
|
||||||
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
|
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},
|
async_engine::{AsyncEngine, SpawnedFuture},
|
||||||
backend::{
|
backend::{
|
||||||
|
|
@ -157,6 +159,7 @@ use {
|
||||||
uapi::{OwnedFd, c},
|
uapi::{OwnedFd, c},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub(crate) struct LayoutAnimationCandidate {
|
pub(crate) struct LayoutAnimationCandidate {
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
old: Rect,
|
old: Rect,
|
||||||
|
|
@ -1583,11 +1586,98 @@ impl State {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let now = self.now_nsec();
|
let now = self.now_nsec();
|
||||||
|
if self.start_multiphase_layout_animation(&candidates, now) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
for candidate in candidates {
|
for candidate in candidates {
|
||||||
self.start_layout_animation_candidate(candidate, now);
|
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(
|
pub fn queue_spawn_in_animation(
|
||||||
self: &Rc<Self>,
|
self: &Rc<Self>,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue