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
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.
- 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:

View file

@ -81,6 +81,33 @@ pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, Mu
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> {
if request.bounds.is_empty() {
return Err(MultiphaseError::EmptyBounds);
@ -380,6 +407,10 @@ fn overlaps(rects: impl IntoIterator<Item = Rect>) -> bool {
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) {
if from != to {
steps.push(MultiphaseStep { node_id, from, to });
@ -697,4 +728,48 @@ mod tests {
}]);
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::{
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
expand_damage_rect,
multiphase::{MultiphaseRequest, MultiphaseWindow, plan_no_overlap},
multiphase::{
MultiphaseRequest, MultiphaseWindow, partition_motion_groups, plan_no_overlap,
},
spawn_in_start_rect,
},
async_engine::{AsyncEngine, SpawnedFuture},
@ -1586,51 +1588,59 @@ 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),
.visual_rect(candidate.node_id, candidate.old, now),
to: candidate.new,
})
.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;
};
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);
}
let Ok(plan) = plan_no_overlap(&MultiphaseRequest { bounds, windows }) else {
let Ok(plan) = plan_no_overlap(&MultiphaseRequest {
bounds,
windows: request_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);
for &idx in group {
let candidate = &candidates[idx];
let window = windows[idx];
let mut current = window.from;
let mut damage = current.union(window.to);
let mut phases = vec![];
for phase in &plan.phases {
match phase
@ -1646,7 +1656,7 @@ impl State {
None => phases.push((current, current)),
}
}
if current != candidate.new {
if current != window.to {
return false;
}
let retained = self