aboutsummaryrefslogtreecommitdiff
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
commit7a6be2a923d8d19d544bb6c52cf172b89ca8298c (patch)
tree4f06c5b1bb4b26083ebb0d5e848b4c1ea5b2ca6d
parentbba9ca1fa273ec57748f2bfabcbf6b19e9200512 (diff)
downloadniri-7a6be2a923d8d19d544bb6c52cf172b89ca8298c.tar.gz
niri-7a6be2a923d8d19d544bb6c52cf172b89ca8298c.tar.bz2
niri-7a6be2a923d8d19d544bb6c52cf172b89ca8298c.zip
Add basic client-server resize animation tests
-rw-r--r--src/tests/animations.rs195
-rw-r--r--src/tests/mod.rs1
2 files changed, 196 insertions, 0 deletions
diff --git a/src/tests/animations.rs b/src/tests/animations.rs
new file mode 100644
index 00000000..d930d254
--- /dev/null
+++ b/src/tests/animations.rs
@@ -0,0 +1,195 @@
+use std::fmt::Write as _;
+use std::time::Duration;
+
+use insta::assert_snapshot;
+use niri_config::{AnimationCurve, AnimationKind, Config, EasingParams, FloatOrInt};
+use niri_ipc::SizeChange;
+use smithay::utils::{Point, Size};
+use wayland_client::protocol::wl_surface::WlSurface;
+
+use super::client::ClientId;
+use super::*;
+use crate::niri::Niri;
+
+fn format_tiles(niri: &Niri) -> String {
+ let mut buf = String::new();
+ let ws = niri.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().get());
+ 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 create_window(f: &mut Fixture, id: ClientId, w: u16, h: u16) -> WlSurface {
+ let window = f.client(id).create_window();
+ let surface = window.surface.clone();
+ window.commit();
+ f.roundtrip(id);
+
+ let window = f.client(id).window(&surface);
+ window.attach_new_buffer();
+ window.set_size(w, h);
+ window.ack_last_and_commit();
+ f.roundtrip(id);
+
+ surface
+}
+
+fn complete_animations(niri: &mut Niri) {
+ niri.clock.set_complete_instantly(true);
+ niri.advance_animations();
+ niri.clock.set_complete_instantly(false);
+}
+
+fn set_time(niri: &mut Niri, time: Duration) {
+ // This is a bit involved because we're dealing with an AdjustableClock that maintains its own
+ // internal current_time.
+
+ // First, reset current_time to zero by matching unadjusted time to it (at rate 0.0), then
+ // setting unadjusted time to zero at rate 1.0 (causing current_time to also go to zero).
+ let now = niri.clock.now();
+ niri.clock.set_unadjusted(now);
+ let _ = niri.clock.now();
+ niri.clock.set_unadjusted(Duration::ZERO);
+ niri.clock.set_rate(1.0);
+ let _ = niri.clock.now();
+
+ // Now, set the desired time at rate 1.0.
+ niri.clock.set_unadjusted(time);
+ let _ = niri.clock.now();
+
+ // Freeze the clock so that clear() inside the niri loop callback followed by some get()
+ // doesn't replace it with the monotonic time.
+ niri.clock.set_rate(0.0);
+}
+
+// Sets up a fixture with linear animations, a renderer, and an output.
+fn set_up() -> Fixture {
+ const LINEAR: AnimationKind = AnimationKind::Easing(EasingParams {
+ duration_ms: 1000,
+ curve: AnimationCurve::Linear,
+ });
+
+ let mut config = Config::default();
+ config.layout.gaps = FloatOrInt(0.0);
+ config.animations.window_resize.anim.kind = LINEAR;
+ config.animations.window_movement.0.kind = LINEAR;
+
+ let mut f = Fixture::with_config(config);
+ f.niri_state().backend.headless().add_renderer().unwrap();
+ f.add_output(1, (1920, 1080));
+
+ f
+}
+
+fn set_up_two_in_column() -> (Fixture, ClientId, WlSurface, WlSurface) {
+ let mut f = set_up();
+
+ let id = f.add_client();
+
+ let surface1 = create_window(&mut f, id, 100, 100);
+ let surface2 = create_window(&mut f, id, 200, 200);
+ f.double_roundtrip(id);
+
+ let _ = f.client(id).window(&surface1).recent_configures();
+ let _ = f.client(id).window(&surface2).recent_configures();
+
+ // Consume into one column.
+ f.niri().layout.focus_left();
+ f.niri().layout.consume_into_column();
+ f.double_roundtrip(id);
+
+ // Commit for the column consume.
+ let window = f.client(id).window(&surface1);
+ window.ack_last_and_commit();
+
+ let window = f.client(id).window(&surface2);
+ window.ack_last_and_commit();
+
+ f.double_roundtrip(id);
+
+ set_time(f.niri(), Duration::ZERO);
+ complete_animations(f.niri());
+
+ (f, id, surface1, surface2)
+}
+
+#[test]
+fn height_resize_animates_next_y() {
+ let (mut f, id, surface1, surface2) = set_up_two_in_column();
+
+ // Issue a resize.
+ f.niri()
+ .layout
+ .set_window_height(None, SizeChange::AdjustFixed(-50));
+ f.double_roundtrip(id);
+
+ // The top window shrinks in response, the bottom remains as is.
+ let window = f.client(id).window(&surface1);
+ window.set_size(100, 50);
+ window.ack_last_and_commit();
+ let window = f.client(id).window(&surface2);
+ window.ack_last_and_commit();
+
+ // This starts the resize animation for the top window and the Y move for the bottom.
+ f.roundtrip(id);
+
+ // No time had passed yet, so we're at the initial state.
+ assert_snapshot!(format_tiles(f.niri()), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ // Advance the time halfway.
+ set_time(f.niri(), Duration::from_millis(500));
+ f.niri().advance_animations();
+
+ // Top window is half-resized at 75 px tall, bottom window is at y=75 matching it.
+ assert_snapshot!(format_tiles(f.niri()), @r"
+ 100 × 75 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 75
+ ");
+
+ // Advance the time to completion.
+ set_time(f.niri(), Duration::from_millis(1000));
+ f.niri().advance_animations();
+
+ // Final state at 50 px.
+ assert_snapshot!(format_tiles(f.niri()), @r"
+ 100 × 50 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 50
+ ");
+}
+
+#[test]
+fn clientside_height_change_doesnt_animate() {
+ let (mut f, id, surface1, _surface2) = set_up_two_in_column();
+
+ // The initial state.
+ assert_snapshot!(format_tiles(f.niri()), @r"
+ 100 × 100 at x: 0 y: 0
+ 200 × 200 at x: 0 y:100
+ ");
+
+ // The top window shrinks by itself, without a niri-issued resize.
+ let window = f.client(id).window(&surface1);
+ window.set_size(100, 50);
+ window.commit();
+
+ // This does not start any animations.
+ f.roundtrip(id);
+
+ // No time had passed yet, but we are at the final state right away.
+ assert_snapshot!(format_tiles(f.niri()), @r"
+ 100 × 50 at x: 0 y: 0
+ 200 × 200 at x: 0 y: 50
+ ");
+}
diff --git a/src/tests/mod.rs b/src/tests/mod.rs
index d57ce22b..cdd04d9c 100644
--- a/src/tests/mod.rs
+++ b/src/tests/mod.rs
@@ -4,6 +4,7 @@ mod client;
mod fixture;
mod server;
+mod animations;
mod floating;
mod fullscreen;
mod layer_shell;