Order nested scale phases by hierarchy
This commit is contained in:
parent
a770089b88
commit
0b6da9d8e0
3 changed files with 237 additions and 25 deletions
|
|
@ -225,6 +225,9 @@ Current pure planner status:
|
|||
only after space exists, and orthogonal growth happens in the final phase.
|
||||
- Same-axis size redistribution is handled as a single scale phase when the
|
||||
exact validator proves adjacent windows stay non-overlapping.
|
||||
- Nested size redistribution can use hierarchy metadata to decompose two-axis
|
||||
resizing into parent-axis then child-axis scale phases, but only when the
|
||||
source/target ancestor split depths give a deterministic order.
|
||||
- Every produced plan is checked analytically for overlap over the full duration
|
||||
of each phase before it is accepted. This solves the linear edge inequalities
|
||||
for each pair of moving rectangles instead of relying on sampled frames.
|
||||
|
|
@ -232,9 +235,9 @@ Current pure planner status:
|
|||
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.
|
||||
parent, depth, sibling index, split axis, nearest ancestor split depth per
|
||||
axis, mono state, and transition kind. The current planner uses this for
|
||||
parent-before-child scale ordering, but not yet for full nested move planning.
|
||||
- Multiphase planning has a diagnostic entry point used by live fallback logs.
|
||||
It distinguishes request validation errors, missing patterns, shrink-bound
|
||||
rejections, invalid phase steps, and exact validation failures such as stale
|
||||
|
|
@ -247,6 +250,7 @@ Tests:
|
|||
|
||||
- horizontal swaps shrink, move, then grow without overlap
|
||||
- extraction from a stack creates space before moving the extracted window
|
||||
- nested size redistribution scales the parent axis before the child axis
|
||||
- nested containers do not produce simultaneous cross-axis motion
|
||||
- interruption restarts only affected phase groups
|
||||
- reversing direction produces equivalent motion in reverse
|
||||
|
|
|
|||
|
|
@ -85,6 +85,8 @@ pub struct MultiphaseHierarchyPosition {
|
|||
pub depth: u16,
|
||||
pub sibling_index: Option<u16>,
|
||||
pub split_axis: Option<PhaseAxis>,
|
||||
pub nearest_horizontal_split_depth: Option<u16>,
|
||||
pub nearest_vertical_split_depth: Option<u16>,
|
||||
pub parent_is_mono: bool,
|
||||
pub mono_active: bool,
|
||||
}
|
||||
|
|
@ -320,6 +322,13 @@ fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, Multiphas
|
|||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
match plan_hierarchy_ordered_axis_scales(request) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(MultiphasePlanFailure::NoPattern) => {}
|
||||
Err(error) => {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
match plan_axis_crossing_lanes(request, axis) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
|
|
@ -383,6 +392,96 @@ fn plan_single_action_phase(
|
|||
build_validated_plan(request, [(action.kind, action.axis, steps)])
|
||||
}
|
||||
|
||||
fn plan_hierarchy_ordered_axis_scales(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
let mut changed_axes = vec![];
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
if request
|
||||
.windows
|
||||
.iter()
|
||||
.any(|window| interval_changed(window.from, window.to, axis))
|
||||
{
|
||||
changed_axes.push(axis);
|
||||
}
|
||||
}
|
||||
let [first_axis, second_axis] = changed_axes
|
||||
.try_into()
|
||||
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
||||
let axes = hierarchy_scale_axis_order(request, first_axis, second_axis)
|
||||
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
||||
let mut current: Vec<_> = request
|
||||
.windows
|
||||
.iter()
|
||||
.map(|window| (window.node_id, window.from))
|
||||
.collect();
|
||||
let mut phases = vec![];
|
||||
for axis in axes {
|
||||
let mut steps = vec![];
|
||||
for window in &request.windows {
|
||||
let (_, rect) = current
|
||||
.iter_mut()
|
||||
.find(|(node_id, _)| *node_id == window.node_id)
|
||||
.unwrap();
|
||||
let next = with_main_interval(
|
||||
*rect,
|
||||
axis,
|
||||
main_start(window.to, axis),
|
||||
main_end(window.to, axis),
|
||||
);
|
||||
if next == *rect {
|
||||
continue;
|
||||
}
|
||||
if main_size(*rect, axis) == main_size(next, axis) {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
steps.push(MultiphaseStep {
|
||||
node_id: window.node_id,
|
||||
from: *rect,
|
||||
to: next,
|
||||
});
|
||||
*rect = next;
|
||||
}
|
||||
if steps.is_empty() {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
phases.push((PhaseKind::Scale, axis, steps));
|
||||
}
|
||||
let [first, second] = phases
|
||||
.try_into()
|
||||
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
||||
build_validated_plan(request, [first, second])
|
||||
}
|
||||
|
||||
fn hierarchy_scale_axis_order(
|
||||
request: &MultiphaseRequest,
|
||||
first_axis: PhaseAxis,
|
||||
second_axis: PhaseAxis,
|
||||
) -> Option<[PhaseAxis; 2]> {
|
||||
let first_priority = hierarchy_axis_priority(request, first_axis)?;
|
||||
let second_priority = hierarchy_axis_priority(request, second_axis)?;
|
||||
match first_priority.cmp(&second_priority) {
|
||||
std::cmp::Ordering::Less => Some([first_axis, second_axis]),
|
||||
std::cmp::Ordering::Greater => Some([second_axis, first_axis]),
|
||||
std::cmp::Ordering::Equal => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option<u16> {
|
||||
request
|
||||
.windows
|
||||
.iter()
|
||||
.filter(|window| interval_changed(window.from, window.to, axis))
|
||||
.flat_map(|window| {
|
||||
[
|
||||
split_depth_for_axis(window.hierarchy.source, axis),
|
||||
split_depth_for_axis(window.hierarchy.target, axis),
|
||||
]
|
||||
})
|
||||
.flatten()
|
||||
.min()
|
||||
}
|
||||
|
||||
fn plan_axis_crossing_lanes(
|
||||
request: &MultiphaseRequest,
|
||||
axis: PhaseAxis,
|
||||
|
|
@ -847,6 +946,17 @@ fn motion_bounds(window: MultiphaseWindow) -> Rect {
|
|||
window.from.union(window.to)
|
||||
}
|
||||
|
||||
fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool {
|
||||
main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis)
|
||||
}
|
||||
|
||||
fn split_depth_for_axis(position: MultiphaseHierarchyPosition, axis: PhaseAxis) -> Option<u16> {
|
||||
match axis {
|
||||
PhaseAxis::Horizontal => position.nearest_horizontal_split_depth,
|
||||
PhaseAxis::Vertical => position.nearest_vertical_split_depth,
|
||||
}
|
||||
}
|
||||
|
||||
fn push_step(steps: &mut Vec<MultiphaseStep>, node_id: NodeId, from: Rect, to: Rect) {
|
||||
if from != to {
|
||||
steps.push(MultiphaseStep { node_id, from, to });
|
||||
|
|
@ -951,18 +1061,37 @@ mod tests {
|
|||
|
||||
fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec<TestLeaf> {
|
||||
let mut leaves = vec![];
|
||||
layout_tree_inner(tree, bounds, None, 0, None, None, &mut leaves);
|
||||
layout_tree_inner(
|
||||
tree,
|
||||
bounds,
|
||||
TestHierarchy {
|
||||
parent: None,
|
||||
depth: 0,
|
||||
sibling_index: None,
|
||||
split_axis: None,
|
||||
nearest_horizontal_split_depth: None,
|
||||
nearest_vertical_split_depth: None,
|
||||
},
|
||||
&mut leaves,
|
||||
);
|
||||
leaves.sort_by_key(|leaf| leaf.node_id.0);
|
||||
leaves
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct TestHierarchy {
|
||||
parent: Option<NodeId>,
|
||||
depth: u16,
|
||||
sibling_index: Option<u16>,
|
||||
split_axis: Option<PhaseAxis>,
|
||||
nearest_horizontal_split_depth: Option<u16>,
|
||||
nearest_vertical_split_depth: Option<u16>,
|
||||
}
|
||||
|
||||
fn layout_tree_inner(
|
||||
tree: &TestTree,
|
||||
bounds: Rect,
|
||||
parent: Option<NodeId>,
|
||||
depth: u16,
|
||||
sibling_index: Option<u16>,
|
||||
split_axis: Option<PhaseAxis>,
|
||||
hierarchy: TestHierarchy,
|
||||
leaves: &mut Vec<TestLeaf>,
|
||||
) {
|
||||
match tree {
|
||||
|
|
@ -970,10 +1099,12 @@ mod tests {
|
|||
node_id: id(*raw),
|
||||
rect: bounds,
|
||||
hierarchy: MultiphaseHierarchyPosition {
|
||||
parent,
|
||||
depth,
|
||||
sibling_index,
|
||||
split_axis,
|
||||
parent: hierarchy.parent,
|
||||
depth: hierarchy.depth,
|
||||
sibling_index: hierarchy.sibling_index,
|
||||
split_axis: hierarchy.split_axis,
|
||||
nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth,
|
||||
nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth,
|
||||
..Default::default()
|
||||
},
|
||||
}),
|
||||
|
|
@ -986,15 +1117,24 @@ mod tests {
|
|||
assert_eq!(weights.len(), children.len());
|
||||
let rects = split_rect_by_weights(bounds, *axis, weights);
|
||||
for (idx, (child, rect)) in children.iter().zip(rects).enumerate() {
|
||||
layout_tree_inner(
|
||||
child,
|
||||
rect,
|
||||
Some(id(*split_id)),
|
||||
depth.saturating_add(1),
|
||||
Some(idx.min(u16::MAX as usize) as u16),
|
||||
Some(*axis),
|
||||
leaves,
|
||||
);
|
||||
let depth = hierarchy.depth.saturating_add(1);
|
||||
let mut child_hierarchy = TestHierarchy {
|
||||
parent: Some(id(*split_id)),
|
||||
depth,
|
||||
sibling_index: Some(idx.min(u16::MAX as usize) as u16),
|
||||
split_axis: Some(*axis),
|
||||
nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth,
|
||||
nearest_vertical_split_depth: hierarchy.nearest_vertical_split_depth,
|
||||
};
|
||||
match axis {
|
||||
PhaseAxis::Horizontal => {
|
||||
child_hierarchy.nearest_horizontal_split_depth = Some(depth);
|
||||
}
|
||||
PhaseAxis::Vertical => {
|
||||
child_hierarchy.nearest_vertical_split_depth = Some(depth);
|
||||
}
|
||||
}
|
||||
layout_tree_inner(child, rect, child_hierarchy, leaves);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1249,6 +1389,57 @@ mod tests {
|
|||
assert!(validate_plan_continuous(&vertical_req, &vertical_plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_nested_size_redistribution_scales_parent_axis_first() {
|
||||
let old = split(
|
||||
10,
|
||||
PhaseAxis::Horizontal,
|
||||
&[1, 1],
|
||||
vec![
|
||||
leaf(1),
|
||||
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]),
|
||||
],
|
||||
);
|
||||
let new = split(
|
||||
10,
|
||||
PhaseAxis::Horizontal,
|
||||
&[1, 3],
|
||||
vec![
|
||||
leaf(1),
|
||||
split(11, PhaseAxis::Vertical, &[3, 1], vec![leaf(2), leaf(3)]),
|
||||
],
|
||||
);
|
||||
let req = generated_request(&old, &new, rect(0, 0, 400, 100));
|
||||
let plan = plan_no_overlap(&req).unwrap();
|
||||
assert_eq!(
|
||||
actions(&plan),
|
||||
vec![
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
axis: PhaseAxis::Vertical,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 100));
|
||||
assert_eq!(step_to(&plan, 0, id(2)), rect(100, 0, 400, 50));
|
||||
assert_eq!(step_to(&plan, 0, id(3)), rect(100, 50, 400, 100));
|
||||
assert!(validate_plan_continuous(&req, &plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_axis_redistribution_without_hierarchy_still_falls_back() {
|
||||
let req = request(vec![
|
||||
window(1, rect(0, 0, 200, 100), rect(0, 0, 100, 100)),
|
||||
window(2, rect(200, 0, 400, 50), rect(100, 0, 400, 75)),
|
||||
window(3, rect(200, 50, 400, 100), rect(100, 75, 400, 100)),
|
||||
]);
|
||||
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_stack_extractions_plan_for_both_axes_and_directions() {
|
||||
let horizontal_old = split(
|
||||
|
|
@ -1525,6 +1716,8 @@ mod tests {
|
|||
depth: 2,
|
||||
sibling_index: Some(0),
|
||||
split_axis: Some(PhaseAxis::Vertical),
|
||||
nearest_horizontal_split_depth: Some(1),
|
||||
nearest_vertical_split_depth: Some(2),
|
||||
..Default::default()
|
||||
};
|
||||
let target = MultiphaseHierarchyPosition {
|
||||
|
|
@ -1532,12 +1725,14 @@ mod tests {
|
|||
depth: 1,
|
||||
sibling_index: Some(2),
|
||||
split_axis: Some(PhaseAxis::Horizontal),
|
||||
nearest_horizontal_split_depth: Some(1),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
MultiphaseWindowHierarchy::new(source, target).transition,
|
||||
MultiphaseHierarchyTransition::Ascending
|
||||
);
|
||||
assert_eq!(source.nearest_vertical_split_depth, Some(2));
|
||||
|
||||
let entering_mono = MultiphaseWindowHierarchy::new(
|
||||
source,
|
||||
|
|
|
|||
|
|
@ -330,9 +330,9 @@ pub trait ToplevelNodeBase: Node {
|
|||
};
|
||||
let mut position = MultiphaseHierarchyPosition {
|
||||
parent: Some(parent.node_id()),
|
||||
depth: multiphase_parent_depth(Some(parent.clone())),
|
||||
..Default::default()
|
||||
};
|
||||
populate_multiphase_ancestor_splits(&mut position, Some(parent.clone()));
|
||||
if let Some(container) = parent.node_into_container() {
|
||||
position.split_axis = Some(match container.split.get() {
|
||||
ContainerSplit::Horizontal => PhaseAxis::Horizontal,
|
||||
|
|
@ -421,16 +421,29 @@ pub trait ToplevelNodeBase: Node {
|
|||
}
|
||||
}
|
||||
|
||||
fn multiphase_parent_depth(mut parent: Option<Rc<dyn ContainingNode>>) -> u16 {
|
||||
fn populate_multiphase_ancestor_splits(
|
||||
position: &mut MultiphaseHierarchyPosition,
|
||||
mut parent: Option<Rc<dyn ContainingNode>>,
|
||||
) {
|
||||
let mut depth = 0u16;
|
||||
while let Some(node) = parent {
|
||||
let Some(toplevel) = node.node_into_toplevel() else {
|
||||
let Some(toplevel) = node.clone().node_into_toplevel() else {
|
||||
break;
|
||||
};
|
||||
depth = depth.saturating_add(1);
|
||||
if let Some(container) = node.node_into_container() {
|
||||
match container.split.get() {
|
||||
ContainerSplit::Horizontal => {
|
||||
position.nearest_horizontal_split_depth.get_or_insert(depth);
|
||||
}
|
||||
ContainerSplit::Vertical => {
|
||||
position.nearest_vertical_split_depth.get_or_insert(depth);
|
||||
}
|
||||
}
|
||||
}
|
||||
parent = toplevel.tl_data().parent.get();
|
||||
}
|
||||
depth
|
||||
position.depth = depth;
|
||||
}
|
||||
|
||||
pub struct FullscreenedData {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue