1
0
Fork 0
forked from wry/wry

Fallback layout animations by motion group

This commit is contained in:
atagen 2026-05-21 18:46:10 +10:00
parent a516b2e721
commit 4ee2c324e1
3 changed files with 115 additions and 27 deletions

View file

@ -221,6 +221,9 @@ Current pure planner status:
orientations: peer/container space scales first, the extracted child moves orientations: peer/container space scales first, the extracted child moves
only after space exists, and orthogonal growth happens in the final phase. only after space exists, and orthogonal growth happens in the final phase.
- Every produced plan is sampled for overlap at each phase before it is accepted. - Every produced plan is sampled for overlap at each phase before it is accepted.
- Live layout batches are partitioned by overlapping motion bounds, so unrelated
groups can still use multiphase animation when another group falls back to
linear motion.
Tests: Tests:

View file

@ -81,6 +81,33 @@ pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, Mu
Err(MultiphaseError::NoPlan) Err(MultiphaseError::NoPlan)
} }
pub(crate) fn partition_motion_groups(windows: &[MultiphaseWindow]) -> Vec<Vec<usize>> {
let mut groups = vec![];
let mut seen = vec![false; windows.len()];
for start in 0..windows.len() {
if seen[start] {
continue;
}
seen[start] = true;
let mut group = vec![];
let mut pending = vec![start];
while let Some(idx) = pending.pop() {
group.push(idx);
let bounds = motion_bounds(windows[idx]);
for other in 0..windows.len() {
if seen[other] || !bounds.intersects(&motion_bounds(windows[other])) {
continue;
}
seen[other] = true;
pending.push(other);
}
}
group.sort_unstable();
groups.push(group);
}
groups
}
fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> { fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError> {
if request.bounds.is_empty() { if request.bounds.is_empty() {
return Err(MultiphaseError::EmptyBounds); return Err(MultiphaseError::EmptyBounds);
@ -380,6 +407,10 @@ fn overlaps(rects: impl IntoIterator<Item = Rect>) -> bool {
false false
} }
fn motion_bounds(window: MultiphaseWindow) -> Rect {
window.from.union(window.to)
}
fn push_step(steps: &mut Vec<MultiphaseStep>, node_id: NodeId, from: Rect, to: Rect) { fn push_step(steps: &mut Vec<MultiphaseStep>, node_id: NodeId, from: Rect, to: Rect) {
if from != to { if from != to {
steps.push(MultiphaseStep { node_id, from, to }); steps.push(MultiphaseStep { node_id, from, to });
@ -697,4 +728,48 @@ mod tests {
}]); }]);
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
} }
#[test]
fn motion_groups_split_disjoint_layout_changes() {
let windows = vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(100, 0, 200, 100),
},
MultiphaseWindow {
node_id: id(2),
from: rect(100, 0, 200, 100),
to: rect(0, 0, 100, 100),
},
MultiphaseWindow {
node_id: id(3),
from: rect(300, 0, 400, 100),
to: rect(400, 0, 500, 100),
},
];
assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1], vec![2]]);
}
#[test]
fn motion_groups_are_transitive() {
let windows = vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(80, 0, 180, 100),
},
MultiphaseWindow {
node_id: id(2),
from: rect(170, 0, 270, 100),
to: rect(250, 0, 350, 100),
},
MultiphaseWindow {
node_id: id(3),
from: rect(90, 0, 180, 100),
to: rect(180, 0, 260, 100),
},
];
assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1, 2]]);
}
} }

View file

@ -5,7 +5,9 @@ use {
animation::{ animation::{
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel, AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
expand_damage_rect, expand_damage_rect,
multiphase::{MultiphaseRequest, MultiphaseWindow, plan_no_overlap}, multiphase::{
MultiphaseRequest, MultiphaseWindow, partition_motion_groups, plan_no_overlap,
},
spawn_in_start_rect, spawn_in_start_rect,
}, },
async_engine::{AsyncEngine, SpawnedFuture}, async_engine::{AsyncEngine, SpawnedFuture},
@ -1586,51 +1588,59 @@ 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 {
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 let windows: Vec<_> = candidates
.iter() .iter()
.map(|candidate| MultiphaseWindow { .map(|candidate| MultiphaseWindow {
node_id: candidate.node_id, node_id: candidate.node_id,
from: self from: self
.animations .animations
.visual_rect(candidate.node_id, candidate.old, now_nsec), .visual_rect(candidate.node_id, candidate.old, now),
to: candidate.new, to: candidate.new,
}) })
.collect(); .collect();
let Some(first) = windows.first() else { for group in partition_motion_groups(&windows) {
if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) {
continue;
}
for idx in group {
self.start_layout_animation_candidate(candidates[idx].clone(), now);
}
}
}
fn start_multiphase_layout_animation(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
now_nsec: u64,
) -> bool {
if group.len() < 2 {
return false;
}
let request_windows: Vec<_> = group.iter().map(|&idx| windows[idx]).collect();
let Some(first) = request_windows.first() else {
return false; return false;
}; };
let mut bounds = first.from.union(first.to); let mut bounds = first.from.union(first.to);
for window in &windows[1..] { for window in &request_windows[1..] {
bounds = bounds.union(window.from).union(window.to); bounds = bounds.union(window.from).union(window.to);
} }
let Ok(plan) = plan_no_overlap(&MultiphaseRequest { bounds, windows }) else { let Ok(plan) = plan_no_overlap(&MultiphaseRequest {
bounds,
windows: request_windows,
}) else {
return false; return false;
}; };
if plan.phases.is_empty() { if plan.phases.is_empty() {
return false; return false;
} }
let mut entries = vec![]; let mut entries = vec![];
for candidate in candidates { for &idx in group {
let mut current = let candidate = &candidates[idx];
self.animations let window = windows[idx];
.visual_rect(candidate.node_id, candidate.old, now_nsec); let mut current = window.from;
let mut damage = current.union(candidate.new); let mut damage = current.union(window.to);
let mut phases = vec![]; let mut phases = vec![];
for phase in &plan.phases { for phase in &plan.phases {
match phase match phase
@ -1646,7 +1656,7 @@ impl State {
None => phases.push((current, current)), None => phases.push((current, current)),
} }
} }
if current != candidate.new { if current != window.to {
return false; return false;
} }
let retained = self let retained = self