1
0
Fork 0
forked from wry/wry

state: split animation orchestration

This commit is contained in:
kossLAN 2026-05-29 19:39:44 -04:00
parent fb31d5115d
commit 19c2265400
No known key found for this signature in database
2 changed files with 866 additions and 851 deletions

View file

@ -1,22 +1,14 @@
mod animations;
mod connectors;
pub(crate) use animations::LayoutAnimationCandidate;
pub use connectors::{ConnectorData, DrmDevData, OutputData};
use {
crate::{
acceptor::Acceptor,
allocator::BufferObject,
animation::{
AnimationCurve, AnimationState, AnimationStyle, AnimationTick, RetainedExitLayer,
RetainedToplevel,
expand_damage_rect,
multiphase::{
MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest,
MultiphaseWindow, MultiphaseWindowHierarchy,
partition_motion_groups, plan_no_overlap_with_diagnostics, validate_phase_paths,
},
spawn_in_start_rect,
},
animation::{AnimationCurve, AnimationState, AnimationStyle},
async_engine::{AsyncEngine, SpawnedFuture},
backend::{
Backend, BackendConnectorStateSerials, BackendEvent, ConnectorId, ConnectorIds,
@ -108,7 +100,7 @@ use {
time::Time,
tree::{
ContainerNode, ContainerSplit, Direction, DisplayNode, FindTreeUsecase, FloatNode,
FoundNode, LatchListener, Node, NodeId, NodeIds, NodeVisitorBase, OutputNode,
FoundNode, LatchListener, Node, NodeIds, NodeVisitorBase, OutputNode,
PlaceholderNode, TearingMode, TileState, ToplevelData, ToplevelIdentifier,
ToplevelNode, ToplevelNodeBase, Transform, VrrMode, WorkspaceDisplayOrder,
WorkspaceNode, WorkspaceNodeId, WsMoveConfig, generic_node_visitor, move_ws_to_output,
@ -157,98 +149,6 @@ use {
uapi::{OwnedFd, c},
};
#[derive(Clone)]
pub(crate) struct LayoutAnimationCandidate {
node_id: NodeId,
old: Rect,
new: Rect,
curve: AnimationCurve,
style: AnimationStyle,
hierarchy: MultiphaseWindowHierarchy,
}
fn coalesce_layout_animation_candidates(
candidates: Vec<LayoutAnimationCandidate>,
) -> Vec<LayoutAnimationCandidate> {
let mut merged: Vec<LayoutAnimationCandidate> = vec![];
for candidate in candidates {
if let Some(existing) = merged
.iter_mut()
.find(|existing| existing.node_id == candidate.node_id)
{
existing.new = candidate.new;
existing.curve = candidate.curve;
existing.style = candidate.style;
existing.hierarchy = MultiphaseWindowHierarchy::new(
existing.hierarchy.source,
candidate.hierarchy.target,
);
} else {
merged.push(candidate);
}
}
merged
}
fn layout_animation_group_uses_plain(
candidates: &[LayoutAnimationCandidate],
group: &[usize],
) -> bool {
group
.iter()
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
}
fn bridged_retarget_plan(
request: &MultiphaseRequest,
candidates: &[LayoutAnimationCandidate],
group: &[usize],
bridge_paths: &[Vec<(Rect, Rect)>],
bridge_phase_count: usize,
follow_phases: &[MultiphasePhase],
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
let mut paths = vec![];
for (group_pos, &idx) in group.iter().enumerate() {
let candidate = &candidates[idx];
let window = request.windows[group_pos];
let Some(bridge_path) = bridge_paths.get(group_pos) else {
return Err(MultiphasePlanFailure::NoPattern);
};
let mut path = bridge_path.clone();
let mut current = path
.last()
.map(|(_, to)| *to)
.unwrap_or(window.from);
while path.len() < bridge_phase_count {
path.push((current, current));
}
if current != candidate.old {
return Err(MultiphasePlanFailure::NoPattern);
}
for phase in follow_phases {
match phase
.steps
.iter()
.find(|step| step.node_id == candidate.node_id)
{
Some(step) => {
if step.from != current {
return Err(MultiphasePlanFailure::NoPattern);
}
path.push((step.from, step.to));
current = step.to;
}
None => path.push((current, current)),
}
}
if current != window.to {
return Err(MultiphasePlanFailure::NoPattern);
}
paths.push(path);
}
validate_phase_paths(request, &paths)
}
pub struct State {
pub pid: c::pid_t,
pub kb_ctx: KbvmContext,
@ -1471,532 +1371,6 @@ impl State {
self.eng.now().msec()
}
pub fn queue_tiled_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
) {
let curve = self
.layout_animation_curve_override
.get()
.unwrap_or_else(|| self.animations.curve.get());
self.queue_layout_animation(
node_id,
old,
new,
curve,
MultiphaseWindowHierarchy::default(),
);
}
pub fn queue_tiled_animation_with_hierarchy(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
hierarchy: MultiphaseWindowHierarchy,
) {
let curve = self
.layout_animation_curve_override
.get()
.unwrap_or_else(|| self.animations.curve.get());
self.queue_layout_animation(node_id, old, new, curve, hierarchy);
}
pub fn queue_linear_layout_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
) {
self.queue_layout_animation(
node_id,
old,
new,
AnimationCurve::Linear,
MultiphaseWindowHierarchy::default(),
);
}
fn queue_layout_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
curve: AnimationCurve,
hierarchy: MultiphaseWindowHierarchy,
) {
if !self.animations.enabled.get()
|| !self.layout_animations_active.get()
|| self.suppress_animations_for_next_layout.get()
{
return;
}
let (old_output, old_scale) = {
let (x, y) = old.center();
let (output, _, _) = self.find_closest_output(x, y);
(output.id, output.global.persistent.scale.get())
};
let (new_output, new_scale) = {
let (x, y) = new.center();
let (output, _, _) = self.find_closest_output(x, y);
(output.id, output.global.persistent.scale.get())
};
if old_output != new_output || old_scale != new_scale {
return;
}
let candidate = LayoutAnimationCandidate {
node_id,
old,
new,
curve,
style: self
.layout_animation_style_override
.get()
.unwrap_or_else(|| self.animations.style.get()),
hierarchy,
};
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
batch.push(candidate);
return;
}
self.start_layout_animation_candidate(candidate, self.now_nsec());
}
fn start_layout_animation_candidate(
self: &Rc<Self>,
candidate: LayoutAnimationCandidate,
now_nsec: u64,
) {
let started = self.animations.set_target(
candidate.node_id,
candidate.old,
candidate.new,
None,
now_nsec,
self.animations.duration_ms.get(),
candidate.curve,
);
if started {
self.damage(expand_damage_rect(
candidate.old.union(candidate.new),
self.theme.sizes.border_width.get().max(0),
));
self.ensure_animation_tick();
}
}
pub fn begin_layout_animation_batch(&self) {
self.layout_animation_batch
.borrow_mut()
.get_or_insert_with(Vec::new);
}
pub fn finish_layout_animation_batch(self: &Rc<Self>) {
let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else {
return;
};
let candidates = coalesce_layout_animation_candidates(candidates);
if candidates.is_empty() {
return;
}
let now = self.now_nsec();
let windows: Vec<_> = candidates
.iter()
.map(|candidate| {
MultiphaseWindow::with_hierarchy(
candidate.node_id,
self.animations
.visual_rect(candidate.node_id, candidate.old, now),
candidate.new,
candidate.hierarchy,
)
})
.collect();
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
if layout_animation_group_uses_plain(&candidates, &group) {
for idx in group {
self.start_layout_animation_candidate(candidates[idx].clone(), now);
}
continue;
}
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 layout_animation_clearance(&self) -> i32 {
let border = self.theme.sizes.border_width.get().max(0);
let gap = self.theme.sizes.gap.get().max(0);
if gap == 0 { border } else { gap + 2 * border }
}
fn start_multiphase_layout_animation(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
now_nsec: u64,
) -> bool {
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 &request_windows[1..] {
bounds = bounds.union(window.from).union(window.to);
}
let request = MultiphaseRequest {
bounds,
windows: request_windows,
clearance: self.layout_animation_clearance(),
};
if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) {
return true;
}
if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) {
return true;
}
let plan = match plan_no_overlap_with_diagnostics(&request) {
Ok(plan) => plan,
Err(diagnostic) => {
log::debug!(
"falling back to plain layout animation for group {:?}: {:?}",
group,
diagnostic
);
return false;
}
};
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_existing_phased_retarget(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
request: &MultiphaseRequest,
now_nsec: u64,
) -> bool {
let mut paths = vec![];
for &idx in group {
let candidate = &candidates[idx];
let window = windows[idx];
let Some(path) =
self.animations
.phased_route_to(candidate.node_id, window.to, now_nsec)
else {
return false;
};
paths.push(path);
}
let plan = match validate_phase_paths(request, &paths) {
Ok(plan) => plan,
Err(error) => {
log::debug!(
"existing phased retarget rejected for group {:?}: {:?}",
group,
error
);
return false;
}
};
log::debug!("retargeting active phased animation for group {:?}", group);
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_bridged_phased_retarget(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
request: &MultiphaseRequest,
now_nsec: u64,
) -> bool {
let mut bridge_paths = vec![];
let mut bridge_phase_count = 0;
let mut has_bridge = false;
for &idx in group {
let candidate = &candidates[idx];
let window = windows[idx];
if window.from == candidate.old {
bridge_paths.push(vec![]);
continue;
}
let Some(path) =
self.animations
.phased_route_to(candidate.node_id, candidate.old, now_nsec)
else {
return false;
};
if !path.is_empty() {
has_bridge = true;
bridge_phase_count = bridge_phase_count.max(path.len());
}
bridge_paths.push(path);
}
if !has_bridge {
return false;
}
let settled_windows: Vec<_> = group
.iter()
.map(|&idx| {
let candidate = &candidates[idx];
MultiphaseWindow::with_hierarchy(
candidate.node_id,
candidate.old,
candidate.new,
candidate.hierarchy,
)
})
.collect();
let Some(first) = settled_windows.first() else {
return false;
};
let mut bounds = first.from.union(first.to);
for window in &settled_windows[1..] {
bounds = bounds.union(window.from).union(window.to);
}
let settled_request = MultiphaseRequest {
bounds,
windows: settled_windows,
clearance: self.layout_animation_clearance(),
};
let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) {
Ok(plan) => plan,
Err(diagnostic) => {
log::debug!(
"bridged phased retarget follow-up rejected for group {:?}: {:?}",
group,
diagnostic
);
return false;
}
};
let plan = match bridged_retarget_plan(
request,
candidates,
group,
&bridge_paths,
bridge_phase_count,
&follow_plan.phases,
) {
Ok(plan) => plan,
Err(error) => {
log::debug!(
"bridged phased retarget rejected for group {:?}: {:?}",
group,
error
);
return false;
}
};
log::debug!("bridging active phased animation for group {:?}", group);
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_multiphase_plan(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
plan_phases: &[crate::animation::multiphase::MultiphasePhase],
now_nsec: u64,
) -> bool {
if plan_phases.is_empty() {
return false;
}
let mut entries = vec![];
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
.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 != window.to {
return false;
}
entries.push((candidate.clone(), phases, damage));
}
let mut started_any = false;
for (candidate, phases, damage) in entries {
if self.animations.set_phased_target(
candidate.node_id,
phases,
None,
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,
target: Rect,
) {
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,
None,
now,
self.animations.duration_ms.get(),
self.animations.curve.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 queue_spawn_out_animation(
self: &Rc<Self>,
from: Rect,
frame_inset: i32,
retained: Rc<RetainedToplevel>,
active: bool,
layer: RetainedExitLayer,
) {
if !self.animations.enabled.get() || from.is_empty() {
return;
}
let now = self.now_nsec();
let started = self.animations.set_spawn_out(
from,
frame_inset,
retained,
active,
layer,
now,
self.animations.duration_ms.get(),
self.animations.curve.get(),
);
if started {
self.damage(expand_damage_rect(
from,
self.theme.sizes.border_width.get().max(0),
));
self.ensure_animation_tick();
}
}
pub fn set_animations_enabled(&self, enabled: bool) {
if self.animations.enabled.replace(enabled) && !enabled {
self.animations.clear();
self.damage(self.root.extents.get());
}
}
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
self.animations.duration_ms.set(duration_ms);
}
pub fn set_animation_curve(&self, curve: u32) {
self.animations
.curve
.set(AnimationCurve::from_config(curve));
}
pub fn set_animation_style(&self, style: u32) -> bool {
let Some(style) = AnimationStyle::from_config(style) else {
return false;
};
self.animations.style.set(style);
true
}
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
return false;
};
self.animations.curve.set(curve);
true
}
pub fn with_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_requested = self.layout_animations_requested.replace(true);
let prev_active = self.layout_animations_active.replace(true);
let res = f();
self.layout_animations_requested.set(prev_requested);
self.layout_animations_active.set(prev_active);
res
}
pub fn with_linear_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_requested = self.layout_animations_requested.replace(true);
let prev_active = self.layout_animations_active.replace(true);
let prev_curve = self
.layout_animation_curve_override
.replace(Some(AnimationCurve::Linear));
let prev_style = self
.layout_animation_style_override
.replace(Some(AnimationStyle::Plain));
let res = f();
self.layout_animations_requested.set(prev_requested);
self.layout_animations_active.set(prev_active);
self.layout_animation_curve_override.set(prev_curve);
self.layout_animation_style_override.set(prev_style);
res
}
fn ensure_animation_tick(self: &Rc<Self>) {
if self.animations.tick_is_active() {
return;
}
let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect();
if outputs.is_empty() {
return;
}
let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak));
for output in &outputs {
tick.attach(output);
}
self.animations.set_tick(tick);
for output in &outputs {
self.damage(output.global.pos.get());
}
}
pub fn output_extents_changed(&self) {
self.root.update_extents();
for seat in self.globals.seats.lock().values() {
@ -2508,227 +1882,6 @@ impl State {
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::animation::multiphase::MultiphaseHierarchyPosition,
};
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
Rect::new_saturating(x1, y1, x2, y2)
}
fn hierarchy(
source: MultiphaseHierarchyPosition,
target: MultiphaseHierarchyPosition,
) -> MultiphaseWindowHierarchy {
MultiphaseWindowHierarchy::new(source, target)
}
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
candidate_rects(
node_id,
rect(0, 0, 100, 100),
rect(100, 0, 200, 100),
style,
)
}
fn candidate_rects(
node_id: u32,
old: Rect,
new: Rect,
style: AnimationStyle,
) -> LayoutAnimationCandidate {
LayoutAnimationCandidate {
node_id: NodeId(node_id),
old,
new,
curve: AnimationCurve::Linear,
style,
hierarchy: MultiphaseWindowHierarchy::default(),
}
}
#[test]
fn plain_style_candidate_forces_group_plain() {
let candidates = vec![
candidate(1, AnimationStyle::Multiphase),
candidate(2, AnimationStyle::Plain),
];
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
}
#[test]
fn bridged_retarget_handles_second_rotation_interrupt() {
let a_left = rect(0, 0, 100, 100);
let c_mid = rect(100, 0, 200, 100);
let c_left = a_left;
let a_mid = c_mid;
let c_current = rect(150, 50, 250, 100);
let c_mid_lane = rect(100, 50, 200, 100);
let candidates = vec![
candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase),
candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase),
];
let request = MultiphaseRequest {
bounds: rect(0, 0, 250, 100),
windows: vec![
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
MultiphaseWindow::new(NodeId(3), c_current, c_left),
],
clearance: 0,
};
let settled_request = MultiphaseRequest {
bounds: rect(0, 0, 200, 100),
windows: vec![
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
MultiphaseWindow::new(NodeId(3), c_mid, c_left),
],
clearance: 0,
};
let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap();
let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]];
let plan = bridged_retarget_plan(
&request,
&candidates,
&[0, 1],
&bridge_paths,
2,
&follow_plan.phases,
)
.unwrap();
assert!(plan
.phases
.iter()
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1))));
assert!(plan
.phases
.iter()
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3))));
}
#[test]
fn layout_animation_candidates_coalesce_duplicate_nodes() {
let source = MultiphaseHierarchyPosition {
parent: Some(NodeId(10).into()),
depth: 2,
sibling_index: Some(1),
..Default::default()
};
let intermediate = MultiphaseHierarchyPosition {
parent: Some(NodeId(11).into()),
depth: 1,
sibling_index: Some(0),
..Default::default()
};
let target = MultiphaseHierarchyPosition {
parent: Some(NodeId(12).into()),
depth: 0,
sibling_index: Some(2),
..Default::default()
};
let second_source = MultiphaseHierarchyPosition {
parent: Some(NodeId(20).into()),
depth: 1,
sibling_index: Some(0),
..Default::default()
};
let second_target = MultiphaseHierarchyPosition {
parent: Some(NodeId(20).into()),
depth: 1,
sibling_index: Some(1),
..Default::default()
};
let candidates = vec![
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(source, intermediate),
},
LayoutAnimationCandidate {
node_id: NodeId(2),
old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(second_source, second_target),
},
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 80, 100),
new: rect(0, 0, 60, 100),
curve: AnimationCurve::from_config(4),
style: AnimationStyle::Plain,
hierarchy: hierarchy(intermediate, target),
},
];
let merged = coalesce_layout_animation_candidates(candidates);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].node_id, NodeId(1));
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 60, 100));
assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[0].hierarchy, hierarchy(source, target));
assert_eq!(merged[1].node_id, NodeId(2));
assert_eq!(merged[1].old, rect(100, 0, 200, 100));
assert_eq!(merged[1].new, rect(120, 0, 220, 100));
assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target));
}
#[test]
fn layout_animation_candidates_keep_coalesced_layout_noops() {
let hierarchy = MultiphaseWindowHierarchy::default();
let candidates = vec![
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy,
},
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 80, 100),
new: rect(0, 0, 100, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Plain,
hierarchy,
},
LayoutAnimationCandidate {
node_id: NodeId(2),
old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy,
},
];
let merged = coalesce_layout_animation_candidates(candidates);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].node_id, NodeId(1));
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 100, 100));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[1].node_id, NodeId(2));
}
}
#[derive(Debug, Error)]
pub enum ShmScreencopyError {
#[error("There is no render context")]

862
src/state/animations.rs Normal file
View file

@ -0,0 +1,862 @@
use {
crate::{
animation::{
AnimationCurve, AnimationStyle, AnimationTick, RetainedExitLayer, RetainedToplevel,
expand_damage_rect,
multiphase::{
MultiphasePhase, MultiphasePlan, MultiphasePlanFailure, MultiphaseRequest,
MultiphaseWindow, MultiphaseWindowHierarchy, partition_motion_groups,
plan_no_overlap_with_diagnostics, validate_phase_paths,
},
spawn_in_start_rect,
},
rect::Rect,
tree::NodeId,
},
std::rc::Rc,
};
use super::State;
#[derive(Clone)]
pub(crate) struct LayoutAnimationCandidate {
node_id: NodeId,
old: Rect,
new: Rect,
curve: AnimationCurve,
style: AnimationStyle,
hierarchy: MultiphaseWindowHierarchy,
}
fn coalesce_layout_animation_candidates(
candidates: Vec<LayoutAnimationCandidate>,
) -> Vec<LayoutAnimationCandidate> {
let mut merged: Vec<LayoutAnimationCandidate> = vec![];
for candidate in candidates {
if let Some(existing) = merged
.iter_mut()
.find(|existing| existing.node_id == candidate.node_id)
{
existing.new = candidate.new;
existing.curve = candidate.curve;
existing.style = candidate.style;
existing.hierarchy = MultiphaseWindowHierarchy::new(
existing.hierarchy.source,
candidate.hierarchy.target,
);
} else {
merged.push(candidate);
}
}
merged
}
fn layout_animation_group_uses_plain(
candidates: &[LayoutAnimationCandidate],
group: &[usize],
) -> bool {
group
.iter()
.any(|&idx| candidates[idx].style == AnimationStyle::Plain)
}
fn bridged_retarget_plan(
request: &MultiphaseRequest,
candidates: &[LayoutAnimationCandidate],
group: &[usize],
bridge_paths: &[Vec<(Rect, Rect)>],
bridge_phase_count: usize,
follow_phases: &[MultiphasePhase],
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
let mut paths = vec![];
for (group_pos, &idx) in group.iter().enumerate() {
let candidate = &candidates[idx];
let window = request.windows[group_pos];
let Some(bridge_path) = bridge_paths.get(group_pos) else {
return Err(MultiphasePlanFailure::NoPattern);
};
let mut path = bridge_path.clone();
let mut current = path
.last()
.map(|(_, to)| *to)
.unwrap_or(window.from);
while path.len() < bridge_phase_count {
path.push((current, current));
}
if current != candidate.old {
return Err(MultiphasePlanFailure::NoPattern);
}
for phase in follow_phases {
match phase
.steps
.iter()
.find(|step| step.node_id == candidate.node_id)
{
Some(step) => {
if step.from != current {
return Err(MultiphasePlanFailure::NoPattern);
}
path.push((step.from, step.to));
current = step.to;
}
None => path.push((current, current)),
}
}
if current != window.to {
return Err(MultiphasePlanFailure::NoPattern);
}
paths.push(path);
}
validate_phase_paths(request, &paths)
}
impl State {
pub fn queue_tiled_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
) {
let curve = self
.layout_animation_curve_override
.get()
.unwrap_or_else(|| self.animations.curve.get());
self.queue_layout_animation(
node_id,
old,
new,
curve,
MultiphaseWindowHierarchy::default(),
);
}
pub fn queue_tiled_animation_with_hierarchy(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
hierarchy: MultiphaseWindowHierarchy,
) {
let curve = self
.layout_animation_curve_override
.get()
.unwrap_or_else(|| self.animations.curve.get());
self.queue_layout_animation(node_id, old, new, curve, hierarchy);
}
pub fn queue_linear_layout_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
) {
self.queue_layout_animation(
node_id,
old,
new,
AnimationCurve::Linear,
MultiphaseWindowHierarchy::default(),
);
}
fn queue_layout_animation(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
curve: AnimationCurve,
hierarchy: MultiphaseWindowHierarchy,
) {
if !self.animations.enabled.get()
|| !self.layout_animations_active.get()
|| self.suppress_animations_for_next_layout.get()
{
return;
}
let (old_output, old_scale) = {
let (x, y) = old.center();
let (output, _, _) = self.find_closest_output(x, y);
(output.id, output.global.persistent.scale.get())
};
let (new_output, new_scale) = {
let (x, y) = new.center();
let (output, _, _) = self.find_closest_output(x, y);
(output.id, output.global.persistent.scale.get())
};
if old_output != new_output || old_scale != new_scale {
return;
}
let candidate = LayoutAnimationCandidate {
node_id,
old,
new,
curve,
style: self
.layout_animation_style_override
.get()
.unwrap_or_else(|| self.animations.style.get()),
hierarchy,
};
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
batch.push(candidate);
return;
}
self.start_layout_animation_candidate(candidate, self.now_nsec());
}
fn start_layout_animation_candidate(
self: &Rc<Self>,
candidate: LayoutAnimationCandidate,
now_nsec: u64,
) {
let started = self.animations.set_target(
candidate.node_id,
candidate.old,
candidate.new,
None,
now_nsec,
self.animations.duration_ms.get(),
candidate.curve,
);
if started {
self.damage(expand_damage_rect(
candidate.old.union(candidate.new),
self.theme.sizes.border_width.get().max(0),
));
self.ensure_animation_tick();
}
}
pub fn begin_layout_animation_batch(&self) {
self.layout_animation_batch
.borrow_mut()
.get_or_insert_with(Vec::new);
}
pub fn finish_layout_animation_batch(self: &Rc<Self>) {
let Some(candidates) = self.layout_animation_batch.borrow_mut().take() else {
return;
};
let candidates = coalesce_layout_animation_candidates(candidates);
if candidates.is_empty() {
return;
}
let now = self.now_nsec();
let windows: Vec<_> = candidates
.iter()
.map(|candidate| {
MultiphaseWindow::with_hierarchy(
candidate.node_id,
self.animations
.visual_rect(candidate.node_id, candidate.old, now),
candidate.new,
candidate.hierarchy,
)
})
.collect();
for group in partition_motion_groups(&windows, self.layout_animation_clearance()) {
if layout_animation_group_uses_plain(&candidates, &group) {
for idx in group {
self.start_layout_animation_candidate(candidates[idx].clone(), now);
}
continue;
}
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 layout_animation_clearance(&self) -> i32 {
let border = self.theme.sizes.border_width.get().max(0);
let gap = self.theme.sizes.gap.get().max(0);
if gap == 0 { border } else { gap + 2 * border }
}
fn start_multiphase_layout_animation(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
now_nsec: u64,
) -> bool {
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 &request_windows[1..] {
bounds = bounds.union(window.from).union(window.to);
}
let request = MultiphaseRequest {
bounds,
windows: request_windows,
clearance: self.layout_animation_clearance(),
};
if self.start_existing_phased_retarget(candidates, windows, group, &request, now_nsec) {
return true;
}
if self.start_bridged_phased_retarget(candidates, windows, group, &request, now_nsec) {
return true;
}
let plan = match plan_no_overlap_with_diagnostics(&request) {
Ok(plan) => plan,
Err(diagnostic) => {
log::debug!(
"falling back to plain layout animation for group {:?}: {:?}",
group,
diagnostic
);
return false;
}
};
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_existing_phased_retarget(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
request: &MultiphaseRequest,
now_nsec: u64,
) -> bool {
let mut paths = vec![];
for &idx in group {
let candidate = &candidates[idx];
let window = windows[idx];
let Some(path) =
self.animations
.phased_route_to(candidate.node_id, window.to, now_nsec)
else {
return false;
};
paths.push(path);
}
let plan = match validate_phase_paths(request, &paths) {
Ok(plan) => plan,
Err(error) => {
log::debug!(
"existing phased retarget rejected for group {:?}: {:?}",
group,
error
);
return false;
}
};
log::debug!("retargeting active phased animation for group {:?}", group);
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_bridged_phased_retarget(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
request: &MultiphaseRequest,
now_nsec: u64,
) -> bool {
let mut bridge_paths = vec![];
let mut bridge_phase_count = 0;
let mut has_bridge = false;
for &idx in group {
let candidate = &candidates[idx];
let window = windows[idx];
if window.from == candidate.old {
bridge_paths.push(vec![]);
continue;
}
let Some(path) =
self.animations
.phased_route_to(candidate.node_id, candidate.old, now_nsec)
else {
return false;
};
if !path.is_empty() {
has_bridge = true;
bridge_phase_count = bridge_phase_count.max(path.len());
}
bridge_paths.push(path);
}
if !has_bridge {
return false;
}
let settled_windows: Vec<_> = group
.iter()
.map(|&idx| {
let candidate = &candidates[idx];
MultiphaseWindow::with_hierarchy(
candidate.node_id,
candidate.old,
candidate.new,
candidate.hierarchy,
)
})
.collect();
let Some(first) = settled_windows.first() else {
return false;
};
let mut bounds = first.from.union(first.to);
for window in &settled_windows[1..] {
bounds = bounds.union(window.from).union(window.to);
}
let settled_request = MultiphaseRequest {
bounds,
windows: settled_windows,
clearance: self.layout_animation_clearance(),
};
let follow_plan = match plan_no_overlap_with_diagnostics(&settled_request) {
Ok(plan) => plan,
Err(diagnostic) => {
log::debug!(
"bridged phased retarget follow-up rejected for group {:?}: {:?}",
group,
diagnostic
);
return false;
}
};
let plan = match bridged_retarget_plan(
request,
candidates,
group,
&bridge_paths,
bridge_phase_count,
&follow_plan.phases,
) {
Ok(plan) => plan,
Err(error) => {
log::debug!(
"bridged phased retarget rejected for group {:?}: {:?}",
group,
error
);
return false;
}
};
log::debug!("bridging active phased animation for group {:?}", group);
self.start_multiphase_plan(candidates, windows, group, &plan.phases, now_nsec)
}
fn start_multiphase_plan(
self: &Rc<Self>,
candidates: &[LayoutAnimationCandidate],
windows: &[MultiphaseWindow],
group: &[usize],
plan_phases: &[crate::animation::multiphase::MultiphasePhase],
now_nsec: u64,
) -> bool {
if plan_phases.is_empty() {
return false;
}
let mut entries = vec![];
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
.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 != window.to {
return false;
}
entries.push((candidate.clone(), phases, damage));
}
let mut started_any = false;
for (candidate, phases, damage) in entries {
if self.animations.set_phased_target(
candidate.node_id,
phases,
None,
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,
target: Rect,
) {
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,
None,
now,
self.animations.duration_ms.get(),
self.animations.curve.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 queue_spawn_out_animation(
self: &Rc<Self>,
from: Rect,
frame_inset: i32,
retained: Rc<RetainedToplevel>,
active: bool,
layer: RetainedExitLayer,
) {
if !self.animations.enabled.get() || from.is_empty() {
return;
}
let now = self.now_nsec();
let started = self.animations.set_spawn_out(
from,
frame_inset,
retained,
active,
layer,
now,
self.animations.duration_ms.get(),
self.animations.curve.get(),
);
if started {
self.damage(expand_damage_rect(
from,
self.theme.sizes.border_width.get().max(0),
));
self.ensure_animation_tick();
}
}
pub fn set_animations_enabled(&self, enabled: bool) {
if self.animations.enabled.replace(enabled) && !enabled {
self.animations.clear();
self.damage(self.root.extents.get());
}
}
pub fn set_animation_duration_ms(&self, duration_ms: u32) {
self.animations.duration_ms.set(duration_ms);
}
pub fn set_animation_curve(&self, curve: u32) {
self.animations
.curve
.set(AnimationCurve::from_config(curve));
}
pub fn set_animation_style(&self, style: u32) -> bool {
let Some(style) = AnimationStyle::from_config(style) else {
return false;
};
self.animations.style.set(style);
true
}
pub fn set_animation_cubic_bezier(&self, x1: f32, y1: f32, x2: f32, y2: f32) -> bool {
let Some(curve) = AnimationCurve::from_cubic_bezier(x1, y1, x2, y2) else {
return false;
};
self.animations.curve.set(curve);
true
}
pub fn with_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_requested = self.layout_animations_requested.replace(true);
let prev_active = self.layout_animations_active.replace(true);
let res = f();
self.layout_animations_requested.set(prev_requested);
self.layout_animations_active.set(prev_active);
res
}
pub fn with_linear_layout_animations<T>(&self, f: impl FnOnce() -> T) -> T {
let prev_requested = self.layout_animations_requested.replace(true);
let prev_active = self.layout_animations_active.replace(true);
let prev_curve = self
.layout_animation_curve_override
.replace(Some(AnimationCurve::Linear));
let prev_style = self
.layout_animation_style_override
.replace(Some(AnimationStyle::Plain));
let res = f();
self.layout_animations_requested.set(prev_requested);
self.layout_animations_active.set(prev_active);
self.layout_animation_curve_override.set(prev_curve);
self.layout_animation_style_override.set(prev_style);
res
}
fn ensure_animation_tick(self: &Rc<Self>) {
if self.animations.tick_is_active() {
return;
}
let outputs: Vec<_> = self.root.outputs.lock().values().cloned().collect();
if outputs.is_empty() {
return;
}
let tick = Rc::new_cyclic(|weak| AnimationTick::new(self, weak));
for output in &outputs {
tick.attach(output);
}
self.animations.set_tick(tick);
for output in &outputs {
self.damage(output.global.pos.get());
}
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::animation::multiphase::MultiphaseHierarchyPosition,
};
fn rect(x1: i32, y1: i32, x2: i32, y2: i32) -> Rect {
Rect::new_saturating(x1, y1, x2, y2)
}
fn hierarchy(
source: MultiphaseHierarchyPosition,
target: MultiphaseHierarchyPosition,
) -> MultiphaseWindowHierarchy {
MultiphaseWindowHierarchy::new(source, target)
}
fn candidate(node_id: u32, style: AnimationStyle) -> LayoutAnimationCandidate {
candidate_rects(
node_id,
rect(0, 0, 100, 100),
rect(100, 0, 200, 100),
style,
)
}
fn candidate_rects(
node_id: u32,
old: Rect,
new: Rect,
style: AnimationStyle,
) -> LayoutAnimationCandidate {
LayoutAnimationCandidate {
node_id: NodeId(node_id),
old,
new,
curve: AnimationCurve::Linear,
style,
hierarchy: MultiphaseWindowHierarchy::default(),
}
}
#[test]
fn plain_style_candidate_forces_group_plain() {
let candidates = vec![
candidate(1, AnimationStyle::Multiphase),
candidate(2, AnimationStyle::Plain),
];
assert!(!layout_animation_group_uses_plain(&candidates, &[0]));
assert!(layout_animation_group_uses_plain(&candidates, &[0, 1]));
}
#[test]
fn bridged_retarget_handles_second_rotation_interrupt() {
let a_left = rect(0, 0, 100, 100);
let c_mid = rect(100, 0, 200, 100);
let c_left = a_left;
let a_mid = c_mid;
let c_current = rect(150, 50, 250, 100);
let c_mid_lane = rect(100, 50, 200, 100);
let candidates = vec![
candidate_rects(1, a_left, a_mid, AnimationStyle::Multiphase),
candidate_rects(3, c_mid, c_left, AnimationStyle::Multiphase),
];
let request = MultiphaseRequest {
bounds: rect(0, 0, 250, 100),
windows: vec![
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
MultiphaseWindow::new(NodeId(3), c_current, c_left),
],
clearance: 0,
};
let settled_request = MultiphaseRequest {
bounds: rect(0, 0, 200, 100),
windows: vec![
MultiphaseWindow::new(NodeId(1), a_left, a_mid),
MultiphaseWindow::new(NodeId(3), c_mid, c_left),
],
clearance: 0,
};
let follow_plan = plan_no_overlap_with_diagnostics(&settled_request).unwrap();
let bridge_paths = vec![vec![], vec![(c_current, c_mid_lane), (c_mid_lane, c_mid)]];
let plan = bridged_retarget_plan(
&request,
&candidates,
&[0, 1],
&bridge_paths,
2,
&follow_plan.phases,
)
.unwrap();
assert!(plan
.phases
.iter()
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(1))));
assert!(plan
.phases
.iter()
.any(|phase| phase.steps.iter().any(|step| step.node_id == NodeId(3))));
}
#[test]
fn layout_animation_candidates_coalesce_duplicate_nodes() {
let source = MultiphaseHierarchyPosition {
parent: Some(NodeId(10).into()),
depth: 2,
sibling_index: Some(1),
..Default::default()
};
let intermediate = MultiphaseHierarchyPosition {
parent: Some(NodeId(11).into()),
depth: 1,
sibling_index: Some(0),
..Default::default()
};
let target = MultiphaseHierarchyPosition {
parent: Some(NodeId(12).into()),
depth: 0,
sibling_index: Some(2),
..Default::default()
};
let second_source = MultiphaseHierarchyPosition {
parent: Some(NodeId(20).into()),
depth: 1,
sibling_index: Some(0),
..Default::default()
};
let second_target = MultiphaseHierarchyPosition {
parent: Some(NodeId(20).into()),
depth: 1,
sibling_index: Some(1),
..Default::default()
};
let candidates = vec![
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(source, intermediate),
},
LayoutAnimationCandidate {
node_id: NodeId(2),
old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy: hierarchy(second_source, second_target),
},
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 80, 100),
new: rect(0, 0, 60, 100),
curve: AnimationCurve::from_config(4),
style: AnimationStyle::Plain,
hierarchy: hierarchy(intermediate, target),
},
];
let merged = coalesce_layout_animation_candidates(candidates);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].node_id, NodeId(1));
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 60, 100));
assert_eq!(merged[0].curve, AnimationCurve::from_config(4));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[0].hierarchy, hierarchy(source, target));
assert_eq!(merged[1].node_id, NodeId(2));
assert_eq!(merged[1].old, rect(100, 0, 200, 100));
assert_eq!(merged[1].new, rect(120, 0, 220, 100));
assert_eq!(merged[1].hierarchy, hierarchy(second_source, second_target));
}
#[test]
fn layout_animation_candidates_keep_coalesced_layout_noops() {
let hierarchy = MultiphaseWindowHierarchy::default();
let candidates = vec![
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 100, 100),
new: rect(0, 0, 80, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy,
},
LayoutAnimationCandidate {
node_id: NodeId(1),
old: rect(0, 0, 80, 100),
new: rect(0, 0, 100, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Plain,
hierarchy,
},
LayoutAnimationCandidate {
node_id: NodeId(2),
old: rect(100, 0, 200, 100),
new: rect(120, 0, 220, 100),
curve: AnimationCurve::Linear,
style: AnimationStyle::Multiphase,
hierarchy,
},
];
let merged = coalesce_layout_animation_candidates(candidates);
assert_eq!(merged.len(), 2);
assert_eq!(merged[0].node_id, NodeId(1));
assert_eq!(merged[0].old, rect(0, 0, 100, 100));
assert_eq!(merged[0].new, rect(0, 0, 100, 100));
assert_eq!(merged[0].style, AnimationStyle::Plain);
assert_eq!(merged[1].node_id, NodeId(2));
}
}