1
0
Fork 0
forked from wry/wry

Carry hierarchy metadata into multiphase planning

This commit is contained in:
atagen 2026-05-21 19:55:16 +10:00
parent a712786ecf
commit 90c00bcdf3
4 changed files with 276 additions and 16 deletions

View file

@ -229,6 +229,10 @@ Current pure planner status:
- Live layout batches are partitioned by overlapping motion bounds, so unrelated
groups can still use multiphase animation when another group falls back to
linear motion.
- Planner requests now carry per-window hierarchy metadata for source/target
parent, depth, sibling index, split axis, mono state, and transition kind.
The current planner records this information but does not yet use it to order
nested-container phases.
Tests:

View file

@ -13,6 +13,104 @@ pub struct MultiphaseWindow {
pub node_id: NodeId,
pub from: Rect,
pub to: Rect,
pub hierarchy: MultiphaseWindowHierarchy,
}
impl MultiphaseWindow {
pub fn new(node_id: NodeId, from: Rect, to: Rect) -> Self {
Self {
node_id,
from,
to,
hierarchy: Default::default(),
}
}
pub fn with_hierarchy(
node_id: NodeId,
from: Rect,
to: Rect,
hierarchy: MultiphaseWindowHierarchy,
) -> Self {
Self {
node_id,
from,
to,
hierarchy,
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct MultiphaseWindowHierarchy {
pub source: MultiphaseHierarchyPosition,
pub target: MultiphaseHierarchyPosition,
pub transition: MultiphaseHierarchyTransition,
}
impl MultiphaseWindowHierarchy {
pub fn new(source: MultiphaseHierarchyPosition, target: MultiphaseHierarchyPosition) -> Self {
let transition = if !source.parent_is_mono && target.parent_is_mono {
MultiphaseHierarchyTransition::EnteringMono
} else if source.parent_is_mono && !target.parent_is_mono {
MultiphaseHierarchyTransition::ExitingMono
} else if source.parent.is_none() || target.parent.is_none() {
MultiphaseHierarchyTransition::Unknown
} else if target.depth < source.depth {
MultiphaseHierarchyTransition::Ascending
} else if target.depth > source.depth {
MultiphaseHierarchyTransition::Descending
} else {
MultiphaseHierarchyTransition::SameLevel
};
Self {
source,
target,
transition,
}
}
fn reversed(self) -> Self {
Self {
source: self.target,
target: self.source,
transition: self.transition.reversed(),
}
}
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub struct MultiphaseHierarchyPosition {
pub parent: Option<NodeId>,
pub depth: u16,
pub sibling_index: Option<u16>,
pub split_axis: Option<PhaseAxis>,
pub parent_is_mono: bool,
pub mono_active: bool,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum MultiphaseHierarchyTransition {
#[default]
Unknown,
SameLevel,
Ascending,
Descending,
EnteringMono,
ExitingMono,
}
impl MultiphaseHierarchyTransition {
fn reversed(self) -> Self {
match self {
Self::Unknown => Self::Unknown,
Self::SameLevel => Self::SameLevel,
Self::Ascending => Self::Descending,
Self::Descending => Self::Ascending,
Self::EnteringMono => Self::ExitingMono,
Self::ExitingMono => Self::EnteringMono,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
@ -519,6 +617,7 @@ fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
node_id: window.node_id,
from: window.to,
to: window.from,
hierarchy: window.hierarchy.reversed(),
})
.collect(),
}
@ -628,6 +727,10 @@ mod tests {
Rect::new_saturating(x1, y1, x2, y2)
}
fn window(raw: u32, from: Rect, to: Rect) -> MultiphaseWindow {
MultiphaseWindow::new(id(raw), from, to)
}
fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
let bounds = windows
.iter()
@ -653,15 +756,12 @@ mod tests {
#[test]
fn horizontal_swap_shrinks_moves_then_grows_without_overlap() {
let req = request(vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(100, 0, 200, 100),
},
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
MultiphaseWindow {
node_id: id(2),
from: rect(100, 0, 200, 100),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -694,11 +794,13 @@ mod tests {
node_id: id(1),
from: rect(100, 0, 200, 100),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 0, 100, 100),
to: rect(100, 0, 200, 100),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -714,11 +816,13 @@ mod tests {
node_id: id(1),
from: rect(100, 0, 200, 100),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 0, 100, 100),
to: rect(100, 0, 200, 100),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -734,11 +838,13 @@ mod tests {
node_id: id(1),
from: rect(0, 100, 100, 200),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 0, 100, 100),
to: rect(0, 100, 100, 200),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -754,16 +860,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 200, 100),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(200, 0, 400, 50),
to: rect(100, 0, 300, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(200, 50, 400, 100),
to: rect(300, 0, 400, 100),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -799,16 +908,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(0, 0, 200, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(100, 0, 300, 100),
to: rect(200, 0, 400, 50),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(300, 0, 400, 100),
to: rect(200, 50, 400, 100),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -839,16 +951,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 200),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 200, 50, 400),
to: rect(0, 100, 100, 300),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(50, 200, 100, 400),
to: rect(0, 300, 100, 400),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -884,16 +999,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(0, 0, 100, 200),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 100, 100, 300),
to: rect(0, 200, 50, 400),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(0, 300, 100, 400),
to: rect(50, 200, 100, 400),
hierarchy: Default::default(),
},
]);
let plan = plan_no_overlap(&req).unwrap();
@ -923,10 +1041,50 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(100, 100, 200, 200),
hierarchy: Default::default(),
}]);
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
}
#[test]
fn hierarchy_metadata_classifies_depth_and_mono_transitions() {
let source = MultiphaseHierarchyPosition {
parent: Some(id(10)),
depth: 2,
sibling_index: Some(0),
split_axis: Some(PhaseAxis::Vertical),
..Default::default()
};
let target = MultiphaseHierarchyPosition {
parent: Some(id(11)),
depth: 1,
sibling_index: Some(2),
split_axis: Some(PhaseAxis::Horizontal),
..Default::default()
};
assert_eq!(
MultiphaseWindowHierarchy::new(source, target).transition,
MultiphaseHierarchyTransition::Ascending
);
let entering_mono = MultiphaseWindowHierarchy::new(
source,
MultiphaseHierarchyPosition {
parent_is_mono: true,
mono_active: true,
..target
},
);
assert_eq!(
entering_mono.transition,
MultiphaseHierarchyTransition::EnteringMono
);
assert_eq!(
entering_mono.reversed().transition,
MultiphaseHierarchyTransition::ExitingMono
);
}
#[test]
fn continuous_validation_rejects_narrow_mid_phase_overlap() {
let req = request(vec![
@ -934,11 +1092,13 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(100, 0, 110, 10),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(13, 0, 14, 10),
to: rect(13, 0, 14, 10),
hierarchy: Default::default(),
},
]);
let plan = MultiphasePlan {
@ -965,11 +1125,13 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(10, 0, 20, 10),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(20, 0, 30, 10),
to: rect(20, 0, 30, 10),
hierarchy: Default::default(),
},
]);
let plan = MultiphasePlan {
@ -995,6 +1157,7 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(20, 0, 30, 10),
hierarchy: Default::default(),
}]);
let plan = MultiphasePlan {
phases: vec![MultiphasePhase {
@ -1020,16 +1183,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(100, 0, 200, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(100, 0, 200, 100),
to: rect(0, 0, 100, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(300, 0, 400, 100),
to: rect(400, 0, 500, 100),
hierarchy: Default::default(),
},
];
assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1], vec![2]]);
@ -1042,16 +1208,19 @@ mod tests {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(80, 0, 180, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(2),
from: rect(170, 0, 270, 100),
to: rect(250, 0, 350, 100),
hierarchy: Default::default(),
},
MultiphaseWindow {
node_id: id(3),
from: rect(90, 0, 180, 100),
to: rect(180, 0, 260, 100),
hierarchy: Default::default(),
},
];
assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1, 2]]);

