aboutsummaryrefslogtreecommitdiff
path: root/niri-visual-tests/src
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 /niri-visual-tests/src
parentd2a4e6a0cbc839813c6d4ef68b75820d87cfc5b0 (diff)
downloadniri-122afff7d10ac96250f428a3f06dca6288d3d0b8.tar.gz
niri-122afff7d10ac96250f428a3f06dca6288d3d0b8.tar.bz2
niri-122afff7d10ac96250f428a3f06dca6288d3d0b8.zip
Add niri-visual-tests
Diffstat (limited to 'niri-visual-tests/src')
-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
6 files changed, 754 insertions, 0 deletions
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();
+
+ 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
+ }
+}
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<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
+ }
+}