diff --git a/Cargo.toml b/Cargo.toml index e5e8d428fa6c..ffbc701b41dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,8 @@ members = [ "smithay-drm-extras", "smallvil", "anvil", - "wlcs_anvil" + "wlcs_anvil", + "test_clients", ] [dependencies] diff --git a/src/wayland/shell/xdg/mod.rs b/src/wayland/shell/xdg/mod.rs index ad3a9da9ed2c..948cab7bff1e 100644 --- a/src/wayland/shell/xdg/mod.rs +++ b/src/wayland/shell/xdg/mod.rs @@ -1542,6 +1542,27 @@ impl ToplevelSurface { }) } + /// A newly-unmapped toplevel surface has to perform the initial commit-configure sequence as if it was + /// a new toplevel. + /// + /// This method is used to mark a surface for reinitialization. + /// + /// NOTE: If you are using smithay's rendering abstractions you don't have to call this manually + /// + /// Calls [`compositor::with_states`] internally. + pub fn reset_initial_configure_sent(&self) { + compositor::with_states(&self.wl_surface, |states| { + let mut data = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + data.initial_configure_sent = false; + data.initial_decoration_configure_sent = false; + }); + } + /// Handles the role specific commit logic /// /// This should be called when the underlying WlSurface @@ -1551,6 +1572,10 @@ impl ToplevelSurface { _dh: &DisplayHandle, surface: &wl_surface::WlSurface, ) { + let is_mapped = crate::backend::renderer::utils::with_renderer_surface_state(surface, |state| { + state.buffer().is_some() + }); + compositor::with_states(surface, |states| { let mut guard = states .data_map @@ -1558,6 +1583,16 @@ impl ToplevelSurface { .unwrap() .lock() .unwrap(); + + // This can be None if rendering utils are not used by the user + if let Some(is_mapped) = is_mapped { + // After xdg surface unmaps it has to perform the initial commit-configure sequence again + if !is_mapped { + guard.initial_configure_sent = false; + guard.initial_decoration_configure_sent = false; + } + } + if let Some(state) = guard.last_acked.clone() { guard.current = state; } @@ -1874,6 +1909,26 @@ impl PopupSurface { }) } + /// A newly-unmapped popup surface has to perform the initial commit-configure sequence as if it was + /// a new popup. + /// + /// This method is used to mark a surface for reinitialization. + /// + /// NOTE: If you are using smithay's rendering abstractions you don't have to call this manually + /// + /// Calls [`compositor::with_states`] internally. + pub fn reset_initial_configure_sent(&self) { + compositor::with_states(&self.wl_surface, |states| { + let mut data = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); + data.initial_configure_sent = false; + }); + } + /// Send a configure event, including the `repositioned` event to the client /// in response to a `reposition` request. /// @@ -1922,6 +1977,10 @@ impl PopupSurface { _dh: &DisplayHandle, surface: &wl_surface::WlSurface, ) { + let is_mapped = crate::backend::renderer::utils::with_renderer_surface_state(surface, |state| { + state.buffer().is_some() + }); + compositor::with_states(surface, |states| { let mut attributes = states .data_map @@ -1930,6 +1989,15 @@ impl PopupSurface { .lock() .unwrap(); attributes.committed = true; + + // This can be None if rendering utils are not used by the user + if let Some(is_mapped) = is_mapped { + // After xdg surface unmaps it has to perform the initial commit-configure sequence again + if !is_mapped { + attributes.initial_configure_sent = false; + } + } + if attributes.initial_configure_sent { if let Some(state) = attributes.last_acked { if state != attributes.current { diff --git a/test_clients/Cargo.toml b/test_clients/Cargo.toml new file mode 100644 index 000000000000..7bfd4a4d8b7f --- /dev/null +++ b/test_clients/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "test_clients" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +smithay-client-toolkit = "0.19.2" +tracing = { version = "0.1.37" } +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/test_clients/src/bin/test_xdg_map_unmap.rs b/test_clients/src/bin/test_xdg_map_unmap.rs new file mode 100644 index 000000000000..8d2ace7723b5 --- /dev/null +++ b/test_clients/src/bin/test_xdg_map_unmap.rs @@ -0,0 +1,265 @@ +//! Cycles through initial commit-configure sequence, over and over again (every 1s) + +use std::{convert::TryInto, time::Duration}; + +use smithay_client_toolkit::reexports::{calloop, client as wayland_client}; + +use smithay_client_toolkit::{ + compositor::{CompositorHandler, CompositorState}, + delegate_compositor, delegate_output, delegate_registry, delegate_shm, delegate_xdg_shell, + delegate_xdg_window, + output::{OutputHandler, OutputState}, + registry::{ProvidesRegistryState, RegistryState}, + registry_handlers, + shell::{ + xdg::{ + window::{Window, WindowConfigure, WindowDecorations, WindowHandler}, + XdgShell, + }, + WaylandSurface, + }, + shm::{ + slot::{Buffer, SlotPool}, + Shm, ShmHandler, + }, +}; +use tracing::info; +use wayland_client::{ + protocol::{ + wl_output::{self, WlOutput}, + wl_shm, + wl_surface::{self, WlSurface}, + }, + Connection, QueueHandle, +}; + +fn main() { + test_clients::init_logging(); + + let (mut event_loop, globals, qh) = test_clients::init_connection::(); + + let compositor = CompositorState::bind(&globals, &qh).unwrap(); + let xdg_shell = XdgShell::bind(&globals, &qh).unwrap(); + + let surface = compositor.create_surface(&qh); + let window = xdg_shell.create_window(surface, WindowDecorations::RequestServer, &qh); + + let shm = Shm::bind(&globals, &qh).unwrap(); + let pool = SlotPool::new(256 * 256 * 4, &shm).unwrap(); + + let mut simple_window = App { + registry_state: RegistryState::new(&globals), + output_state: OutputState::new(&globals, &qh), + shm, + + should_be_mapped: false, + + first_configure: true, + pool, + width: 256, + height: 256, + shift: 0, + buffer: None, + window, + loop_signal: event_loop.get_signal(), + }; + + event_loop + .handle() + .insert_source(calloop::timer::Timer::immediate(), |_, _, app| { + app.map_toggle(); + calloop::timer::TimeoutAction::ToDuration(Duration::from_secs(1)) + }) + .unwrap(); + + event_loop.run(None, &mut simple_window, |_| {}).unwrap(); +} + +struct App { + registry_state: RegistryState, + output_state: OutputState, + shm: Shm, + + should_be_mapped: bool, + + first_configure: bool, + pool: SlotPool, + width: u32, + height: u32, + shift: u32, + buffer: Option, + window: Window, + loop_signal: calloop::LoopSignal, +} + +impl CompositorHandler for App { + fn frame( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _surface: &wl_surface::WlSurface, + _time: u32, + ) { + self.draw(conn, qh); + } + fn surface_enter(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn surface_leave(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: &WlOutput) {} + fn scale_factor_changed(&mut self, _: &Connection, _: &QueueHandle, _: &WlSurface, _: i32) {} + fn transform_changed( + &mut self, + _: &Connection, + _: &QueueHandle, + _: &WlSurface, + _: wl_output::Transform, + ) { + } +} + +impl WindowHandler for App { + fn request_close(&mut self, _: &Connection, _: &QueueHandle, _: &Window) { + self.loop_signal.stop(); + } + + fn configure( + &mut self, + conn: &Connection, + qh: &QueueHandle, + _window: &Window, + configure: WindowConfigure, + _serial: u32, + ) { + if self.first_configure { + info!("Window initial configure"); + } else { + info!("Window configure"); + } + + self.buffer = None; + self.width = configure.new_size.0.map(|v| v.get()).unwrap_or(256); + self.height = configure.new_size.1.map(|v| v.get()).unwrap_or(256); + + if self.first_configure { + self.first_configure = false; + self.draw(conn, qh); + } + } +} + +impl App { + fn map_toggle(&mut self) { + if self.should_be_mapped { + self.unmap() + } else { + self.map() + } + } + + fn map(&mut self) { + self.first_configure = true; + self.should_be_mapped = true; + self.window.commit(); + } + + fn unmap(&mut self) { + self.buffer = None; + self.first_configure = false; + self.should_be_mapped = false; + self.window.attach(None, 0, 0); + self.window.commit(); + } + + fn draw(&mut self, _conn: &Connection, qh: &QueueHandle) { + if !self.should_be_mapped { + return; + } + + let width = self.width; + let height = self.height; + let stride = self.width as i32 * 4; + + let buffer = self.buffer.get_or_insert_with(|| { + self.pool + .create_buffer(width as i32, height as i32, stride, wl_shm::Format::Argb8888) + .unwrap() + .0 + }); + + let canvas = match self.pool.canvas(buffer) { + Some(canvas) => canvas, + None => { + // This should be rare, but if the compositor has not released the previous + // buffer, we need double-buffering. + let (second_buffer, canvas) = self + .pool + .create_buffer( + self.width as i32, + self.height as i32, + stride, + wl_shm::Format::Argb8888, + ) + .unwrap(); + *buffer = second_buffer; + canvas + } + }; + + // Draw to the window: + canvas.chunks_exact_mut(4).enumerate().for_each(|(index, chunk)| { + let x = ((index + self.shift as usize) % width as usize) as u32; + let y = (index / width as usize) as u32; + + let a = 0xFF; + let r = u32::min(((width - x) * 0xFF) / width, ((height - y) * 0xFF) / height); + let g = u32::min((x * 0xFF) / width, ((height - y) * 0xFF) / height); + let b = u32::min(((width - x) * 0xFF) / width, (y * 0xFF) / height); + let color = (a << 24) + (r << 16) + (g << 8) + b; + + let array: &mut [u8; 4] = chunk.try_into().unwrap(); + *array = color.to_le_bytes(); + }); + + self.shift = (self.shift + 1) % width; + + self.window + .wl_surface() + .damage_buffer(0, 0, self.width as i32, self.height as i32); + + self.window + .wl_surface() + .frame(qh, self.window.wl_surface().clone()); + + buffer.attach_to(self.window.wl_surface()).expect("buffer attach"); + self.window.commit(); + } +} + +delegate_compositor!(App); +delegate_output!(App); +delegate_shm!(App); + +delegate_xdg_shell!(App); +delegate_xdg_window!(App); + +delegate_registry!(App); + +impl OutputHandler for App { + fn output_state(&mut self) -> &mut OutputState { + &mut self.output_state + } + fn new_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn update_output(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} + fn output_destroyed(&mut self, _: &Connection, _: &QueueHandle, _: WlOutput) {} +} + +impl ShmHandler for App { + fn shm_state(&mut self) -> &mut Shm { + &mut self.shm + } +} + +impl ProvidesRegistryState for App { + fn registry(&mut self) -> &mut RegistryState { + &mut self.registry_state + } + registry_handlers![OutputState,]; +} diff --git a/test_clients/src/lib.rs b/test_clients/src/lib.rs new file mode 100644 index 000000000000..40e1839fcf9f --- /dev/null +++ b/test_clients/src/lib.rs @@ -0,0 +1,39 @@ +use smithay_client_toolkit::reexports::{ + calloop, + calloop_wayland_source::WaylandSource, + client::{self as wayland_client, globals::GlobalList}, +}; + +use calloop::EventLoop; +use wayland_client::{ + globals::registry_queue_init, globals::GlobalListContents, protocol::wl_registry::WlRegistry, Connection, + Dispatch, QueueHandle, +}; + +pub fn init_logging() { + if let Ok(env_filter) = tracing_subscriber::EnvFilter::try_from_default_env() { + tracing_subscriber::fmt() + .compact() + .with_env_filter(env_filter) + .init(); + } else { + tracing_subscriber::fmt().compact().init(); + } +} + +pub fn init_connection() -> (EventLoop<'static, APP>, GlobalList, QueueHandle) +where + APP: Dispatch + 'static, +{ + let conn = Connection::connect_to_env().unwrap(); + + let (globals, event_queue) = registry_queue_init(&conn).unwrap(); + let qh = event_queue.handle(); + let event_loop: EventLoop = EventLoop::try_new().unwrap(); + let loop_handle = event_loop.handle(); + WaylandSource::new(conn.clone(), event_queue) + .insert(loop_handle) + .unwrap(); + + (event_loop, globals, qh) +}