View file

@ -6,7 +6,8 @@ use {
AnimationCurve, AnimationState, AnimationTick, RetainedExitLayer, RetainedToplevel,
expand_damage_rect,
multiphase::{
MultiphaseRequest, MultiphaseWindow, partition_motion_groups, plan_no_overlap,
MultiphaseRequest, MultiphaseWindow, MultiphaseWindowHierarchy,
partition_motion_groups, plan_no_overlap,
},
spawn_in_start_rect,
},
@ -168,6 +169,7 @@ pub(crate) struct LayoutAnimationCandidate {
new: Rect,
retained: Option<Rc<RetainedToplevel>>,
curve: AnimationCurve,
hierarchy: MultiphaseWindowHierarchy,
}
pub struct State {
@ -1500,7 +1502,29 @@ impl State {
.layout_animation_curve_override
.get()
.unwrap_or_else(|| self.animations.curve.get());
self.queue_layout_animation(node_id, old, new, retained, curve);
self.queue_layout_animation(
node_id,
old,
new,
retained,
curve,
MultiphaseWindowHierarchy::default(),
);
}
pub fn queue_tiled_animation_with_hierarchy(
self: &Rc<Self>,
node_id: NodeId,
old: Rect,
new: Rect,
retained: Option<Rc<RetainedToplevel>>,
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, retained, curve, hierarchy);
}
pub fn queue_linear_layout_animation(
@ -1510,7 +1534,14 @@ impl State {
new: Rect,
retained: Option<Rc<RetainedToplevel>>,
) {
self.queue_layout_animation(node_id, old, new, retained, AnimationCurve::Linear);
self.queue_layout_animation(
node_id,
old,
new,
retained,
AnimationCurve::Linear,
MultiphaseWindowHierarchy::default(),
);
}
fn queue_layout_animation(
@ -1520,6 +1551,7 @@ impl State {
new: Rect,
retained: Option<Rc<RetainedToplevel>>,
curve: AnimationCurve,
hierarchy: MultiphaseWindowHierarchy,
) {
if !self.animations.enabled.get()
|| !self.layout_animations_active.get()
@ -1546,6 +1578,7 @@ impl State {
new,
retained,
curve,
hierarchy,
};
if let Some(batch) = self.layout_animation_batch.borrow_mut().as_mut() {
batch.push(candidate);
@ -1590,12 +1623,14 @@ impl State {
let now = self.now_nsec();
let windows: Vec<_> = candidates
.iter()
.map(|candidate| MultiphaseWindow {
node_id: candidate.node_id,
from: self
.animations
.visual_rect(candidate.node_id, candidate.old, now),
to: candidate.new,
.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) {

View file

@ -1,6 +1,9 @@
use {
crate::{
animation::{RetainedExitLayer, RetainedToplevel},
animation::{
RetainedExitLayer, RetainedToplevel,
multiphase::{MultiphaseHierarchyPosition, MultiphaseWindowHierarchy, PhaseAxis},
},
client::{Client, ClientId},
criteria::{
CritDestroyListener, CritMatcherId,
@ -186,6 +189,11 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
fn tl_change_extents(self: Rc<Self>, rect: &Rect) {
let data = self.tl_data();
let prev = data.desired_extents.replace(*rect);
let target_hierarchy = self.tl_multiphase_hierarchy_position();
let hierarchy = MultiphaseWindowHierarchy::new(
data.layout_animation_position.replace(target_hierarchy),
target_hierarchy,
);
let spawn_in_pending = data.spawn_in_pending.get();
let parent_is_mono = data
.parent
@ -200,11 +208,12 @@ impl<T: ToplevelNodeBase> ToplevelNode for T {
&& !self.node_is_container()
&& !parent_is_mono
{
data.state.clone().queue_tiled_animation(
data.state.clone().queue_tiled_animation_with_hierarchy(
data.node_id,
prev,
*rect,
self.tl_animation_snapshot(),
hierarchy,
);
}
if spawn_in_pending
@ -314,6 +323,35 @@ pub trait ToplevelNodeBase: Node {
true
}
fn tl_multiphase_hierarchy_position(&self) -> MultiphaseHierarchyPosition {
let data = self.tl_data();
let Some(parent) = data.parent.get() else {
return Default::default();
};
let mut position = MultiphaseHierarchyPosition {
parent: Some(parent.node_id()),
depth: multiphase_parent_depth(Some(parent.clone())),
..Default::default()
};
if let Some(container) = parent.node_into_container() {
position.split_axis = Some(match container.split.get() {
ContainerSplit::Horizontal => PhaseAxis::Horizontal,
ContainerSplit::Vertical => PhaseAxis::Vertical,
});
if let Some(mono) = container.mono_child.get() {
position.parent_is_mono = true;
position.mono_active = mono.node.node_id() == data.node_id;
}
for (idx, child) in container.children.iter().enumerate() {
if child.node.node_id() == data.node_id {
position.sibling_index = Some(idx.min(u16::MAX as usize) as u16);
break;
}
}
}
position
}
fn tl_set_active(&self, active: bool) {
let _ = active;
}
@ -383,6 +421,18 @@ pub trait ToplevelNodeBase: Node {
}
}
fn multiphase_parent_depth(mut parent: Option<Rc<dyn ContainingNode>>) -> u16 {
let mut depth = 0u16;
while let Some(node) = parent {
let Some(toplevel) = node.node_into_toplevel() else {
break;
};
depth = depth.saturating_add(1);
parent = toplevel.tl_data().parent.get();
}
depth
}
pub struct FullscreenedData {
pub placeholder: Rc<PlaceholderNode>,
pub workspace: Rc<WorkspaceNode>,
@ -453,6 +503,7 @@ pub struct ToplevelData {
pub spawn_in_pending: Cell<bool>,
pub pos: Cell<Rect>,
pub desired_extents: Cell<Rect>,
pub layout_animation_position: Cell<MultiphaseHierarchyPosition>,
pub seat_state: NodeSeatState,
pub wants_attention: Cell<bool>,
pub requested_attention: Cell<bool>,
@ -517,6 +568,7 @@ impl ToplevelData {
spawn_in_pending: Cell::new(false),
pos: Default::default(),
desired_extents: Default::default(),
layout_animation_position: Default::default(),
seat_state: Default::default(),
wants_attention: Cell::new(false),
requested_attention: Cell::new(false),