aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIvan Molodetskikh <yalterz@gmail.com>2024-02-06 09:01:26 +0400
committerIvan Molodetskikh <yalterz@gmail.com>2024-02-06 09:40:45 +0400
commit122afff7d10ac96250f428a3f06dca6288d3d0b8 (patch)
tree1c6d89f1ee1cf08c42be102907a9e4594dedbb1a
parentd2a4e6a0cbc839813c6d4ef68b75820d87cfc5b0 (diff)
downloadniri-122afff7d10ac96250f428a3f06dca6288d3d0b8.tar.gz
niri-122afff7d10ac96250f428a3f06dca6288d3d0b8.tar.bz2
niri-122afff7d10ac96250f428a3f06dca6288d3d0b8.zip
Add niri-visual-tests
-rw-r--r--.github/workflows/ci.yml35
-rw-r--r--Cargo.lock236
-rw-r--r--Cargo.toml9
-rw-r--r--niri-visual-tests/Cargo.toml18
-rw-r--r--niri-visual-tests/README.md14
-rw-r--r--niri-visual-tests/resources/style.css3
-rw-r--r--niri-visual-tests/src/cases/mod.rs21
-rw-r--r--niri-visual-tests/src/cases/tile.rs83
-rw-r--r--niri-visual-tests/src/cases/window.rs57
-rw-r--r--niri-visual-tests/src/main.rs142
-rw-r--r--niri-visual-tests/src/smithay_view.rs245
-rw-r--r--niri-visual-tests/src/test_window.rs206
12 files changed, 1062 insertions, 7 deletions
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b57abcbb..3828a500 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -52,10 +52,35 @@ jobs:
run: cargo build ${{ matrix.release-flag }} --features profile-with-tracy
- name: Build Tests
- run: cargo test --no-run --all ${{ matrix.release-flag }}
+ run: cargo test --no-run --all --exclude niri-visual-tests ${{ matrix.release-flag }}
- name: Test
- run: cargo test --all ${{ matrix.release-flag }} -- --nocapture
+ run: cargo test --all --exclude niri-visual-tests ${{ matrix.release-flag }} -- --nocapture
+
+ visual-tests:
+ strategy:
+ fail-fast: false
+
+ name: visual tests
+ runs-on: ubuntu-22.04
+ container: ubuntu:23.10
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ show-progress: false
+
+ - name: Install dependencies
+ run: |
+ apt-get update -y
+ apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - uses: Swatinem/rust-cache@v2
+
+ - name: Build
+ run: cargo build --package niri-visual-tests
clippy:
strategy:
@@ -73,7 +98,7 @@ jobs:
- name: Install dependencies
run: |
apt-get update -y
- apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev
+ apt-get install -y curl gcc clang libudev-dev libgbm-dev libxkbcommon-dev libegl1-mesa-dev libwayland-dev libinput-dev libdbus-1-dev libsystemd-dev libseat-dev libpipewire-0.3-dev libpango1.0-dev libadwaita-1-dev
- uses: dtolnay/rust-toolchain@stable
with:
@@ -113,8 +138,8 @@ jobs:
- name: Install dependencies
run: |
sudo dnf update -y
- sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang
+ sudo dnf install -y cargo gcc libudev-devel libgbm-devel libxkbcommon-devel wayland-devel libinput-devel dbus-devel systemd-devel libseat-devel pipewire-devel pango-devel cairo-gobject-devel clang libadwaita-devel
- uses: Swatinem/rust-cache@v2
- - run: cargo build
+ - run: cargo build --all
diff --git a/Cargo.lock b/Cargo.lock
index db38d427..fd405670 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -995,6 +995,16 @@ dependencies = [
]
[[package]]
+name = "field-offset"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
+dependencies = [
+ "memoffset 0.9.0",
+ "rustc_version",
+]
+
+[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1171,6 +1181,63 @@ dependencies = [
]
[[package]]
+name = "gdk-pixbuf"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c311c47800051b87de1335e8792774d7cec551c91a0a3d109ab21d76b36f208f"
+dependencies = [
+ "gdk-pixbuf-sys",
+ "gio",
+ "glib",
+ "libc",
+]
+
+[[package]]
+name = "gdk-pixbuf-sys"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcbd04c1b2c4834cc008b4828bc917d062483b88d26effde6342e5622028f96"
+dependencies = [
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gdk4"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6771942f85a2beaa220c64739395e4401b9fab4a52aba9b503fa1e6ed4d4d806"
+dependencies = [
+ "cairo-rs",
+ "gdk-pixbuf",
+ "gdk4-sys",
+ "gio",
+ "glib",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gdk4-sys"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1eb95854fab65072023a7814434f003db571d6e45c287c0b0c540c1c78bdf6ae"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "pango-sys",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
name = "generator"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1339,6 +1406,114 @@ dependencies = [
]
[[package]]
+name = "graphene-rs"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147827e4f506f8073ac3ec5b28cc2255bdf3abc30f5b4e101a80506eebe11d2c"
+dependencies = [
+ "glib",
+ "graphene-sys",
+ "libc",
+]
+
+[[package]]
+name = "graphene-sys"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "236ed66cc9b18d8adf233716f75de803d0bf6fc806f60d14d948974a12e240d0"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "pkg-config",
+ "system-deps",
+]
+
+[[package]]
+name = "gsk4"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e8ce8dee0fd87a11002214b1204ff18c9272fbd530408f0884a0f9b25dc31de"
+dependencies = [
+ "cairo-rs",
+ "gdk4",
+ "glib",
+ "graphene-rs",
+ "gsk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gsk4-sys"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2660a652da5b662d43924df19ba40d73f015ed427329ef51d2b1360a4e0dc0e4"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk4-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
+name = "gtk4"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d26ffa3ec6316ccaa1df62d3e7f5bae1637c0acbb43f250fabef38319f73c64"
+dependencies = [
+ "cairo-rs",
+ "field-offset",
+ "futures-channel",
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "graphene-rs",
+ "gsk4",
+ "gtk4-macros",
+ "gtk4-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "gtk4-macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8b86439e9896f6f3f47c3d8077c5c8205174078760afdabd9098a8e9e937d97"
+dependencies = [
+ "anyhow",
+ "proc-macro-crate 3.1.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "gtk4-sys"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2abc0a6d356d59a3806021829ce6ed3e70bba3509b41a535fedcb09fae13fbc0"
+dependencies = [
+ "cairo-sys-rs",
+ "gdk-pixbuf-sys",
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "graphene-sys",
+ "gsk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
name = "hashbrown"
version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1546,6 +1721,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
+name = "libadwaita"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91b4990248b9e1ec5e72094a2ccaea70ec3809f88f6fd52192f2af306b87c5d9"
+dependencies = [
+ "gdk-pixbuf",
+ "gdk4",
+ "gio",
+ "glib",
+ "gtk4",
+ "libadwaita-sys",
+ "libc",
+ "pango",
+]
+
+[[package]]
+name = "libadwaita-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23a748e4e92be1265cd9e93d569c0b5dfc7814107985aa6743d670ab281ea1a8"
+dependencies = [
+ "gdk4-sys",
+ "gio-sys",
+ "glib-sys",
+ "gobject-sys",
+ "gtk4-sys",
+ "libc",
+ "pango-sys",
+ "system-deps",
+]
+
+[[package]]
name = "libc"
version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1897,6 +2104,20 @@ dependencies = [
]
[[package]]
+name = "niri-visual-tests"
+version = "0.1.1"
+dependencies = [
+ "anyhow",
+ "gtk4",
+ "libadwaita",
+ "niri",
+ "niri-config",
+ "smithay",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
name = "nix"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2538,6 +2759,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
name = "rustix"
version = "0.37.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2616,6 +2846,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32"
[[package]]
+name = "semver"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97ed7a9823b74f99c7742f5336af7be5ecd3eeafcb1507d1fa93347b1d589b0"
+
+[[package]]
name = "serde"
version = "1.0.196"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index f0dbdc8e..64f1d41a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,3 +1,6 @@
+[workspace]
+members = ["niri-visual-tests"]
+
[workspace.package]
version = "0.1.1"
description = "A scrollable-tiling Wayland compositor"
@@ -7,10 +10,12 @@ edition = "2021"
repository = "https://github.com/YaLTeR/niri"
[workspace.dependencies]
+anyhow = "1.0.79"
bitflags = "2.4.2"
directories = "5.0.1"
serde = { version = "1.0.196", features = ["derive"] }
tracing = { version = "0.1.40", features = ["max_level_trace", "release_max_level_debug"] }
+tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
tracy-client = { version = "0.16.5", default-features = false }
[workspace.dependencies.smithay]
@@ -35,7 +40,7 @@ readme = "README.md"
keywords = ["wayland", "compositor", "tiling", "smithay", "wm"]
[dependencies]
-anyhow = { version = "1.0.79" }
+anyhow.workspace = true
arrayvec = "0.7.4"
async-channel = { version = "2.1.1", optional = true }
async-io = { version = "1.13.0", optional = true }
@@ -61,7 +66,7 @@ sd-notify = "0.4.1"
serde.workspace = true
serde_json = "1.0.113"
smithay-drm-extras.workspace = true
-tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
+tracing-subscriber.workspace = true
tracing.workspace = true
tracy-client.workspace = true
url = { version = "2.5.0", optional = true }
diff --git a/niri-visual-tests/Cargo.toml b/niri-visual-tests/Cargo.toml
new file mode 100644
index 00000000..47825e59
--- /dev/null
+++ b/niri-visual-tests/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "niri-visual-tests"
+version.workspace = true
+description.workspace = true
+authors.workspace = true
+license.workspace = true
+edition.workspace = true
+repository.workspace = true
+
+[dependencies]
+adw = { version = "0.6.0", package = "libadwaita", features = ["v1_4"] }
+anyhow.workspace = true
+gtk = { version = "0.8.0", package = "gtk4", features = ["v4_12"] }
+niri = { version = "0.1.1", path = ".." }
+niri-config = { version = "0.1.1", path = "../niri-config" }
+smithay.workspace = true
+tracing.workspace = true
+tracing-subscriber.workspace = true
diff --git a/niri-visual-tests/README.md b/niri-visual-tests/README.md
new file mode 100644
index 00000000..34d5ca1e
--- /dev/null
+++ b/niri-visual-tests/README.md
@@ -0,0 +1,14 @@
+# niri-visual-tests
+
+> [!NOTE]
+>
+> This is a development-only app, you shouldn't package it.
+
+This app contains a number of hard-coded test scenarios for visual inspection.
+It uses the real niri layout and rendering code, but with mock windows instead of Wayland clients.
+The idea is to go through the test scenarios and check that everything *looks* right.
+
+## Running
+
+You will need recent GTK and libadwaita.
+Then, `cargo run`.
diff --git a/niri-visual-tests/resources/style.css b/niri-visual-tests/resources/style.css
new file mode 100644
index 00000000..4f74d041
--- /dev/null
+++ b/niri-visual-tests/resources/style.css
@@ -0,0 +1,3 @@
+.anim-control-bar {
+ padding: 12px;
+}
diff --git a/niri-visual-tests/src/cases/mod.rs b/niri-visual-tests/src/cases/mod.rs
new file mode 100644
index 00000000..b7d71cd0
--- /dev/null
+++ b/niri-visual-tests/src/cases/mod.rs
@@ -0,0 +1,21 @@
+use std::time::Duration;
+
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Physical, Size};
+
+pub mod tile;
+pub mod window;
+
+pub trait TestCase {
+ fn resize(&mut self, width: i32, height: i32);
+ fn are_animations_ongoing(&self) -> bool {
+ false
+ }
+ fn advance_animations(&mut self, _current_time: Duration) {}
+ fn render(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>>;
+}
diff --git a/niri-visual-tests/src/cases/tile.rs b/niri-visual-tests/src/cases/tile.rs
new file mode 100644
index 00000000..cdf1e5f6
--- /dev/null
+++ b/niri-visual-tests/src/cases/tile.rs
@@ -0,0 +1,83 @@
+use std::rc::Rc;
+use std::time::Duration;
+
+use niri::layout::tile::Tile;
+use niri::layout::Options;
+use niri_config::Color;
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Point, Scale, Size};
+
+use super::TestCase;
+use crate::test_window::TestWindow;
+
+pub struct JustTile {
+ window: TestWindow,
+ tile: Tile<TestWindow>,
+}
+
+impl JustTile {
+ pub fn freeform(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::freeform(0);
+ let mut rv = Self::with_window(window);
+ rv.tile.request_tile_size(size);
+ rv.window.communicate();
+ rv
+ }
+
+ pub fn fixed_size(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::fixed_size(0);
+ let mut rv = Self::with_window(window);
+ rv.tile.request_tile_size(size);
+ rv.window.communicate();
+ rv
+ }
+
+ pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::fixed_size(0);
+ window.set_csd_shadow_width(64);
+ let mut rv = Self::with_window(window);
+ rv.tile.request_tile_size(size);
+ rv.window.communicate();
+ rv
+ }
+
+ pub fn with_window(window: TestWindow) -> Self {
+ let options = Options {
+ border: niri_config::FocusRing {
+ off: false,
+ width: 32,
+ active_color: Color::new(255, 163, 72, 255),
+ ..Default::default()
+ },
+ ..Default::default()
+ };
+ let tile = Tile::new(window.clone(), Rc::new(options));
+ Self { window, tile }
+ }
+}
+
+impl TestCase for JustTile {
+ fn resize(&mut self, width: i32, height: i32) {
+ self.tile.request_tile_size(Size::from((width, height)));
+ self.window.communicate();
+ }
+
+ fn advance_animations(&mut self, current_time: Duration) {
+ self.tile.advance_animations(current_time, true);
+ }
+
+ fn render(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let tile_size = self.tile.tile_size().to_physical(1);
+ let location = Point::from(((size.w - tile_size.w) / 2, (size.h - tile_size.h) / 2));
+
+ self.tile
+ .render(renderer, location, Scale::from(1.))
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/cases/window.rs b/niri-visual-tests/src/cases/window.rs
new file mode 100644
index 00000000..869cbf45
--- /dev/null
+++ b/niri-visual-tests/src/cases/window.rs
@@ -0,0 +1,57 @@
+use niri::layout::LayoutElement;
+use smithay::backend::renderer::element::RenderElement;
+use smithay::backend::renderer::gles::GlesRenderer;
+use smithay::utils::{Logical, Physical, Point, Scale, Size};
+
+use super::TestCase;
+use crate::test_window::TestWindow;
+
+pub struct JustWindow {
+ window: TestWindow,
+}
+
+impl JustWindow {
+ pub fn freeform(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::freeform(0);
+ window.request_size(size);
+ window.communicate();
+ Self { window }
+ }
+
+ pub fn fixed_size(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::fixed_size(0);
+ window.request_size(size);
+ window.communicate();
+ Self { window }
+ }
+
+ pub fn fixed_size_with_csd_shadow(size: Size<i32, Logical>) -> Self {
+ let window = TestWindow::fixed_size(0);
+ window.set_csd_shadow_width(64);
+ window.request_size(size);
+ window.communicate();
+ Self { window }
+ }
+}
+
+impl TestCase for JustWindow {
+ fn resize(&mut self, width: i32, height: i32) {
+ self.window.request_size(Size::from((width, height)));
+ self.window.communicate();
+ }
+
+ fn render(
+ &mut self,
+ renderer: &mut GlesRenderer,
+ size: Size<i32, Physical>,
+ ) -> Vec<Box<dyn RenderElement<GlesRenderer>>> {
+ let win_size = self.window.size().to_physical(1);
+ let location = Point::from(((size.w - win_size.w) / 2, (size.h - win_size.h) / 2));
+
+ self.window
+ .render(renderer, location, Scale::from(1.))
+ .into_iter()
+ .map(|elem| Box::new(elem) as _)
+ .collect()
+ }
+}
diff --git a/niri-visual-tests/src/main.rs b/niri-visual-tests/src/main.rs
new file mode 100644
index 00000000..99919043
--- /dev/null
+++ b/niri-visual-tests/src/main.rs
@@ -0,0 +1,142 @@
+#[macro_use]
+extern crate tracing;
+
+use std::env;
+use std::sync::atomic::Ordering;
+
+use adw::prelude::{AdwApplicationWindowExt, NavigationPageExt};
+use cases::tile::JustTile;
+use cases::window::JustWindow;
+use gtk::prelude::{
+ AdjustmentExt, ApplicationExt, ApplicationExtManual, BoxExt, GtkWindowExt, WidgetExt,
+};
+use gtk::{gdk, gio, glib};
+use niri::animation::ANIMATION_SLOWDOWN;
+use smithay::utils::{Logical, Size};
+use smithay_view::SmithayView;
+use tracing_subscriber::EnvFilter;
+
+use crate::cases::TestCase;
+
+mod cases;
+mod smithay_view;
+mod test_window;
+
+fn main() -> glib::ExitCode {
+ let directives =
+ env::var("RUST_LOG").unwrap_or_else(|_| "niri-visual-tests=debug,niri=debug".to_owned());
+ let env_filter = EnvFilter::builder().parse_lossy(directives);
+ tracing_subscriber::fmt()
+ .compact()
+ .with_env_filter(env_filter)
+ .init();
+
+ let app = adw::Application::new(None::<&str>, gio::ApplicationFlags::NON_UNIQUE);
+ app.connect_startup(on_startup);
+ app.connect_activate(build_ui);
+ app.run()
+}
+
+fn on_startup(_app: &adw::Application) {
+ // Load our CSS.
+ let provider = gtk::CssProvider::new();
+ provider.load_from_string(include_str!("../resources/style.css"));
+ if let Some(display) = gdk::Display::default() {
+ gtk::style_context_add_provider_for_display(
+ &display,
+ &provider,
+ gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
+ );
+ }
+}
+
+fn build_ui(app: &adw::Application) {
+ let stack = gtk::Stack::new();
+
+ struct S {
+ stack: gtk::Stack,
+ }
+
+ impl S {
+ fn add<T: TestCase + 'static>(
+ &self,
+ make: impl Fn(Size<i32, Logical>) -> T + 'static,
+ title: &str,
+ ) {
+ let view = SmithayView::new(make);
+ self.stack.add_titled(&view, None, title);
+ }
+ }
+
+ let s = S {
+ stack: stack.clone(),
+ };
+
+ s.add(JustWindow::freeform, "Freeform Window");
+ s.add(JustWindow::fixed_size, "Fixed Size Window");
+ s.add(
+ JustWindow::fixed_size_with_csd_shadow,
+ "Fixed Size Window - CSD Shadow",
+ );
+ s.add(JustTile::freeform, "Freeform Tile");
+ s.add(JustTile::fixed_size, "Fixed Size Tile");
+ s.add(
+ JustTile::fixed_size_with_csd_shadow,
+ "Fixed Size Tile - CSD Shadow",
+ );
+
+ let content_headerbar = adw::HeaderBar::new();
+
+ let anim_adjustment = gtk::Adjustment::new(1., 0., 10., 0.1, 0.5, 0.);
+ anim_adjustment
+ .connect_value_changed(|adj| ANIMATION_SLOWDOWN.store(adj.value(), Ordering::SeqCst));
+ let anim_scale = gtk::Scale::new(gtk::Orientation::Horizontal, Some(&anim_adjustment));
+ anim_scale.set_hexpand(true);
+
+ let anim_control_bar = gtk::Box::new(gtk::Orientation::Horizontal, 6);
+ anim_control_bar.add_css_class("anim-control-bar");
+ anim_control_bar.append(&gtk::Label::new(Some("Slowdown")));
+ anim_control_bar.append(&anim_scale);
+
+ let content_view = adw::ToolbarView::new();
+ content_view.set_top_bar_style(adw::ToolbarStyle::RaisedBorder);
+ content_view.set_bottom_bar_style(adw::ToolbarStyle::RaisedBorder);
+ content_view.add_top_bar(&content_headerbar);
+ content_view.add_bottom_bar(&anim_control_bar);
+ content_view.set_content(Some(&stack));
+ let content = adw::NavigationPage::new(
+ &content_view,
+ stack
+ .page(&stack.visible_child().unwrap())
+ .title()
+ .as_deref()
+ .unwrap(),
+ );
+
+ let sidebar_header = adw::HeaderBar::new();
+ let stack_sidebar = gtk::StackSidebar::new();
+ stack_sidebar.set_stack(&stack);
+ let sidebar_view = adw::ToolbarView::new();
+ sidebar_view.add_top_bar(&sidebar_header);
+ sidebar_view.set_content(Some(&stack_sidebar));
+ let sidebar = adw::NavigationPage::new(&sidebar_view, "Tests");
+
+ let split_view = adw::NavigationSplitView::new();
+ split_view.set_content(Some(&content));
+ split_view.set_sidebar(Some(&sidebar));
+
+ stack.connect_visible_child_notify(move |stack| {
+ content.set_title(
+ stack
+ .visible_child()
+ .and_then(|c| stack.page(&c).title())
+ .as_deref()
+ .unwrap_or_default(),
+ )
+ });
+
+ let window = adw::ApplicationWindow::new(app);
+ window.set_title(Some("niri visual tests"));
+ window.set_content(Some(&split_view));
+ window.present();
+}
diff --git a/niri-visual-tests/src/smithay_view.rs b/niri-visual-tests/src/smithay_view.rs
new file mode 100644
index 00000000..db8eb9ec
--- /dev/null
+++ b/niri-visual-tests/src/smithay_view.rs
@@ -0,0 +1,245 @@
+use gtk::glib;
+use gtk::subclass::prelude::*;
+use smithay::utils::{Logical, Size};
+
+use crate::cases::TestCase;
+
+mod imp {
+ use std::cell::{Cell, OnceCell, RefCell};
+ use std::ptr::null;
+
+ use anyhow::{ensure, Context};
+ use gtk::gdk;
+ use gtk::prelude::*;
+ use niri::utils::get_monotonic_time;
+ use smithay::backend::egl::ffi::egl;
+ use smithay::backend::egl::EGLContext;
+ use smithay::backend::renderer::gles::{Capability, GlesRenderer};
+ use smithay::backend::renderer::{Frame, Renderer, Unbind};
+ use smithay::utils::{Physical, Rectangle, Scale, Transform};
+
+ use super::*;
+
+ type DynMakeTestCase = Box<dyn Fn(Size<i32, Logical>) -> Box<dyn TestCase>>;
+
+ #[derive(Default)]
+ pub struct SmithayView {
+ gl_area: gtk::GLArea,
+ size: Cell<(i32, i32)>,
+ renderer: RefCell<Option<Result<GlesRenderer, ()>>>,
+ pub make_test_case: OnceCell<DynMakeTestCase>,
+ test_case: RefCell<Option<Box<dyn TestCase>>>,
+ }
+
+ #[glib::object_subclass]
+ impl ObjectSubclass for SmithayView {
+ const NAME: &'static str = "NiriSmithayView";
+ type Type = super::SmithayView;
+ type ParentType = gtk::Widget;
+
+ fn class_init(klass: &mut Self::Class) {
+ klass.set_layout_manager_type::<gtk::BinLayout>();
+ }
+ }
+
+ impl ObjectImpl for SmithayView {
+ fn constructed(&self) {
+ let obj = self.obj();
+
+ self.parent_constructed();
+
+ self.gl_area.set_allowed_apis(gdk::GLAPI::GLES);
+ self.gl_area.set_parent(&*obj);
+
+ self.gl_area.connect_resize({
+ let imp = self.downgrade();
+ move |_, width, height| {
+ if let Some(imp) = imp.upgrade() {
+ imp.resize(width, height);
+ }
+ }
+ });
+
+ self.gl_area.connect_render({
+ let imp = self.downgrade();
+ move |_, gl_context| {
+ if let Some(imp) = imp.upgrade() {
+ if let Err(err) = imp.render(gl_context) {
+ warn!("error rendering: {err:?}");
+ }
+ }
+ glib::Propagation::Stop
+ }
+ });
+
+ obj.add_tick_callback(|obj, _frame_clock| {
+ let imp = obj.imp();
+
+ if let Some(case) = &mut *imp.test_case.borrow_mut() {
+ if case.are_animations_ongoing() {
+ imp.gl_area.queue_draw();
+ }
+ }
+
+ glib::ControlFlow::Continue
+ });
+ }
+
+ fn dispose(&self) {
+ self.gl_area.unparent();
+ }
+ }
+
+ impl WidgetImpl for SmithayView {
+ fn unmap(&self) {
+ self.test_case.replace(None);
+ self.parent_unmap();
+ }
+
+ fn unrealize(&self) {
+ self.renderer.replace(None);
+ self.parent_unrealize();
+ }
+ }
+
+ impl SmithayView {
+ fn resize(&self, width: i32, height: i32) {
+ self.size.set((width, height));
+
+ if let Some(case) = &mut *self.test_case.borrow_mut() {
+ case.resize(width, height);
+ }
+ }
+
+ fn render(&self, _gl_context: &gdk::GLContext) -> anyhow::Result<()> {
+ // Set up the Smithay renderer.
+ let mut renderer = self.renderer.borrow_mut();
+ let renderer = renderer.get_or_insert_with(|| {
+ unsafe { create_renderer() }
+ .map_err(|err| warn!("error creating a Smithay renderer: {err:?}"))
+ });
+ let Ok(renderer) = renderer else {
+ return Ok(());
+ };
+
+ let size = self.size.get();
+
+ // Create the test case if missing.
+ let mut case = self.test_case.borrow_mut();
+ let case = case.get_or_insert_with(|| {
+ let make = self.make_test_case.get().unwrap();
+ make(Size::from(size))
+ });
+
+ case.advance_animations(get_monotonic_time());
+
+ let rect: Rectangle<i32, Physical> = Rectangle::from_loc_and_size((0, 0), size);
+
+ let elements = unsafe {
+ with_framebuffer_save_restore(renderer, |renderer| {
+ case.render(renderer, Size::from(size))
+ })
+ }?;
+
+ let mut frame = renderer
+ .render(rect.size, Transform::Normal)
+ .context("error creating frame")?;
+
+ frame
+ .clear([0.3, 0.3, 0.3, 1.], &[rect])
+ .context("error clearing")?;
+
+ for element in elements.iter().rev() {
+ let src = element.src();
+ let dst = element.geometry(Scale::from(1.));
+
+ if let Some(mut damage) = rect.intersection(dst) {
+ damage.loc -= dst.loc;
+ element
+ .draw(&mut frame, src, dst, &[damage])
+ .context("error drawing element")?;
+ }
+ }
+
+ Ok(())
+ }
+ }
+
+ unsafe fn create_renderer() -> anyhow::Result<GlesRenderer> {
+ smithay::backend::egl::ffi::make_sure_egl_is_loaded()
+ .context("error loading EGL symbols in Smithay")?;
+
+ let egl_display = egl::GetCurrentDisplay();
+ ensure!(egl_display != egl::NO_DISPLAY, "no current EGL display");
+
+ let egl_context = egl::GetCurrentContext();
+ ensure!(egl_context != egl::NO_CONTEXT, "no current EGL context");
+
+ // There's no config ID on the EGL context and there's no current EGL surface, but we don't
+ // really use it anyway so just get some random one.
+ let mut egl_config_id = null();
+ let mut num_configs = 0;
+ let res = egl::GetConfigs(egl_display, &mut egl_config_id, 1, &mut num_configs);
+ ensure!(res == egl::TRUE, "error choosing EGL config");
+ ensure!(num_configs != 0, "no EGL config");
+
+ let egl_context = EGLContext::from_raw(egl_display, egl_config_id as *const _, egl_context)
+ .context("error creating EGL context")?;
+ let capabilities = GlesRenderer::supported_capabilities(&egl_context)
+ .context("error getting supported renderer capabilities")?
+ .into_iter()
+ .filter(|c| *c != Capability::ColorTransformations);
+
+ GlesRenderer::with_capabilities(egl_context, capabilities)
+ .context("error creating GlesRenderer")
+ }
+
+ unsafe fn with_framebuffer_save_restore<T>(
+ renderer: &mut GlesRenderer,
+ f: impl FnOnce(&mut GlesRenderer) -> T,
+ ) -> anyhow::Result<T> {
+ let mut framebuffer = 0;
+ renderer
+ .with_context(|gl| unsafe {
+ gl.GetIntegerv(
+ smithay::backend::renderer::gles::ffi::FRAMEBUFFER_BINDING,
+ &mut framebuffer,
+ );
+ })
+ .context("error running closure in GL context")?;
+ ensure!(framebuffer != 0, "error getting the framebuffer");
+
+ let rv = f(renderer);
+
+ renderer.unbind().context("error unbinding")?;
+ renderer
+ .with_context(|gl| unsafe {
+ gl.BindFramebuffer(
+ smithay::backend::renderer::gles::ffi::FRAMEBUFFER,
+ framebuffer as u32,
+ );
+ })
+ .context("error running closure in GL context")?;
+
+ Ok(rv)
+ }
+}
+
+glib::wrapper! {
+ pub struct SmithayView(ObjectSubclass<imp::SmithayView>)
+ @extends gtk::Widget;
+}
+
+impl SmithayView {
+ pub fn new<T: TestCase + 'static>(
+ make_test_case: impl Fn(Size<i32, Logical>) -> T + 'static,
+ ) -> Self {
+ let obj: Self = glib::Object::builder().build();