Refine animation planner test fixes
This commit is contained in:
parent
d2138b45f6
commit
1a75f47709
3 changed files with 214 additions and 77 deletions
|
|
@ -22,8 +22,6 @@ const DEFAULT_DURATION_MS: u32 = 160;
|
|||
const CURVE_MAX_POINTS: usize = 33;
|
||||
const CURVE_FLATNESS_EPSILON: f32 = 0.001;
|
||||
const CURVE_MAX_DEPTH: u8 = 8;
|
||||
const SPAWN_IN_INITIAL_SCALE_NUMERATOR: i32 = 4;
|
||||
const SPAWN_IN_INITIAL_SCALE_DENOMINATOR: i32 = 5;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum AnimationCurve {
|
||||
|
|
@ -296,7 +294,7 @@ impl AnimationState {
|
|||
duration_ms: u32,
|
||||
curve: AnimationCurve,
|
||||
) -> bool {
|
||||
if old == new || old.is_empty() || new.is_empty() || duration_ms == 0 {
|
||||
if old == new || new.is_empty() || duration_ms == 0 {
|
||||
self.windows.borrow_mut().remove(&node_id);
|
||||
self.phased.borrow_mut().remove(&node_id);
|
||||
return false;
|
||||
|
|
@ -420,7 +418,7 @@ impl AnimationState {
|
|||
return false;
|
||||
}
|
||||
let to = spawn_in_start_rect(from);
|
||||
if to == from || to.is_empty() {
|
||||
if to == from {
|
||||
return false;
|
||||
}
|
||||
let source_body_size = body_size_for_frame(from, frame_inset);
|
||||
|
|
@ -690,20 +688,8 @@ impl LatchListener for AnimationTick {
|
|||
}
|
||||
|
||||
pub(crate) fn spawn_in_start_rect(target: Rect) -> Rect {
|
||||
fn scaled_dimension(value: i32) -> i32 {
|
||||
let scaled = (value as i64 * SPAWN_IN_INITIAL_SCALE_NUMERATOR as i64
|
||||
/ SPAWN_IN_INITIAL_SCALE_DENOMINATOR as i64) as i32;
|
||||
scaled.clamp(1, value.max(1))
|
||||
}
|
||||
|
||||
let width = scaled_dimension(target.width());
|
||||
let height = scaled_dimension(target.height());
|
||||
Rect::new_sized_saturating(
|
||||
target.x1() + (target.width() - width) / 2,
|
||||
target.y1() + (target.height() - height) / 2,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
let (cx, cy) = target.center();
|
||||
Rect::new_empty(cx, cy)
|
||||
}
|
||||
|
||||
fn body_size_for_frame(rect: Rect, frame_inset: i32) -> (i32, i32) {
|
||||
|
|
@ -936,12 +922,9 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn spawn_in_start_rect_is_centered_and_non_empty() {
|
||||
fn spawn_in_start_rect_is_centered_and_empty() {
|
||||
let target = Rect::new_sized_saturating(10, 20, 100, 50);
|
||||
assert_eq!(
|
||||
spawn_in_start_rect(target),
|
||||
Rect::new_sized_saturating(20, 25, 80, 40)
|
||||
);
|
||||
assert_eq!(spawn_in_start_rect(target), Rect::new_empty(60, 45));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -952,7 +935,7 @@ mod tests {
|
|||
assert!(state.set_spawn_in(id, target, None, 0, 160));
|
||||
assert_eq!(
|
||||
state.visual_rect(id, target, 80_000_000),
|
||||
Rect::new_sized_saturating(15, 23, 90, 45)
|
||||
Rect::new_sized_saturating(35, 33, 50, 25)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const MIN_SHRINK_DENOMINATOR: i32 = 4;
|
|||
pub struct MultiphaseRequest {
|
||||
pub bounds: Rect,
|
||||
pub windows: Vec<MultiphaseWindow>,
|
||||
pub clearance: i32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
|
|
@ -488,6 +489,22 @@ fn plan_forward(
|
|||
}
|
||||
}
|
||||
}
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
match plan_space_then_orthogonal_growth(request, axis) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(error) => {
|
||||
record_rejection(
|
||||
&mut attempted,
|
||||
direction,
|
||||
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
||||
error,
|
||||
);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
match plan_hierarchy_ordered_axis_scales(request) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(error) => {
|
||||
|
|
@ -534,22 +551,6 @@ fn plan_forward(
|
|||
}
|
||||
}
|
||||
}
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
match plan_space_then_orthogonal_growth(request, axis) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(error) => {
|
||||
record_rejection(
|
||||
&mut attempted,
|
||||
direction,
|
||||
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
||||
error,
|
||||
);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(PlanForwardFailure {
|
||||
reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern),
|
||||
attempted,
|
||||
|
|
@ -750,7 +751,13 @@ fn plan_axis_crossing_lanes(
|
|||
request: &MultiphaseRequest,
|
||||
axis: PhaseAxis,
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
if request.windows.len() != 2 {
|
||||
let moving_windows: Vec<_> = request
|
||||
.windows
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|window| window.from != window.to)
|
||||
.collect();
|
||||
if moving_windows.len() != 2 {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
let orth_min = request
|
||||
|
|
@ -765,7 +772,7 @@ fn plan_axis_crossing_lanes(
|
|||
.map(|window| orth_end(window.from, axis))
|
||||
.max()
|
||||
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
||||
if request.windows.iter().any(|window| {
|
||||
if moving_windows.iter().any(|window| {
|
||||
main_size(window.from, axis) != main_size(window.to, axis)
|
||||
|| orth_start(window.from, axis) != orth_min
|
||||
|| orth_end(window.from, axis) != orth_max
|
||||
|
|
@ -775,7 +782,18 @@ fn plan_axis_crossing_lanes(
|
|||
}) {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
let lane_size = (orth_max - orth_min) / request.windows.len() as i32;
|
||||
let clearance = request.clearance.max(0);
|
||||
let lane_count = moving_windows.len() as i32;
|
||||
let available = (orth_max - orth_min) - clearance.saturating_mul(lane_count - 1);
|
||||
if available <= 0 {
|
||||
return Err(MultiphasePlanFailure::ShrinkBound {
|
||||
axis: axis.other(),
|
||||
available: 0,
|
||||
required: sane_min_size(orth_max - orth_min),
|
||||
});
|
||||
}
|
||||
let lane_size = available / lane_count;
|
||||
let mut lane_remainder = available % lane_count;
|
||||
let required = sane_min_size(orth_max - orth_min);
|
||||
if lane_size < required {
|
||||
return Err(MultiphasePlanFailure::ShrinkBound {
|
||||
|
|
@ -785,7 +803,7 @@ fn plan_axis_crossing_lanes(
|
|||
});
|
||||
}
|
||||
|
||||
let mut windows = request.windows.clone();
|
||||
let mut windows = moving_windows;
|
||||
windows.sort_by_key(|window| lane_index_for_direction(*window, axis));
|
||||
if windows.windows(2).any(|pair| {
|
||||
lane_index_for_direction(pair[0], axis) == lane_index_for_direction(pair[1], axis)
|
||||
|
|
@ -795,13 +813,15 @@ fn plan_axis_crossing_lanes(
|
|||
let mut phase1 = vec![];
|
||||
let mut phase2 = vec![];
|
||||
let mut phase3 = vec![];
|
||||
let mut lane_start = orth_min;
|
||||
for (idx, window) in windows.iter().enumerate() {
|
||||
let lane_start = orth_min + lane_size * idx as i32;
|
||||
let lane_end = if idx + 1 == windows.len() {
|
||||
orth_max
|
||||
let extra = if lane_remainder > 0 {
|
||||
lane_remainder -= 1;
|
||||
1
|
||||
} else {
|
||||
lane_start + lane_size
|
||||
0
|
||||
};
|
||||
let lane_end = lane_start + lane_size + extra;
|
||||
let lane_from = with_orth_interval(window.from, axis, lane_start, lane_end);
|
||||
let lane_to = with_main_interval(
|
||||
lane_from,
|
||||
|
|
@ -812,6 +832,9 @@ fn plan_axis_crossing_lanes(
|
|||
push_step(&mut phase1, window.node_id, window.from, lane_from);
|
||||
push_step(&mut phase2, window.node_id, lane_from, lane_to);
|
||||
push_step(&mut phase3, window.node_id, lane_to, window.to);
|
||||
if idx + 1 < windows.len() {
|
||||
lane_start = lane_end + clearance;
|
||||
}
|
||||
}
|
||||
build_validated_plan(
|
||||
request,
|
||||
|
|
@ -882,6 +905,7 @@ fn plan_space_then_orthogonal_growth(
|
|||
|| main_end(window.from, axis) != main_end(window.to, axis);
|
||||
let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis)
|
||||
|| orth_end(window.from, axis) != orth_end(window.to, axis);
|
||||
let mut orth_from = window.from;
|
||||
if main_changes && main_size(window.from, axis) == main_size(window.to, axis) {
|
||||
let after_move = with_main_interval(
|
||||
window.from,
|
||||
|
|
@ -890,22 +914,50 @@ fn plan_space_then_orthogonal_growth(
|
|||
main_end(window.to, axis),
|
||||
);
|
||||
push_step(&mut phase2, window.node_id, window.from, after_move);
|
||||
if orth_changes {
|
||||
push_step(&mut phase3, window.node_id, after_move, window.to);
|
||||
}
|
||||
orth_from = after_move;
|
||||
} else if main_changes {
|
||||
let after_main_scale = with_main_interval(
|
||||
window.from,
|
||||
axis,
|
||||
main_start(window.to, axis),
|
||||
main_end(window.to, axis),
|
||||
);
|
||||
let target_size = main_size(window.to, axis);
|
||||
let after_main_scale = if main_start(window.from, axis) == main_start(window.to, axis)
|
||||
|| main_end(window.from, axis) == main_end(window.to, axis)
|
||||
{
|
||||
with_main_interval(
|
||||
window.from,
|
||||
axis,
|
||||
main_start(window.to, axis),
|
||||
main_end(window.to, axis),
|
||||
)
|
||||
} else if main_start(window.to, axis) < main_start(window.from, axis) {
|
||||
with_main_interval(
|
||||
window.from,
|
||||
axis,
|
||||
main_end(window.from, axis) - target_size,
|
||||
main_end(window.from, axis),
|
||||
)
|
||||
} else {
|
||||
with_main_interval(
|
||||
window.from,
|
||||
axis,
|
||||
main_start(window.from, axis),
|
||||
main_start(window.from, axis) + target_size,
|
||||
)
|
||||
};
|
||||
push_step(&mut phase1, window.node_id, window.from, after_main_scale);
|
||||
if orth_changes {
|
||||
push_step(&mut phase3, window.node_id, after_main_scale, window.to);
|
||||
orth_from = after_main_scale;
|
||||
if main_start(after_main_scale, axis) != main_start(window.to, axis)
|
||||
|| main_end(after_main_scale, axis) != main_end(window.to, axis)
|
||||
{
|
||||
let after_move = with_main_interval(
|
||||
after_main_scale,
|
||||
axis,
|
||||
main_start(window.to, axis),
|
||||
main_end(window.to, axis),
|
||||
);
|
||||
push_step(&mut phase2, window.node_id, after_main_scale, after_move);
|
||||
orth_from = after_move;
|
||||
}
|
||||
} else if orth_changes {
|
||||
push_step(&mut phase3, window.node_id, window.from, window.to);
|
||||
}
|
||||
if orth_changes {
|
||||
push_step(&mut phase3, window.node_id, orth_from, window.to);
|
||||
}
|
||||
}
|
||||
if phase1.is_empty() || phase2.is_empty() || phase3.is_empty() {
|
||||
|
|
@ -1372,6 +1424,7 @@ fn single_action_reason(action: PhaseAction) -> PhaseReason {
|
|||
fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
|
||||
MultiphaseRequest {
|
||||
bounds: request.bounds,
|
||||
clearance: request.clearance,
|
||||
windows: request
|
||||
.windows
|
||||
.iter()
|
||||
|
|
@ -1686,7 +1739,11 @@ mod tests {
|
|||
MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy),
|
||||
));
|
||||
}
|
||||
MultiphaseRequest { bounds, windows }
|
||||
MultiphaseRequest {
|
||||
bounds,
|
||||
windows,
|
||||
clearance: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) {
|
||||
|
|
@ -1739,7 +1796,11 @@ mod tests {
|
|||
.map(|window| window.from.union(window.to))
|
||||
.reduce(|bounds, rect| bounds.union(rect))
|
||||
.unwrap_or_else(|| rect(0, 0, 1, 1));
|
||||
MultiphaseRequest { bounds, windows }
|
||||
MultiphaseRequest {
|
||||
bounds,
|
||||
windows,
|
||||
clearance: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> {
|
||||
|
|
@ -1765,6 +1826,20 @@ mod tests {
|
|||
strategy: PlanStrategy::SingleAction,
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
RejectedStrategy {
|
||||
direction,
|
||||
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
RejectedStrategy {
|
||||
direction,
|
||||
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Vertical,
|
||||
},
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
RejectedStrategy {
|
||||
direction,
|
||||
strategy: PlanStrategy::HierarchyOrderedScales,
|
||||
|
|
@ -1798,20 +1873,6 @@ mod tests {
|
|||
},
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
RejectedStrategy {
|
||||
direction,
|
||||
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
RejectedStrategy {
|
||||
direction,
|
||||
strategy: PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Vertical,
|
||||
},
|
||||
reason: MultiphasePlanFailure::NoPattern,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -1916,6 +1977,38 @@ mod tests {
|
|||
assert!(validate_plan_continuous(&req, &plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_lanes_respect_requested_clearance() {
|
||||
let mut req = request(vec![
|
||||
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
||||
window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)),
|
||||
]);
|
||||
req.clearance = 10;
|
||||
|
||||
let plan = plan_no_overlap(&req).unwrap();
|
||||
assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 45));
|
||||
assert_eq!(step_to(&plan, 0, id(2)), rect(100, 55, 200, 100));
|
||||
assert!(validate_plan_continuous(&req, &plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swap_lanes_tolerate_stationary_siblings_in_request() {
|
||||
let req = request(vec![
|
||||
window(1, rect(0, 0, 100, 100), rect(100, 0, 200, 100)),
|
||||
window(2, rect(100, 0, 200, 100), rect(0, 0, 100, 100)),
|
||||
window(3, rect(200, 0, 300, 100), rect(200, 0, 300, 100)),
|
||||
]);
|
||||
|
||||
let planned = plan_no_overlap_explained(&req).unwrap();
|
||||
assert_eq!(
|
||||
planned.explanation.strategy,
|
||||
PlanStrategy::SwapLanes {
|
||||
axis: PhaseAxis::Horizontal,
|
||||
}
|
||||
);
|
||||
assert!(validate_plan_continuous(&req, &planned.plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vertical_swap_lanes_follow_motion_direction_not_node_id() {
|
||||
let req = request(vec![
|
||||
|
|
@ -2232,6 +2325,59 @@ mod tests {
|
|||
assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stack_extraction_with_resized_moving_child_still_moves_before_growth() {
|
||||
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, 1, 1],
|
||||
vec![leaf(1), leaf(2), leaf(3)],
|
||||
);
|
||||
let req = generated_request(&old, &new, rect(0, 0, 300, 120));
|
||||
let planned = plan_no_overlap_explained(&req).unwrap();
|
||||
let plan = &planned.plan;
|
||||
|
||||
assert_eq!(
|
||||
planned.explanation.strategy,
|
||||
PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Horizontal,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
actions(plan),
|
||||
vec![
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Move,
|
||||
axis: PhaseAxis::Horizontal,
|
||||
},
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
axis: PhaseAxis::Vertical,
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 120));
|
||||
assert_eq!(step_to(plan, 0, id(2)), rect(200, 0, 300, 60));
|
||||
assert_eq!(step_to(plan, 0, id(3)), rect(200, 60, 300, 120));
|
||||
assert_eq!(step_to(plan, 1, id(2)), rect(100, 0, 200, 60));
|
||||
assert_eq!(step_to(plan, 2, id(2)), rect(100, 0, 200, 120));
|
||||
assert_eq!(step_to(plan, 2, id(3)), rect(200, 0, 300, 120));
|
||||
assert!(validate_plan_continuous(&req, plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bounded_generated_supported_split_tree_corpus_is_deterministic() {
|
||||
let mut cases = vec![];
|
||||
|
|
@ -2543,6 +2689,7 @@ mod tests {
|
|||
fn diagnostics_report_shrink_bound_rejections() {
|
||||
let req = MultiphaseRequest {
|
||||
bounds: rect(0, 0, 400, 100),
|
||||
clearance: 0,
|
||||
windows: vec![
|
||||
MultiphaseWindow {
|
||||
node_id: id(1),
|
||||
|
|
|
|||
|
|
@ -1650,6 +1650,12 @@ impl State {
|
|||
}
|
||||
}
|
||||
|
||||
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],
|
||||
|
|
@ -1671,6 +1677,7 @@ impl State {
|
|||
let request = MultiphaseRequest {
|
||||
bounds,
|
||||
windows: request_windows,
|
||||
clearance: self.layout_animation_clearance(),
|
||||
};
|
||||
let plan = match plan_no_overlap_with_diagnostics(&request) {
|
||||
Ok(plan) => plan,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue