state: split animation orchestration
This commit is contained in:
parent
fb31d5115d
commit
19c2265400
2 changed files with 866 additions and 851 deletions
855
src/state.rs
855
src/state.rs
|
|
@ -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
862
src/state/animations.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue