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.
|
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
|
- Same-axis size redistribution is handled as a single scale phase when the
|
||||||
exact validator proves adjacent windows stay non-overlapping.
|
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
|
- 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
|
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.
|
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
|
groups can still use multiphase animation when another group falls back to
|
||||||
linear motion.
|
linear motion.
|
||||||
- Planner requests now carry per-window hierarchy metadata for source/target
|
- Planner requests now carry per-window hierarchy metadata for source/target
|
||||||
parent, depth, sibling index, split axis, mono state, and transition kind.
|
parent, depth, sibling index, split axis, nearest ancestor split depth per
|
||||||
The current planner records this information but does not yet use it to order
|
axis, mono state, and transition kind. The current planner uses this for
|
||||||
nested-container phases.
|
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.
|
- Multiphase planning has a diagnostic entry point used by live fallback logs.
|
||||||
It distinguishes request validation errors, missing patterns, shrink-bound
|
It distinguishes request validation errors, missing patterns, shrink-bound
|
||||||
rejections, invalid phase steps, and exact validation failures such as stale
|
rejections, invalid phase steps, and exact validation failures such as stale
|
||||||
|
|
@ -247,6 +250,7 @@ Tests:
|
||||||
|
|
||||||
- horizontal swaps shrink, move, then grow without overlap
|
- horizontal swaps shrink, move, then grow without overlap
|
||||||
- extraction from a stack creates space before moving the extracted window
|
- 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
|
- nested containers do not produce simultaneous cross-axis motion
|
||||||
- interruption restarts only affected phase groups
|
- interruption restarts only affected phase groups
|
||||||
- reversing direction produces equivalent motion in reverse
|
- reversing direction produces equivalent motion in reverse
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,8 @@ pub struct MultiphaseHierarchyPosition {
|
||||||
pub depth: u16,
|
pub depth: u16,
|
||||||
pub sibling_index: Option<u16>,
|
pub sibling_index: Option<u16>,
|
||||||
pub split_axis: Option<PhaseAxis>,
|
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 parent_is_mono: bool,
|
||||||
pub mono_active: bool,
|
pub mono_active: bool,
|
||||||
}
|
}
|
||||||
|
|
@ -320,6 +322,13 @@ fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, Multiphas
|
||||||
rejection.get_or_insert(error);
|
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] {
|
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||||
match plan_axis_crossing_lanes(request, axis) {
|
match plan_axis_crossing_lanes(request, axis) {
|
||||||
Ok(plan) => return Ok(plan),
|
Ok(plan) => return Ok(plan),
|
||||||
|
|
@ -383,6 +392,96 @@ fn plan_single_action_phase(
|
||||||
build_validated_plan(request, [(action.kind, action.axis, steps)])
|
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(
|
fn plan_axis_crossing_lanes(
|
||||||
request: &MultiphaseRequest,
|
request: &MultiphaseRequest,
|
||||||
axis: PhaseAxis,
|
axis: PhaseAxis,
|
||||||
|
|
@ -847,6 +946,17 @@ fn motion_bounds(window: MultiphaseWindow) -> Rect {
|
||||||
window.from.union(window.to)
|
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) {
|
fn push_step(steps: &mut Vec<MultiphaseStep>, node_id: NodeId, from: Rect, to: Rect) {
|
||||||
if from != to {
|
if from != to {
|
||||||
steps.push(MultiphaseStep { node_id, from, to });
|
steps.push(MultiphaseStep { node_id, from, to });
|
||||||
|
|
@ -951,18 +1061,37 @@ mod tests {
|
||||||
|
|
||||||
fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec<TestLeaf> {
|
fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec<TestLeaf> {
|
||||||
let mut leaves = vec![];
|
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.sort_by_key(|leaf| leaf.node_id.0);
|
||||||
leaves
|
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(
|
fn layout_tree_inner(
|
||||||
tree: &TestTree,
|
tree: &TestTree,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
parent: Option<NodeId>,
|
hierarchy: TestHierarchy,
|
||||||
depth: u16,
|
|
||||||
sibling_index: Option<u16>,
|
|
||||||
split_axis: Option<PhaseAxis>,
|
|
||||||
leaves: &mut Vec<TestLeaf>,
|
leaves: &mut Vec<TestLeaf>,
|
||||||
) {
|
) {
|
||||||
match tree {
|
match tree {
|
||||||
|
|
@ -970,10 +1099,12 @@ mod tests {
|
||||||
node_id: id(*raw),
|
node_id: id(*raw),
|
||||||
rect: bounds,
|
rect: bounds,
|
||||||
hierarchy: MultiphaseHierarchyPosition {
|
hierarchy: MultiphaseHierarchyPosition {
|
||||||
parent,
|
parent: hierarchy.parent,
|
||||||
depth,
|
depth: hierarchy.depth,
|
||||||
sibling_index,
|
sibling_index: hierarchy.sibling_index,
|
||||||
split_axis,
|
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()
|
..Default::default()
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -986,15 +1117,24 @@ mod tests {
|
||||||
assert_eq!(weights.len(), children.len());
|
assert_eq!(weights.len(), children.len());
|
||||||
let rects = split_rect_by_weights(bounds, *axis, weights);
|
let rects = split_rect_by_weights(bounds, *axis, weights);
|
||||||
for (idx, (child, rect)) in children.iter().zip(rects).enumerate() {
|
for (idx, (child, rect)) in children.iter().zip(rects).enumerate() {
|
||||||
layout_tree_inner(
|
let depth = hierarchy.depth.saturating_add(1);
|
||||||
child,
|
let mut child_hierarchy = TestHierarchy {
|
||||||
rect,
|
parent: Some(id(*split_id)),
|
||||||
Some(id(*split_id)),
|
depth,
|
||||||
depth.saturating_add(1),
|
sibling_index: Some(idx.min(u16::MAX as usize) as u16),
|
||||||
Some(idx.min(u16::MAX as usize) as u16),
|
split_axis: Some(*axis),
|
||||||
Some(*axis),
|
nearest_horizontal_split_depth: hierarchy.nearest_horizontal_split_depth,
|
||||||
leaves,
|
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));
|
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]
|
#[test]
|
||||||
fn generated_stack_extractions_plan_for_both_axes_and_directions() {
|
fn generated_stack_extractions_plan_for_both_axes_and_directions() {
|
||||||
let horizontal_old = split(
|
let horizontal_old = split(
|
||||||
|
|
@ -1525,6 +1716,8 @@ mod tests {
|
||||||
depth: 2,
|
depth: 2,
|
||||||
sibling_index: Some(0),
|
sibling_index: Some(0),
|
||||||
split_axis: Some(PhaseAxis::Vertical),
|
split_axis: Some(PhaseAxis::Vertical),
|
||||||
|
nearest_horizontal_split_depth: Some(1),
|
||||||
|
nearest_vertical_split_depth: Some(2),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let target = MultiphaseHierarchyPosition {
|
let target = MultiphaseHierarchyPosition {
|
||||||
|
|
@ -1532,12 +1725,14 @@ mod tests {
|
||||||
depth: 1,
|
depth: 1,
|
||||||
sibling_index: Some(2),
|
sibling_index: Some(2),
|
||||||
split_axis: Some(PhaseAxis::Horizontal),
|
split_axis: Some(PhaseAxis::Horizontal),
|
||||||
|
nearest_horizontal_split_depth: Some(1),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
MultiphaseWindowHierarchy::new(source, target).transition,
|
MultiphaseWindowHierarchy::new(source, target).transition,
|
||||||
MultiphaseHierarchyTransition::Ascending
|
MultiphaseHierarchyTransition::Ascending
|
||||||
);
|
);
|
||||||
|
assert_eq!(source.nearest_vertical_split_depth, Some(2));
|
||||||
|
|
||||||
let entering_mono = MultiphaseWindowHierarchy::new(
|
let entering_mono = MultiphaseWindowHierarchy::new(
|
||||||
source,
|
source,
|
||||||
|
|
|
||||||
|
|
@ -330,9 +330,9 @@ pub trait ToplevelNodeBase: Node {
|
||||||
};
|
};
|
||||||
let mut position = MultiphaseHierarchyPosition {
|
let mut position = MultiphaseHierarchyPosition {
|
||||||
parent: Some(parent.node_id()),
|
parent: Some(parent.node_id()),
|
||||||
depth: multiphase_parent_depth(Some(parent.clone())),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
populate_multiphase_ancestor_splits(&mut position, Some(parent.clone()));
|
||||||
if let Some(container) = parent.node_into_container() {
|
if let Some(container) = parent.node_into_container() {
|
||||||
position.split_axis = Some(match container.split.get() {
|
position.split_axis = Some(match container.split.get() {
|
||||||
ContainerSplit::Horizontal => PhaseAxis::Horizontal,
|
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;
|
let mut depth = 0u16;
|
||||||
while let Some(node) = parent {
|
while let Some(node) = parent {
|
||||||
let Some(toplevel) = node.node_into_toplevel() else {
|
let Some(toplevel) = node.clone().node_into_toplevel() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
depth = depth.saturating_add(1);
|
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();
|
parent = toplevel.tl_data().parent.get();
|
||||||
}
|
}
|
||||||
depth
|
position.depth = depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FullscreenedData {
|
pub struct FullscreenedData {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue