Add niri-visual-tests

This commit is contained in:
Ivan Molodetskikh 2024-02-06 09:01:26 +04:00
parent d2a4e6a0cb
commit 122afff7d1
12 changed files with 1062 additions and 7 deletions

View File

@ -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

236
Cargo.lock generated
View File

@ -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"

View File

@ -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 }

View File

@ -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

View File

@ -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`.

View File

@ -0,0 +1,3 @@
.anim-control-bar {
padding: 12px;
}

View File

@ -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>>>;
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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();
}

View File

@ -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();
let make = move |size| Box::new(make_test_case(size)) as Box<dyn TestCase>;
let make_test_case = Box::new(make) as _;
let _ = obj.imp().make_test_case.set(make_test_case);
obj
}
}

View File

@ -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<i32, Logical>,
requested_size: Option<Size<i32, Logical>>,
min_size: Size<i32, Logical>,
max_size: Size<i32, Logical>,
buffer: SolidColorBuffer,
pending_fullscreen: bool,
csd_shadow_width: i32,
csd_shadow_buffer: SolidColorBuffer,
}
#[derive(Debug, Clone)]
pub struct TestWindow(Rc<RefCell<TestWindowInner>>);
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<i32, Logical>) {
self.0.borrow_mut().min_size = size;
}
pub fn set_max_size(&self, size: Size<i32, Logical>) {
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<i32, Logical> {
self.0.borrow().size
}
fn buf_loc(&self) -> Point<i32, Logical> {
(0, 0).into()
}
fn is_in_input_region(&self, _point: Point<f64, Logical>) -> bool {
false
}
fn render<R: NiriRenderer>(
&self,
_renderer: &mut R,
location: Point<i32, Logical>,
scale: Scale<f64>,
) -> Vec<LayoutElementRenderElement<R>> {
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<i32, Logical>) {
self.0.borrow_mut().requested_size = Some(size);
self.0.borrow_mut().pending_fullscreen = false;
}
fn request_fullscreen(&self, _size: Size<i32, Logical>) {
self.0.borrow_mut().pending_fullscreen = true;
}
fn min_size(&self) -> Size<i32, Logical> {
self.0.borrow().min_size
}
fn max_size(&self) -> Size<i32, Logical> {
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
}
}