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 @@ -994,6 +994,16 @@ dependencies = [ "simd-adler32", ] +[[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" @@ -1170,6 +1180,63 @@ dependencies = [ "libc", ] +[[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" @@ -1338,6 +1405,114 @@ dependencies = [ "system-deps", ] +[[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" @@ -1545,6 +1720,38 @@ version = "1.3.0" 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" @@ -1896,6 +2103,20 @@ dependencies = [ "serde", ] +[[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" @@ -2537,6 +2758,15 @@ version = "1.1.0" 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" @@ -2615,6 +2845,12 @@ version = "0.4.1" 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" 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, + ) -> Vec>>; +} 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, +} + +impl JustTile { + pub fn freeform(size: Size) -> 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) -> 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) -> 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, + ) -> Vec>> { + 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) -> Self { + let window = TestWindow::freeform(0); + window.request_size(size); + window.communicate(); + Self { window } + } + + pub fn fixed_size(size: Size) -> Self { + let window = TestWindow::fixed_size(0); + window.request_size(size); + window.communicate(); + Self { window } + } + + pub fn fixed_size_with_csd_shadow(size: Size) -> 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, + ) -> Vec>> { + 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( + &self, + make: impl Fn(Size) -> 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(>k::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) -> Box>; + + #[derive(Default)] + pub struct SmithayView { + gl_area: gtk::GLArea, + size: Cell<(i32, i32)>, + renderer: RefCell>>, + pub make_test_case: OnceCell, + test_case: RefCell>>, + } + + #[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::(); + } + } + + 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 = 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 { + 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( + renderer: &mut GlesRenderer, + f: impl FnOnce(&mut GlesRenderer) -> T, + ) -> anyhow::Result { + 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) + @extends gtk::Widget; +} + +impl SmithayView { + pub fn new( + make_test_case: impl Fn(Size) -> T + 'static, + ) -> Self { + let obj: Self = glib::Object::builder().build(); + + let make = move |size| Box::new(make_test_case(size)) as Box; + let make_test_case = Box::new(make) as _; + let _ = obj.imp().make_test_case.set(make_test_case); + + obj + } +} diff --git a/niri-visual-tests/src/test_window.rs b/niri-visual-tests/src/test_window.rs new file mode 100644 index 00000000..2523fe60 --- /dev/null +++ b/niri-visual-tests/src/test_window.rs @@ -0,0 +1,206 @@ +use std::cell::RefCell; +use std::cmp::{max, min}; +use std::rc::Rc; + +use niri::layout::{LayoutElement, LayoutElementRenderElement}; +use niri::render_helpers::NiriRenderer; +use smithay::backend::renderer::element::solid::{SolidColorBuffer, SolidColorRenderElement}; +use smithay::backend::renderer::element::Kind; +use smithay::output::Output; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Point, Scale, Size, Transform}; + +#[derive(Debug)] +struct TestWindowInner { + id: usize, + size: Size, + requested_size: Option>, + min_size: Size, + max_size: Size, + buffer: SolidColorBuffer, + pending_fullscreen: bool, + csd_shadow_width: i32, + csd_shadow_buffer: SolidColorBuffer, +} + +#[derive(Debug, Clone)] +pub struct TestWindow(Rc>); + +impl TestWindow { + pub fn freeform(id: usize) -> Self { + let size = Size::from((100, 200)); + let min_size = Size::from((0, 0)); + let max_size = Size::from((0, 0)); + let buffer = SolidColorBuffer::new(size, [0.15, 0.64, 0.41, 1.]); + + Self(Rc::new(RefCell::new(TestWindowInner { + id, + size, + requested_size: None, + min_size, + max_size, + buffer, + pending_fullscreen: false, + csd_shadow_width: 0, + csd_shadow_buffer: SolidColorBuffer::new((0, 0), [0., 0., 0., 0.3]), + }))) + } + + pub fn fixed_size(id: usize) -> Self { + let rv = Self::freeform(id); + rv.set_min_size((200, 400).into()); + rv.set_max_size((200, 400).into()); + rv.set_color([0.88, 0.11, 0.14, 1.]); + rv.communicate(); + rv + } + + pub fn set_min_size(&self, size: Size) { + self.0.borrow_mut().min_size = size; + } + + pub fn set_max_size(&self, size: Size) { + self.0.borrow_mut().max_size = size; + } + + pub fn set_color(&self, color: [f32; 4]) { + self.0.borrow_mut().buffer.set_color(color); + } + + pub fn set_csd_shadow_width(&self, width: i32) { + self.0.borrow_mut().csd_shadow_width = width; + } + + pub fn communicate(&self) -> bool { + let mut rv = false; + let mut inner = self.0.borrow_mut(); + + let mut new_size = inner.size; + + if let Some(size) = inner.requested_size.take() { + assert!(size.w >= 0); + assert!(size.h >= 0); + + if size.w != 0 { + new_size.w = size.w; + } + if size.h != 0 { + new_size.h = size.h; + } + } + + if inner.max_size.w > 0 { + new_size.w = min(new_size.w, inner.max_size.w); + } + if inner.max_size.h > 0 { + new_size.h = min(new_size.h, inner.max_size.h); + } + if inner.min_size.w > 0 { + new_size.w = max(new_size.w, inner.min_size.w); + } + if inner.min_size.h > 0 { + new_size.h = max(new_size.h, inner.min_size.h); + } + + if inner.size != new_size { + inner.size = new_size; + inner.buffer.resize(new_size); + rv = true; + } + + let mut csd_shadow_size = new_size; + csd_shadow_size.w += inner.csd_shadow_width * 2; + csd_shadow_size.h += inner.csd_shadow_width * 2; + inner.csd_shadow_buffer.resize(csd_shadow_size); + + rv + } +} + +impl PartialEq for TestWindow { + fn eq(&self, other: &Self) -> bool { + self.0.borrow().id == other.0.borrow().id + } +} + +impl LayoutElement for TestWindow { + fn size(&self) -> Size { + self.0.borrow().size + } + + fn buf_loc(&self) -> Point { + (0, 0).into() + } + + fn is_in_input_region(&self, _point: Point) -> bool { + false + } + + fn render( + &self, + _renderer: &mut R, + location: Point, + scale: Scale, + ) -> Vec> { + let inner = self.0.borrow(); + + vec![ + SolidColorRenderElement::from_buffer( + &inner.buffer, + location.to_physical_precise_round(scale), + scale, + 1., + Kind::Unspecified, + ) + .into(), + SolidColorRenderElement::from_buffer( + &inner.csd_shadow_buffer, + (location - Point::from((inner.csd_shadow_width, inner.csd_shadow_width))) + .to_physical_precise_round(scale), + scale, + 1., + Kind::Unspecified, + ) + .into(), + ] + } + + fn request_size(&self, size: Size) { + self.0.borrow_mut().requested_size = Some(size); + self.0.borrow_mut().pending_fullscreen = false; + } + + fn request_fullscreen(&self, _size: Size) { + self.0.borrow_mut().pending_fullscreen = true; + } + + fn min_size(&self) -> Size { + self.0.borrow().min_size + } + + fn max_size(&self) -> Size { + self.0.borrow().max_size + } + + fn is_wl_surface(&self, _wl_surface: &WlSurface) -> bool { + false + } + + fn set_preferred_scale_transform(&self, _scale: i32, _transform: Transform) {} + + fn has_ssd(&self) -> bool { + false + } + + fn output_enter(&self, _output: &Output) {} + + fn output_leave(&self, _output: &Output) {} + + fn is_fullscreen(&self) -> bool { + false + } + + fn is_pending_fullscreen(&self) -> bool { + self.0.borrow().pending_fullscreen + } +}