diff --git a/Cargo.lock b/Cargo.lock index 0faf03a..da6c1a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -446,6 +446,16 @@ dependencies = [ "ferese-layout", ] +[[package]] +name = "ferese-effects-probe" +version = "0.1.0" +dependencies = [ + "ferese-protocols", + "tempfile", + "wayland-client", + "wayland-protocols-wlr", +] + [[package]] name = "ferese-ipc" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9dade15..d205210 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "crates/ferese-ipc", "crates/ferese-layout", "crates/ferese-protocols", + "tools/ferese-effects-probe", "tools/ferese-malformed-client", ] resolver = "3" @@ -35,9 +36,11 @@ tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" +tempfile = "3.23.0" toml = "0.9.7" xcursor = "0.3.11" wayland-backend = "0.3.17" wayland-client = "0.31.15" wayland-scanner = "0.31.11" wayland-server = "0.31.14" +wayland-protocols-wlr = { version = "0.3.12", features = ["client"] } diff --git a/tools/ferese-effects-probe/Cargo.toml b/tools/ferese-effects-probe/Cargo.toml new file mode 100644 index 0000000..322b7c9 --- /dev/null +++ b/tools/ferese-effects-probe/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "ferese-effects-probe" +version.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +ferese-protocols = { path = "../../crates/ferese-protocols", default-features = false, features = ["client"] } +tempfile.workspace = true +wayland-client.workspace = true +wayland-protocols-wlr.workspace = true diff --git a/tools/ferese-effects-probe/src/main.rs b/tools/ferese-effects-probe/src/main.rs new file mode 100644 index 0000000..17592b0 --- /dev/null +++ b/tools/ferese-effects-probe/src/main.rs @@ -0,0 +1,231 @@ +use std::error::Error; +use std::io::{Seek, SeekFrom, Write}; +use std::os::fd::AsFd; + +use ferese_protocols::effects::v1::client::{ + ferese_effects_manager_v1::FereseEffectsManagerV1, + ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1}, +}; +use wayland_client::{ + Connection, Dispatch, QueueHandle, delegate_noop, + protocol::{wl_buffer, wl_compositor, wl_registry, wl_shm, wl_shm_pool, wl_surface}, +}; +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::{self, ZwlrLayerShellV1}, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, +}; + +const WIDTH: u32 = 640; +const HEIGHT: u32 = 64; + +fn main() -> Result<(), Box> { + let connection = Connection::connect_to_env()?; + let mut queue = connection.new_event_queue(); + let qh = queue.handle(); + connection.display().get_registry(&qh, ()); + let mut state = ProbeState::default(); + + while state.effects.is_none() || !state.configured { + queue.blocking_dispatch(&mut state)?; + state.try_initialize(&qh)?; + if state.closed { + return Err("layer surface was closed before its first configure".into()); + } + } + + let effects = state.effects.clone().expect("effects initialized"); + let initial_configures = state.configure_count; + for role in [ + ferese_surface_effects_v1::Role::PanelElevated, + ferese_surface_effects_v1::Role::Popover, + ferese_surface_effects_v1::Role::Menu, + ferese_surface_effects_v1::Role::Notification, + ferese_surface_effects_v1::Role::Hud, + ferese_surface_effects_v1::Role::Modal, + ferese_surface_effects_v1::Role::Panel, + ] { + effects.set_role(role); + queue.roundtrip(&mut state)?; + if state.configure_count != initial_configures { + return Err(format!("semantic role {role:?} changed layer geometry").into()); + } + } + effects.clear_role(); + queue.roundtrip(&mut state)?; + if state.configure_count != initial_configures { + return Err("clearing the semantic role changed layer geometry".into()); + } + + println!( + "PASS surface.panel and all semantic roles preserved {}x{} layer geometry", + state.width, state.height + ); + Ok(()) +} + +#[derive(Default)] +struct ProbeState { + compositor: Option, + shm: Option, + layer_shell: Option, + effects_manager: Option, + surface: Option, + layer_surface: Option, + effects: Option, + buffer: Option, + configured: bool, + closed: bool, + configure_count: u32, + width: u32, + height: u32, +} + +impl ProbeState { + fn try_initialize(&mut self, qh: &QueueHandle) -> Result<(), Box> { + if self.surface.is_some() { + return Ok(()); + } + let (Some(compositor), Some(_shm), Some(layer_shell), Some(effects_manager)) = ( + self.compositor.as_ref(), + self.shm.as_ref(), + self.layer_shell.as_ref(), + self.effects_manager.as_ref(), + ) else { + return Ok(()); + }; + + let surface = compositor.create_surface(qh, ()); + let layer_surface = layer_shell.get_layer_surface( + &surface, + None, + zwlr_layer_shell_v1::Layer::Top, + "ferese-effects-probe".to_owned(), + qh, + (), + ); + layer_surface.set_anchor( + zwlr_layer_surface_v1::Anchor::Top + | zwlr_layer_surface_v1::Anchor::Left + | zwlr_layer_surface_v1::Anchor::Right, + ); + layer_surface.set_size(WIDTH, HEIGHT); + layer_surface.set_exclusive_zone(HEIGHT as i32); + + let effects = effects_manager.get_surface_effects(&surface, qh, ()); + effects.set_role(ferese_surface_effects_v1::Role::Panel); + + surface.commit(); + self.surface = Some(surface); + self.layer_surface = Some(layer_surface); + self.effects = Some(effects); + Ok(()) + } +} + +fn create_buffer( + shm: &wl_shm::WlShm, + qh: &QueueHandle, + width: u32, + height: u32, +) -> Result> { + let stride = width * 4; + let size = stride * height; + let mut file = tempfile::tempfile()?; + file.set_len(u64::from(size))?; + let pixel = [0x28_u8, 0x24, 0x20, 0xd8]; + for _ in 0..width * height { + file.write_all(&pixel)?; + } + file.seek(SeekFrom::Start(0))?; + + let pool = shm.create_pool(file.as_fd(), size as i32, qh, ()); + let buffer = pool.create_buffer( + 0, + width as i32, + height as i32, + stride as i32, + wl_shm::Format::Argb8888, + qh, + (), + ); + pool.destroy(); + Ok(buffer) +} + +impl Dispatch for ProbeState { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _connection: &Connection, + qh: &QueueHandle, + ) { + let wl_registry::Event::Global { + name, + interface, + version, + } = event + else { + return; + }; + + match interface.as_str() { + "wl_compositor" => state.compositor = Some(registry.bind(name, version.min(6), qh, ())), + "wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())), + "zwlr_layer_shell_v1" => { + state.layer_shell = Some(registry.bind(name, version.min(4), qh, ())) + } + "ferese_effects_manager_v1" => { + state.effects_manager = Some(registry.bind(name, 1, qh, ())) + } + _ => {} + } + } +} + +impl Dispatch for ProbeState { + fn event( + state: &mut Self, + layer_surface: &ZwlrLayerSurfaceV1, + event: zwlr_layer_surface_v1::Event, + _data: &(), + _connection: &Connection, + qh: &QueueHandle, + ) { + match event { + zwlr_layer_surface_v1::Event::Configure { + serial, + width, + height, + } => { + layer_surface.ack_configure(serial); + state.configure_count = state.configure_count.saturating_add(1); + state.width = if width == 0 { WIDTH } else { width }; + state.height = if height == 0 { HEIGHT } else { height }; + if !state.configured { + let surface = state.surface.clone().expect("surface initialized"); + let shm = state.shm.as_ref().expect("shm initialized"); + let buffer = create_buffer(shm, qh, state.width, state.height) + .expect("create configured shared-memory buffer"); + surface.attach(Some(&buffer), 0, 0); + surface.damage_buffer(0, 0, state.width as i32, state.height as i32); + surface.commit(); + state.buffer = Some(buffer); + state.configured = true; + } + } + zwlr_layer_surface_v1::Event::Closed => state.closed = true, + _ => {} + } + } +} + +delegate_noop!(ProbeState: ignore wl_compositor::WlCompositor); +delegate_noop!(ProbeState: ignore wl_shm::WlShm); +delegate_noop!(ProbeState: ignore wl_shm_pool::WlShmPool); +delegate_noop!(ProbeState: ignore wl_buffer::WlBuffer); +delegate_noop!(ProbeState: ignore wl_surface::WlSurface); +delegate_noop!(ProbeState: ignore ZwlrLayerShellV1); +delegate_noop!(ProbeState: ignore FereseEffectsManagerV1); +delegate_noop!(ProbeState: ignore FereseSurfaceEffectsV1);