aboutsummaryrefslogtreecommitdiff
path: root/src/layout
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2025-08-12 22:34:13 +0300
committerIvan Molodetskikh <yalterz@gmail.com>2025-08-14 15:58:59 +0300
commitea438b21e933d45672e80ca04db42eb54050fcca (patch)
treecbe29285bb7d69e8720e78145dcb9b1724018728 /src/layout
parent42bd107795771593ac16e26b02fae15043e5e121 (diff)
downloadniri-ea438b21e933d45672e80ca04db42eb54050fcca.tar.gz
niri-ea438b21e933d45672e80ca04db42eb54050fcca.tar.bz2
niri-ea438b21e933d45672e80ca04db42eb54050fcca.zip
layout/tests: Add column resize animation tests
Diffstat (limited to 'src/layout')
-rw-r--r--src/layout/tests.rs2
-rw-r--r--src/layout/tests/animations.rs909
2 files changed, 911 insertions, 0 deletions
diff --git a/src/layout/tests.rs b/src/layout/tests.rs
index d5d29683..04823a54 100644
--- a/src/layout/tests.rs
+++ b/src/layout/tests.rs
@@ -11,6 +11,8 @@ use smithay::utils::Rectangle;
use super::*;
+mod animations;
+
impl<W: LayoutElement> Default for Layout<W> {
fn default() -> Self {
Self::with_options(Clock::with_time(Duration::ZERO), Default::default())
diff --git a/src/layout/tests/animations.rs b/src/layout/tests/animations.rs
new file mode 100644
index 00000000..817304df
--- /dev/null
+++ b/src/layout/tests/animations.rs
@@ -0,0 +1,909 @@
+use std::fmt::Write as _;
+
+use insta::assert_snapshot;
+use niri_config::{AnimationCurve, AnimationKind, EasingParams};
+
+use super::*;
+
+fn format_tiles(layout: &Layout<TestWindow>) -> String {
+ let mut buf = String::new();
+ let ws = layout.active_workspace().unwrap();
+ let mut tiles: Vec<_> = ws.tiles_with_render_positions().collect();
+
+ // We sort by id since that gives us a consistent order (from first opened to last), but we
+ // don't print the id since it's nondeterministic (the id is a global counter across all
+ // running tests in the same binary).
+ tiles.sort_by_key(|(tile, _, _)| tile.window().id());
+ for (tile, pos, _visible) in tiles {
+ let Size { w, h, .. } = tile.animated_tile_size();
+ let Point { x, y, .. } = pos;
+ writeln!(&mut buf, "{w:>3.0} × {h:>3.0} at x:{x:>3.0} y:{y:>3.0}").unwrap();
+ }
+ buf
+}
+
+fn make_options() -> Options {
+ const LINEAR: AnimationKind = AnimationKind::Easing(EasingParams {
+ duration_ms: 1000,
+ curve: AnimationCurve::Linear,
+ });
+
+ let mut options = Options {
+ gaps: 0.0,
+ ..Options::default()
+ };
+ options.animations.window_resize.anim.kind = LINEAR;
+ options.animations.window_movement.0.kind = LINEAR;
+
+ options
+}
+
+fn set_up_two_in_column() -> Layout<TestWindow> {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::ConsumeWindowIntoColumn,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ ];
+
+ check_ops_with_options(make_options(), &ops)
+}
+
+#[test]
+fn height_resize_animates_next_y() {
+ let mut layout = set_up_two_in_column();
+
+ let ops = [
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::AdjustFixed(-50),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 50)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed yet, so we're at the initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Top window is half-resized at 75 px tall, bottom window is at y=75 matching it.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 75 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 75
+ ");
+
+ // Advance the time to completion.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Final state at 50 px.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 50 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 50
+ ");
+}
+
+#[test]
+fn clientside_height_change_doesnt_animate() {
+ let mut layout = set_up_two_in_column();
+
+ // The initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ let ops = [
+ // The top window shrinks by itself, without a niri-issued resize.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 50)),
+ },
+ // This does not start any animations.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed yet, but we are at the final state right away.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 50 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 50
+ ");
+}
+
+#[test]
+fn height_resize_and_back() {
+ let mut layout = set_up_two_in_column();
+
+ // The initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ let ops = [
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 500 },
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // Top window is half-resized at 150 px tall, bottom window is at y=150 matching it.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 150 at x: 0 y: 0
+ 200 × 200 at x: 0 y:150
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This starts a new resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed yet, and we expect no animation jumps, so this state matches the last.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 150 at x: 0 y: 0
+ 200 × 200 at x: 0 y:150
+ ");
+
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Halfway through at 125px.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 125 at x: 0 y: 0
+ 200 × 200 at x: 0 y:125
+ ");
+
+ // Advance the time to completion.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Final state back at 100px.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+}
+
+#[test]
+fn height_resize_and_cancel() {
+ let mut layout = set_up_two_in_column();
+
+ // The initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ let ops = [
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time slightly.
+ Op::AdvanceAnimations { msec_delta: 50 },
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // Top window is half-resized at 105 px tall, bottom window is at y=105 matching it.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 105 at x: 0 y: 0
+ 200 × 200 at x: 0 y:105
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This cancels the resize animation since the change of 5 px is less than the resize
+ // animation threshold.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // Since the resize animation is cancelled, the height goes to the new value immediately, and
+ // the Y position should also go to 100 immediately, cancelling the movement.
+ //
+ // FIXME: right now, the Y position jumps to 5 and will continue animating to 100 from there
+ // because cancelling the resize anim doesn't cancel the induced Y movement.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 5
+ ");
+}
+
+#[test]
+fn height_resize_and_back_during_another_y_anim() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ ];
+ let mut layout = check_ops_with_options(make_options(), &ops);
+
+ // The initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Consume second window into column, starting the X/Y move anim down.
+ Op::ConsumeWindowIntoColumn.apply(&mut layout);
+
+ // No time had passed, so no change in coordinates yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Second window halfway to the bottom.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 50 y: 50
+ ");
+
+ let ops = [
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed, so no change in state yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 50 y: 50
+ ");
+
+ // Advance the time a bit.
+ Op::AdvanceAnimations { msec_delta: 200 }.apply(&mut layout);
+
+ // X changed by 20, but y changed by 30 since the Y movement from the resize compounds with the
+ // Y movement from consume-into-column.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 120 at x: 0 y: 0
+ 200 × 200 at x: 30 y: 80
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed, so no change in state yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 120 at x: 0 y: 0
+ 200 × 200 at x: 30 y: 80
+ ");
+
+ // Advance the time a bit. Both resize and consume movement are still ongoing.
+ Op::AdvanceAnimations { msec_delta: 200 }.apply(&mut layout);
+
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 116 at x: 0 y: 0
+ 200 × 200 at x: 10 y: 84
+ ");
+
+ // Advance the time to complete the consume movement.
+ Op::AdvanceAnimations { msec_delta: 100 }.apply(&mut layout);
+
+ // The Y position is still lower than the height since the window started the resize-induced Y
+ // movement high up.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 114 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 86
+ ");
+
+ // Advance the time to complete the resize.
+ Op::AdvanceAnimations { msec_delta: 700 }.apply(&mut layout);
+
+ // Final state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+}
+
+#[test]
+fn height_resize_and_cancel_during_another_y_anim() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ ];
+ let mut layout = check_ops_with_options(make_options(), &ops);
+
+ // The initial state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Consume second window into column, starting the X/Y move anim down.
+ Op::ConsumeWindowIntoColumn.apply(&mut layout);
+
+ // No time had passed, so no change in coordinates yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 500 }.apply(&mut layout);
+
+ // Second window halfway to the bottom.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 50 y: 50
+ ");
+
+ let ops = [
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time slightly.
+ Op::AdvanceAnimations { msec_delta: 50 },
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // X changed by 5, but y changed by 8 since the Y movement from the resize compounds with the Y
+ // movement from consume-into-column.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 105 at x: 0 y: 0
+ 200 × 200 at x: 45 y: 58
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This cancels the resize animation since the change of 5 px is less than the resize
+ // animation threshold.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // Since the resize anim was cancelled, second window's Y should jump a little to go back to
+ // its original trajectory.
+ //
+ // FIXME: right now, the Y position jumps and will continue to animate from there because
+ // cancelling the resize anim doesn't cancel the induced Y movement.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 45 y:-43
+ ");
+
+ // Advance the time to complete the consume movement.
+ Op::AdvanceAnimations { msec_delta: 450 }.apply(&mut layout);
+
+ // Final state. Y should be at 100 since the resize-induced Y anim was cancelled.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 25
+ ");
+}
+
+#[test]
+fn height_resize_before_another_y_anim_then_back() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time a bit.
+ Op::AdvanceAnimations { msec_delta: 200 },
+ ];
+ let mut layout = check_ops_with_options(make_options(), &ops);
+
+ // The resize is in progress.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 120 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Consume second window into column, starting the X/Y move anim down.
+ Op::ConsumeWindowIntoColumn.apply(&mut layout);
+
+ // No time had passed, so no change in coordinates yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 120 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Advance the time halfway.
+ Op::AdvanceAnimations { msec_delta: 600 }.apply(&mut layout);
+
+ // Second window halfway to the bottom. Since consume happened after the start of the first
+ // window's resize, the second window's Y is unaffected by it and is animating towards the
+ // final position right away.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 180 at x: 0 y: 0
+ 200 × 200 at x: 40 y:120
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // No time had passed, so no change in state yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 180 at x: 0 y: 0
+ 200 × 200 at x: 40 y:120
+ ");
+
+ // Advance the time a bit. Both resize and consume movement are still ongoing.
+ Op::AdvanceAnimations { msec_delta: 200 }.apply(&mut layout);
+
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 164 at x: 0 y: 0
+ 200 × 200 at x: 20 y:116
+ ");
+
+ // Advance the time to complete the consume movement.
+ Op::AdvanceAnimations { msec_delta: 200 }.apply(&mut layout);
+
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 148 at x: 0 y: 0
+ 200 × 200 at x: 0 y:112
+ ");
+
+ // Advance the time to complete the resize.
+ Op::AdvanceAnimations { msec_delta: 600 }.apply(&mut layout);
+
+ // Final state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+}
+
+#[test]
+fn height_resize_before_another_y_anim_then_cancel() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time a bit.
+ Op::AdvanceAnimations { msec_delta: 20 },
+ ];
+ let mut layout = check_ops_with_options(make_options(), &ops);
+
+ // The resize is in progress.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 102 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Consume second window into column, starting the X/Y move anim down.
+ Op::ConsumeWindowIntoColumn.apply(&mut layout);
+
+ // No time had passed, so no change in coordinates yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 102 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Advance the time a little.
+ Op::AdvanceAnimations { msec_delta: 20 }.apply(&mut layout);
+
+ // Second window on its way to the bottom.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 104 at x: 0 y: 0
+ 200 × 200 at x: 98 y: 4
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This cancels the resize animation since the change of 4 px is less than the resize
+ // animation threshold.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // This should cause the second window's trajectory to readjust to the new final position at
+ // 100px. Ideally without big jumps because even though the first window's actual size changed
+ // from 200 px to 100 px, the cancelled resize only shifted it from 104 px to 100 px.
+ //
+ // FIXME: currently causes a 100 px jump due to the change in the actual window size.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 98 y:-96
+ ");
+
+ // Advance the time to complete the consume movement.
+ Op::AdvanceAnimations { msec_delta: 980 }.apply(&mut layout);
+
+ // Final state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+}
+
+#[test]
+fn clientside_height_change_during_another_y_anim() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ Op::ConsumeWindowIntoColumn,
+ // Clear the animate next configure flag.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time a bit.
+ Op::AdvanceAnimations { msec_delta: 200 },
+ ];
+ let mut layout = check_ops_with_options(make_options(), &ops);
+
+ // Second window on its way to the bottom.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 80 y: 20
+ ");
+
+ let ops = [
+ // The top window suddenly grows.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // This should cause the second window's trajectory to readjust to the new final position at
+ // 200px. Ideally without big jumps.
+ //
+ // FIXME: currently causes a 100 px jump due to the change in the window size.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 200 at x: 0 y: 0
+ 200 × 200 at x: 80 y:120
+ ");
+
+ // Advance the time to complete the consume movement.
+ Op::AdvanceAnimations { msec_delta: 800 }.apply(&mut layout);
+
+ // Final state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 200 at x: 0 y: 0
+ 200 × 200 at x: 0 y:200
+ ");
+}
+
+#[test]
+fn height_resize_cancel_with_stationary_second_window() {
+ let ops = [
+ Op::AddOutput(1),
+ Op::AddWindow {
+ params: TestWindowParams::new(1),
+ },
+ Op::AddWindow {
+ params: TestWindowParams::new(2),
+ },
+ Op::FocusColumnLeft,
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ Op::SetForcedSize {
+ id: 2,
+ size: Some(Size::new(200, 200)),
+ },
+ Op::Communicate(1),
+ Op::Communicate(2),
+ Op::CompleteAnimations,
+ // Issue a resize.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(200),
+ },
+ // The top window grows in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 200)),
+ },
+ // This starts the resize animation.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ // Advance the time a bit.
+ Op::AdvanceAnimations { msec_delta: 20 },
+ ];
+ let mut options = make_options();
+ // Window movement will happen instantly.
+ options.animations.window_movement.0.off = true;
+ let mut layout = check_ops_with_options(options, &ops);
+
+ // The resize is in progress.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 102 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Consume second window into column, starting the X/Y move anim down.
+ Op::ConsumeWindowIntoColumn.apply(&mut layout);
+
+ // No time had passed, so no change in coordinates yet.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 102 at x: 0 y: 0
+ 200 × 200 at x:100 y: 0
+ ");
+
+ // Advance the time a little.
+ Op::AdvanceAnimations { msec_delta: 20 }.apply(&mut layout);
+
+ // The window movement anim is off, so the second window is already at the bottom. Since
+ // consume started after the resize, the second window is unaffected by the resize-induced Y
+ // movement, and sits at the final position at 200 px.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 104 at x: 0 y: 0
+ 200 × 200 at x: 0 y:200
+ ");
+
+ let ops = [
+ // Issue a resize back.
+ Op::SetWindowHeight {
+ id: None,
+ change: SizeChange::SetFixed(100),
+ },
+ // The top window shrinks in response, the bottom remains as is.
+ Op::SetForcedSize {
+ id: 1,
+ size: Some(Size::new(100, 100)),
+ },
+ // This cancels the resize animation since the change of 4 px is less than the resize
+ // animation threshold.
+ Op::Communicate(1),
+ Op::Communicate(2),
+ ];
+ check_ops_on_layout(&mut layout, &ops);
+
+ // This causes the second window to jump down, which is correct because it hadn't been in an
+ // animation, and as far as it's concerned, this is the same case as a window just deciding to
+ // do a clientside resize on its own, which is not animated.
+ //
+ // Since the resize is also cancelled, this is the final state.
+ assert_snapshot!(format_tiles(&layout), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+}