diff --git a/Cargo.lock b/Cargo.lock index c0901b2..5487f8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3282,6 +3282,16 @@ dependencies = [ "zstd", ] +[[package]] +name = "wayray-wm-protocol" +version = "0.1.0" +dependencies = [ + "wayland-backend", + "wayland-client", + "wayland-scanner", + "wayland-server", +] + [[package]] name = "web-sys" version = "0.3.94" @@ -3805,6 +3815,16 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wr-wm-tiling" +version = "0.1.0" +dependencies = [ + "tracing", + "tracing-subscriber", + "wayland-client", + "wayray-wm-protocol", +] + [[package]] name = "wradm" version = "0.1.0" @@ -3849,6 +3869,7 @@ dependencies = [ "tracing", "tracing-subscriber", "wayray-protocol", + "wayray-wm-protocol", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index aead0b5..cec2950 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,9 +2,11 @@ resolver = "3" members = [ "crates/wayray-protocol", + "crates/wayray-wm-protocol", "crates/wrsrvd", "crates/wrclient", "crates/wradm", + "crates/wr-wm-tiling", ] [workspace.package] @@ -14,6 +16,7 @@ license = "MPL-2.0" [workspace.dependencies] wayray-protocol = { path = "crates/wayray-protocol" } +wayray-wm-protocol = { path = "crates/wayray-wm-protocol" } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } miette = { version = "7", features = ["fancy"] } diff --git a/crates/wayray-wm-protocol/Cargo.toml b/crates/wayray-wm-protocol/Cargo.toml new file mode 100644 index 0000000..ad08473 --- /dev/null +++ b/crates/wayray-wm-protocol/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wayray-wm-protocol" +edition.workspace = true +version.workspace = true +license.workspace = true +description = "WayRay pluggable window management Wayland protocol" + +[dependencies] +wayland-backend = "0.3" +wayland-server = { version = "0.31", optional = true } +wayland-client = { version = "0.31", optional = true } +wayland-scanner = "0.31" + +[features] +default = [] +server = ["dep:wayland-server"] +client = ["dep:wayland-client"] diff --git a/crates/wayray-wm-protocol/protocols/wayray-wm-v1.xml b/crates/wayray-wm-protocol/protocols/wayray-wm-v1.xml new file mode 100644 index 0000000..8c47345 --- /dev/null +++ b/crates/wayray-wm-protocol/protocols/wayray-wm-v1.xml @@ -0,0 +1,414 @@ + + + + Copyright 2026 WayRay Contributors + SPDX-License-Identifier: MPL-2.0 + + + + This protocol allows an external window manager process to control + window layout, focus, and keybindings in the WayRay compositor. + + Only one WM client may be bound at a time. If a second WM binds, + the first receives a "replaced" event and should disconnect. + + Window management follows a two-phase transaction model: + 1. Manage phase: WM makes policy decisions (dimensions, focus, decorations) + 2. Render phase: WM specifies visual placement (position, z-order, visibility) + + + + + + + + + The main entry point for external window managers. A WM client binds + this global to receive window lifecycle events and participate in the + two-phase manage/render transaction model. + + + + + + + Sent when a new toplevel is mapped. The WM receives a + wayray_wm_window_v1 object to interact with this window. + Property events (title, app_id, size_hints) follow immediately, + terminated by a done event. + + + + + + + Sent when a toplevel is unmapped and destroyed. The wayray_wm_window_v1 + object becomes inert after this event. + + + + + + + Sent when state changes require WM policy decisions. The WM should + evaluate pending changes and respond with policy requests on the + affected window objects, then call manage_done. + + + + + + Sent after clients have acknowledged configures and committed. The WM + should set visual placement (position, z-order, visibility) on window + objects, then call render_done. All changes are applied atomically. + + + + + + Sent when a new output is available for window placement. + + + + + + + + + Sent when an output is no longer available. + + + + + + + Sent when another WM client has connected and taken over. The old WM + should disconnect gracefully after receiving this event. + + + + + + + + Signals that the WM has finished making policy decisions for this + manage phase. The compositor will send configures to affected windows. + + + + + + Signals that the WM has finished specifying visual placement. The + compositor will apply all changes atomically in one frame. + + + + + + Creates a wayray_wm_seat_v1 object for registering keybindings + and initiating interactive move/resize operations. + + + + + + + Creates a wayray_wm_workspace_v1 object for managing virtual + desktops and window-to-workspace assignments. + + + + + + + Destroy this manager object. The compositor returns to its built-in + window management. + + + + + + + + + + + Represents a single toplevel window. The compositor sends property + events and the WM sends policy/placement requests. + + Policy requests (propose_dimensions, set_focus, etc.) should be sent + during the manage phase. Visual placement requests (set_position, + set_z_*, etc.) should be sent during the render phase. + + + + + + + + + + + + + + + + + Null if this window has no parent. + + + + + + + A value of 0 means unconstrained. + + + + + + + + + + Respond with grant_fullscreen or deny_fullscreen during manage phase. + + + + + + + + + + + + + + Actual committed size after client draws. + + + + + + + + Process all preceding property events atomically. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Register keybindings, create binding modes, and initiate + interactive move/resize operations. + + + + + + + Empty mode string means default mode. + + + + + + + + + + + + + + + + + + + + + Empty string returns to default mode only. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Create and manage virtual desktops. Supports both exclusive workspaces + and tag-based systems (windows on multiple tags simultaneously). + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Window is visible when any tag matches the output's active tags. + + + + + + + + + + + + + + + + + + + + + + diff --git a/crates/wayray-wm-protocol/src/lib.rs b/crates/wayray-wm-protocol/src/lib.rs new file mode 100644 index 0000000..40e47d1 --- /dev/null +++ b/crates/wayray-wm-protocol/src/lib.rs @@ -0,0 +1,48 @@ +//! WayRay pluggable window management Wayland protocol. +//! +//! Provides generated Rust bindings for the `wayray_wm_v1` protocol family: +//! - `wayray_wm_manager_v1` — global WM entry point +//! - `wayray_wm_window_v1` — per-window management +//! - `wayray_wm_seat_v1` — keybindings and interactive operations +//! - `wayray_wm_workspace_v1` — virtual desktop / tag management +//! +//! Enable the `server` feature for compositor-side bindings or +//! `client` feature for WM client-side bindings. + +#![allow( + dead_code, + non_camel_case_types, + unused_unsafe, + unused_variables, + non_upper_case_globals, + non_snake_case, + unused_imports, + missing_docs, + clippy::all +)] + +#[cfg(feature = "client")] +pub mod client { + use wayland_client; + use wayland_client::protocol::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("./protocols/wayray-wm-v1.xml"); + } + use self::__interfaces::*; + wayland_scanner::generate_client_code!("./protocols/wayray-wm-v1.xml"); +} + +#[cfg(feature = "server")] +pub mod server { + use wayland_server; + use wayland_server::protocol::*; + + pub mod __interfaces { + use wayland_server::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("./protocols/wayray-wm-v1.xml"); + } + use self::__interfaces::*; + wayland_scanner::generate_server_code!("./protocols/wayray-wm-v1.xml"); +} diff --git a/crates/wr-wm-tiling/Cargo.toml b/crates/wr-wm-tiling/Cargo.toml new file mode 100644 index 0000000..0dad0f8 --- /dev/null +++ b/crates/wr-wm-tiling/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "wr-wm-tiling" +edition.workspace = true +version.workspace = true +license.workspace = true +description = "Reference tiling window manager for WayRay" + +[dependencies] +wayray-wm-protocol = { workspace = true, features = ["client"] } +wayland-client = "0.31" +tracing.workspace = true +tracing-subscriber.workspace = true diff --git a/crates/wr-wm-tiling/src/main.rs b/crates/wr-wm-tiling/src/main.rs new file mode 100644 index 0000000..8cb9003 --- /dev/null +++ b/crates/wr-wm-tiling/src/main.rs @@ -0,0 +1,328 @@ +//! Reference tiling window manager for WayRay. +//! +//! Demonstrates the WayRay WM protocol with a simple BSP (binary space +//! partitioning) tiling layout. Windows are tiled into a grid that splits +//! alternately horizontally and vertically. + +use tracing::{info, warn}; +use wayland_client::{Connection, Dispatch, QueueHandle, protocol::wl_registry}; +use wayray_wm_protocol::client::{ + wayray_wm_manager_v1::{self, WayrayWmManagerV1}, + wayray_wm_seat_v1::WayrayWmSeatV1, + wayray_wm_window_v1::{self, WayrayWmWindowV1}, + wayray_wm_workspace_v1::WayrayWmWorkspaceV1, +}; + +/// Per-window state tracked by the tiling WM. +#[derive(Debug)] +struct WindowState { + obj: WayrayWmWindowV1, + width: i32, + height: i32, +} + +/// Central state for the tiling WM client. +struct TilingWm { + manager: Option, + windows: Vec, + /// Output dimensions (set from output_new event). + output_width: i32, + output_height: i32, + /// Whether we're in the manage phase. + in_manage_phase: bool, + /// Whether we're in the render phase. + in_render_phase: bool, +} + +impl TilingWm { + fn new() -> Self { + Self { + manager: None, + windows: Vec::new(), + output_width: 1280, + output_height: 720, + in_manage_phase: false, + in_render_phase: false, + } + } + + /// Calculate BSP tiling positions for all windows. + /// Returns (x, y, width, height) for each window. + fn calculate_layout(&self) -> Vec<(i32, i32, i32, i32)> { + let n = self.windows.len(); + if n == 0 { + return Vec::new(); + } + + let gap = 4; + let mut layouts = Vec::with_capacity(n); + + if n == 1 { + layouts.push(( + gap, + gap, + self.output_width - 2 * gap, + self.output_height - 2 * gap, + )); + return layouts; + } + + bsp_layout( + gap, + gap, + self.output_width - 2 * gap, + self.output_height - 2 * gap, + n, + true, + &mut layouts, + ); + + layouts + } +} + +/// Recursive BSP layout: split area alternately horizontal/vertical. +fn bsp_layout( + x: i32, + y: i32, + w: i32, + h: i32, + count: usize, + horizontal: bool, + layouts: &mut Vec<(i32, i32, i32, i32)>, +) { + let gap = 4; + if count == 1 { + layouts.push((x, y, w, h)); + return; + } + + let first_half = count / 2; + let second_half = count - first_half; + + if horizontal { + let first_w = w * first_half as i32 / count as i32 - gap / 2; + let second_w = w - first_w - gap; + bsp_layout(x, y, first_w, h, first_half, !horizontal, layouts); + bsp_layout( + x + first_w + gap, + y, + second_w, + h, + second_half, + !horizontal, + layouts, + ); + } else { + let first_h = h * first_half as i32 / count as i32 - gap / 2; + let second_h = h - first_h - gap; + bsp_layout(x, y, w, first_h, first_half, !horizontal, layouts); + bsp_layout( + x, + y + first_h + gap, + w, + second_h, + second_half, + !horizontal, + layouts, + ); + } +} + +// ============================================================================= +// Wayland client dispatch implementations +// ============================================================================= + +impl Dispatch for TilingWm { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _data: &(), + _conn: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + && interface == "wayray_wm_manager_v1" + { + info!("found wayray_wm_manager_v1 global"); + let manager = registry.bind::(name, version, qh, ()); + state.manager = Some(manager); + } + } +} + +impl Dispatch for TilingWm { + fn event( + state: &mut Self, + manager: &WayrayWmManagerV1, + event: wayray_wm_manager_v1::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + wayray_wm_manager_v1::Event::WindowNew { window } => { + info!("new window received from compositor"); + state.windows.push(WindowState { + obj: window, + width: 0, + height: 0, + }); + } + wayray_wm_manager_v1::Event::WindowClosed { window } => { + info!("window closed"); + state.windows.retain(|w| w.obj != window); + } + wayray_wm_manager_v1::Event::ManageStart => { + state.in_manage_phase = true; + + // Calculate dimensions for all windows. + let layouts = state.calculate_layout(); + for (i, (_, _, w, h)) in layouts.iter().enumerate() { + if i < state.windows.len() { + state.windows[i].obj.propose_dimensions(*w, *h); + } + } + + // Focus the last (most recent) window. + if let Some(last) = state.windows.last() { + last.obj.set_focus(); + } + + manager.manage_done(); + state.in_manage_phase = false; + } + wayray_wm_manager_v1::Event::RenderStart => { + state.in_render_phase = true; + + // Apply BSP tiling positions. + let layouts = state.calculate_layout(); + for (i, (x, y, _w, _h)) in layouts.iter().enumerate() { + if i < state.windows.len() { + state.windows[i].obj.set_position(*x, *y); + state.windows[i].obj.set_z_top(); + state.windows[i].obj.show(); + } + } + + manager.render_done(); + state.in_render_phase = false; + } + wayray_wm_manager_v1::Event::OutputNew { + output_name, + width, + height, + } => { + info!(output_name, width, height, "output available"); + state.output_width = width; + state.output_height = height; + } + wayray_wm_manager_v1::Event::OutputRemoved { output_name } => { + info!(output_name, "output removed"); + } + wayray_wm_manager_v1::Event::Replaced => { + warn!("replaced by another WM, shutting down"); + std::process::exit(0); + } + _ => {} + } + } +} + +impl Dispatch for TilingWm { + fn event( + state: &mut Self, + window: &WayrayWmWindowV1, + event: wayray_wm_window_v1::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + match event { + wayray_wm_window_v1::Event::Title { title } => { + info!(title, "window title"); + } + wayray_wm_window_v1::Event::AppId { app_id } => { + info!(app_id, "window app_id"); + } + wayray_wm_window_v1::Event::Dimensions { width, height } => { + if let Some(ws) = state.windows.iter_mut().find(|w| w.obj == *window) { + ws.width = width; + ws.height = height; + } + } + wayray_wm_window_v1::Event::Done => { + // End of property batch — nothing to do for tiling. + } + _ => {} + } + } +} + +impl Dispatch for TilingWm { + fn event( + _state: &mut Self, + _proxy: &WayrayWmSeatV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +impl Dispatch for TilingWm { + fn event( + _state: &mut Self, + _proxy: &WayrayWmWorkspaceV1, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qh: &QueueHandle, + ) { + } +} + +fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + info!("wr-wm-tiling starting"); + + let conn = Connection::connect_to_env().expect("failed to connect to Wayland display"); + let display = conn.display(); + + let mut event_queue = conn.new_event_queue(); + let qh = event_queue.handle(); + + let _registry = display.get_registry(&qh, ()); + + let mut state = TilingWm::new(); + + // Initial roundtrip to discover globals. + event_queue + .roundtrip(&mut state) + .expect("initial roundtrip failed"); + + if state.manager.is_none() { + eprintln!("wayray_wm_manager_v1 global not found — is this a WayRay compositor?"); + std::process::exit(1); + } + + info!("connected to WayRay compositor, entering event loop"); + + loop { + event_queue + .blocking_dispatch(&mut state) + .expect("event dispatch failed"); + } +} diff --git a/crates/wrsrvd/Cargo.toml b/crates/wrsrvd/Cargo.toml index fa7594a..e863e2b 100644 --- a/crates/wrsrvd/Cargo.toml +++ b/crates/wrsrvd/Cargo.toml @@ -11,6 +11,7 @@ winit = ["smithay/renderer_gl", "smithay/backend_winit"] [dependencies] wayray-protocol.workspace = true +wayray-wm-protocol = { workspace = true, features = ["server"] } tracing.workspace = true tracing-subscriber.workspace = true miette.workspace = true diff --git a/crates/wrsrvd/src/backend/headless.rs b/crates/wrsrvd/src/backend/headless.rs index ba2cee5..1873f6e 100644 --- a/crates/wrsrvd/src/backend/headless.rs +++ b/crates/wrsrvd/src/backend/headless.rs @@ -237,6 +237,9 @@ fn render_headless_frame(data: &mut CalloopData) { // Must be called each frame before rendering. data.state.space.refresh(); + // Apply WM render phase — positions/z-order before frame capture. + data.state.apply_wm_render_commands(); + let custom_elements: &[TextureRenderElement] = &[]; let render_result = render_output::<_, _, Window, _>( diff --git a/crates/wrsrvd/src/handlers/mod.rs b/crates/wrsrvd/src/handlers/mod.rs index 9455711..29f13fa 100644 --- a/crates/wrsrvd/src/handlers/mod.rs +++ b/crates/wrsrvd/src/handlers/mod.rs @@ -1,6 +1,7 @@ mod compositor; mod input; mod output; +mod wm_protocol; mod xdg_shell; pub use compositor::ClientState; diff --git a/crates/wrsrvd/src/handlers/wm_protocol.rs b/crates/wrsrvd/src/handlers/wm_protocol.rs new file mode 100644 index 0000000..9e20a95 --- /dev/null +++ b/crates/wrsrvd/src/handlers/wm_protocol.rs @@ -0,0 +1,140 @@ +//! Dispatch delegation for the WayRay WM protocol. +//! +//! Connects the generated protocol types to our WmProtocolState implementation. + +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, +}; +use wayray_wm_protocol::server::{ + wayray_wm_manager_v1::WayrayWmManagerV1, wayray_wm_seat_v1::WayrayWmSeatV1, + wayray_wm_window_v1::WayrayWmWindowV1, wayray_wm_workspace_v1::WayrayWmWorkspaceV1, +}; + +use crate::state::WayRay; +use crate::wm::protocol::{WmGlobalData, WmProtocolHandler, WmProtocolState, WmWindowData}; + +impl WmProtocolHandler for WayRay { + fn wm_protocol_state(&mut self) -> &mut WmProtocolState { + self.wm_state + .protocol + .as_mut() + .expect("WM protocol state not initialized") + } + + fn existing_windows(&self) -> Vec { + self.window_ids + .iter() + .filter_map(|(id, window)| { + let toplevel = window.toplevel()?; + let size = toplevel.current_state().size?; + Some((*id, None, None, size.w, size.h)) + }) + .collect() + } +} + +// Delegate GlobalDispatch for the manager global. +impl GlobalDispatch for WayRay { + fn bind( + state: &mut Self, + dh: &DisplayHandle, + client: &Client, + resource: New, + global_data: &WmGlobalData, + data_init: &mut DataInit<'_, Self>, + ) { + >::bind( + state, + dh, + client, + resource, + global_data, + data_init, + ); + } +} + +// Delegate Dispatch for manager requests. +impl Dispatch for WayRay { + fn request( + state: &mut Self, + client: &Client, + resource: &WayrayWmManagerV1, + request: ::Request, + data: &(), + dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + >::request( + state, client, resource, request, data, dh, data_init, + ); + } + + fn destroyed(state: &mut Self, client: ClientId, resource: &WayrayWmManagerV1, data: &()) { + >::destroyed( + state, client, resource, data, + ); + } +} + +// Delegate Dispatch for window requests. +impl Dispatch for WayRay { + fn request( + state: &mut Self, + client: &Client, + resource: &WayrayWmWindowV1, + request: ::Request, + data: &WmWindowData, + dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + >::request( + state, client, resource, request, data, dh, data_init, + ); + } + + fn destroyed( + state: &mut Self, + client: ClientId, + resource: &WayrayWmWindowV1, + data: &WmWindowData, + ) { + >::destroyed( + state, client, resource, data, + ); + } +} + +// Delegate Dispatch for seat requests. +impl Dispatch for WayRay { + fn request( + state: &mut Self, + client: &Client, + resource: &WayrayWmSeatV1, + request: ::Request, + data: &(), + dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + >::request( + state, client, resource, request, data, dh, data_init, + ); + } +} + +// Delegate Dispatch for workspace requests. +impl Dispatch for WayRay { + fn request( + state: &mut Self, + client: &Client, + resource: &WayrayWmWorkspaceV1, + request: ::Request, + data: &(), + dh: &DisplayHandle, + data_init: &mut DataInit<'_, Self>, + ) { + >::request( + state, client, resource, request, data, dh, data_init, + ); + } +} diff --git a/crates/wrsrvd/src/handlers/xdg_shell.rs b/crates/wrsrvd/src/handlers/xdg_shell.rs index 2abe226..3bebf12 100644 --- a/crates/wrsrvd/src/handlers/xdg_shell.rs +++ b/crates/wrsrvd/src/handlers/xdg_shell.rs @@ -14,6 +14,7 @@ use smithay::{ use tracing::info; use crate::state::WayRay; +use crate::wm::types::WindowInfo; impl XdgShellHandler for WayRay { fn xdg_shell_state(&mut self) -> &mut XdgShellState { @@ -21,16 +22,46 @@ impl XdgShellHandler for WayRay { } fn new_toplevel(&mut self, surface: ToplevelSurface) { - // Set a suggested size and send the initial configure so the - // client can start drawing. + let output_size = self.output.current_mode().unwrap().size; + + // Gather window info for the WM. + let info = WindowInfo { + title: None, + app_id: None, + output_width: output_size.w, + output_height: output_size.h, + min_size: None, + max_size: None, + }; + + let window = Window::new_wayland_window(surface.clone()); + let id = self.register_window(window.clone()); + + // Ask the WM where to place and how to size this window. + let response = self.wm_state.active_wm().on_new_toplevel(id, info); + surface.with_pending_state(|state| { - state.size = Some((800, 600).into()); + state.size = Some(response.size.into()); }); surface.send_configure(); - let window = Window::new_wayland_window(surface); - self.space.map_element(window, (0, 0), true); - info!("new toplevel mapped with suggested size 800x600"); + self.space + .map_element(window.clone(), response.position, true); + + // Set keyboard focus if the WM requested it. + if response.focus { + let serial = smithay::utils::SERIAL_COUNTER.next_serial(); + let keyboard = self.seat.get_keyboard().unwrap(); + let wl_surface = window.toplevel().map(|t| t.wl_surface().clone()); + keyboard.set_focus(self, wl_surface, serial); + } + + info!( + ?id, + size = ?response.size, + pos = ?response.position, + "new toplevel mapped via WM" + ); } fn new_popup(&mut self, surface: PopupSurface, _positioner: PositionerState) { diff --git a/crates/wrsrvd/src/main.rs b/crates/wrsrvd/src/main.rs index 660c61f..7750e48 100644 --- a/crates/wrsrvd/src/main.rs +++ b/crates/wrsrvd/src/main.rs @@ -3,6 +3,7 @@ mod errors; mod handlers; pub mod network; mod state; +mod wm; use crate::network::{ServerConfig, start_server}; use crate::state::WayRay; diff --git a/crates/wrsrvd/src/state.rs b/crates/wrsrvd/src/state.rs index b88c3d8..744906b 100644 --- a/crates/wrsrvd/src/state.rs +++ b/crates/wrsrvd/src/state.rs @@ -25,6 +25,8 @@ use smithay::{ use tracing::info; use wayray_protocol::messages::InputMessage; +use crate::wm::{self, WmState}; + /// Central compositor state holding all Smithay subsystem states. /// /// This is the "god struct" pattern required by Smithay — a single type that @@ -40,6 +42,12 @@ pub struct WayRay { pub seat: Seat, pub clock: Clock, pub output: Output, + /// Window management state — delegates to built-in or external WM. + pub wm_state: WmState, + /// Maps Smithay Window to WindowId for WM communication. + pub window_ids: Vec<(wm::types::WindowId, smithay::desktop::Window)>, + /// Counter for allocating WindowIds. + next_window_id: u64, // Kept alive to maintain their Wayland globals — not accessed directly. _output_manager_state: OutputManagerState, _xdg_decoration_state: XdgDecorationState, @@ -65,6 +73,13 @@ impl WayRay { info!("all Smithay subsystem states initialized"); + let output_mode = output.current_mode().unwrap(); + let mut wm_state = WmState::new(output_mode.size.w, output_mode.size.h); + + // Register the WM protocol global for external window managers. + let wm_protocol = wm::protocol::WmProtocolState::new::(&dh); + wm_state.init_protocol(wm_protocol); + Self { compositor_state, xdg_shell_state, @@ -76,11 +91,69 @@ impl WayRay { seat, clock: Clock::new(), output, + wm_state, + window_ids: Vec::new(), + next_window_id: 1, _output_manager_state: OutputManagerState::new_with_xdg_output::(&dh), _xdg_decoration_state: XdgDecorationState::new::(&dh), } } + /// Allocate a new WindowId and associate it with a Smithay Window. + pub fn register_window(&mut self, window: smithay::desktop::Window) -> wm::types::WindowId { + let id = wm::types::WindowId::from_raw(self.next_window_id); + self.next_window_id += 1; + self.window_ids.push((id, window)); + id + } + + /// Find the WindowId for a Smithay Window. + pub fn window_id_for(&self, window: &smithay::desktop::Window) -> Option { + self.window_ids + .iter() + .find(|(_, w)| w == window) + .map(|(id, _)| *id) + } + + /// Find the Smithay Window for a WindowId. + #[allow(dead_code)] + pub fn window_for_id(&self, id: wm::types::WindowId) -> Option<&smithay::desktop::Window> { + self.window_ids + .iter() + .find(|(wid, _)| *wid == id) + .map(|(_, w)| w) + } + + /// Remove a window from the id mapping. + #[allow(dead_code)] + pub fn unregister_window(&mut self, window: &smithay::desktop::Window) { + if let Some(id) = self.window_id_for(window) { + self.wm_state.active_wm().on_close_toplevel(id); + self.window_ids.retain(|(_, w)| w != window); + } + } + + /// Apply WM render commands to the Space before frame rendering. + pub fn apply_wm_render_commands(&mut self) { + let ids: Vec<_> = self.window_ids.iter().map(|(id, _)| *id).collect(); + let commands = self.wm_state.active_wm().on_render(&ids); + + for cmd in commands { + if let Some(window) = self + .window_ids + .iter() + .find(|(id, _)| *id == cmd.id) + .map(|(_, w)| w.clone()) + { + if cmd.visible { + self.space.map_element(window, cmd.position, false); + } else { + self.space.unmap_elem(&window); + } + } + } + } + /// Process an input event from the backend and forward it to the appropriate /// Smithay seat device (keyboard or pointer). /// Only used by the Winit backend for local input processing. @@ -128,15 +201,25 @@ impl WayRay { let serial = SERIAL_COUNTER.next_serial(); let pointer = self.seat.get_pointer().unwrap(); - // On button press, focus the window under the pointer. + // On button press, focus the window under the pointer via WM. if event.state() == ButtonState::Pressed { let pos = pointer.current_location(); - if let Some((window, _loc)) = self.space.element_under(pos) { - let window = window.clone(); - self.space.raise_element(&window, true); - + if let Some(focus_window) = self + .space + .element_under(pos) + .map(|(w, _)| w.clone()) + .and_then(|w| self.window_id_for(&w)) + .and_then(|wid| self.wm_state.active_wm().on_pointer_focus(wid)) + .and_then(|fid| { + self.window_ids + .iter() + .find(|(id, _)| *id == fid) + .map(|(_, w)| w.clone()) + }) + { + self.space.raise_element(&focus_window, true); let keyboard = self.seat.get_keyboard().unwrap(); - let wl_surface = window.toplevel().map(|t| t.wl_surface().clone()); + let wl_surface = focus_window.toplevel().map(|t| t.wl_surface().clone()); keyboard.set_focus(self, wl_surface, serial); } } @@ -252,15 +335,25 @@ impl WayRay { wayray_protocol::messages::ButtonState::Released => ButtonState::Released, }; - // Click-to-focus on button press. + // Click-to-focus on button press — delegate to WM. if state == ButtonState::Pressed { let pos = pointer.current_location(); - if let Some((window, _loc)) = self.space.element_under(pos) { - let window = window.clone(); - self.space.raise_element(&window, true); - + if let Some(focus_window) = self + .space + .element_under(pos) + .map(|(w, _)| w.clone()) + .and_then(|w| self.window_id_for(&w)) + .and_then(|wid| self.wm_state.active_wm().on_pointer_focus(wid)) + .and_then(|fid| { + self.window_ids + .iter() + .find(|(id, _)| *id == fid) + .map(|(_, w)| w.clone()) + }) + { + self.space.raise_element(&focus_window, true); let keyboard = self.seat.get_keyboard().unwrap(); - let wl_surface = window.toplevel().map(|t| t.wl_surface().clone()); + let wl_surface = focus_window.toplevel().map(|t| t.wl_surface().clone()); keyboard.set_focus(self, wl_surface, serial); } } diff --git a/crates/wrsrvd/src/wm/floating.rs b/crates/wrsrvd/src/wm/floating.rs new file mode 100644 index 0000000..ba2cc9f --- /dev/null +++ b/crates/wrsrvd/src/wm/floating.rs @@ -0,0 +1,231 @@ +use std::collections::HashMap; + +use super::WindowManager; +use super::types::{DecorationMode, ManageResponse, RenderCommand, WindowId, WindowInfo, ZOrder}; + +/// State for a single managed window in the floating WM. +#[derive(Debug, Clone)] +struct WindowState { + position: (i32, i32), + size: (i32, i32), + /// Stack index: higher means more on top. + z_index: u32, + visible: bool, +} + +/// Built-in floating window manager providing sane defaults. +/// +/// Active when no external WM is connected. Centers new windows, +/// implements click-to-focus, and provides basic keyboard shortcuts. +pub struct BuiltinFloatingWm { + windows: HashMap, + /// Focus stack: last element is the focused window. + focus_stack: Vec, + /// Counter for z-ordering. + next_z: u32, + /// Output dimensions for centering calculations. + output_width: i32, + output_height: i32, +} + +impl BuiltinFloatingWm { + pub fn new(output_width: i32, output_height: i32) -> Self { + Self { + windows: HashMap::new(), + focus_stack: Vec::new(), + next_z: 1, + output_width, + output_height, + } + } + + /// Move window to the top of the focus stack. + fn raise_to_top(&mut self, id: WindowId) { + self.focus_stack.retain(|&w| w != id); + self.focus_stack.push(id); + + if let Some(state) = self.windows.get_mut(&id) { + state.z_index = self.next_z; + self.next_z += 1; + } + } +} + +impl WindowManager for BuiltinFloatingWm { + fn on_new_toplevel(&mut self, id: WindowId, info: WindowInfo) -> ManageResponse { + let width = 800.min(info.output_width); + let height = 600.min(info.output_height); + + // Respect size hints if present. + let width = match (info.min_size, info.max_size) { + (Some((min_w, _)), _) if width < min_w => min_w, + (_, Some((max_w, _))) if width > max_w => max_w, + _ => width, + }; + let height = match (info.min_size, info.max_size) { + (Some((_, min_h)), _) if height < min_h => min_h, + (_, Some((_, max_h))) if height > max_h => max_h, + _ => height, + }; + + // Center on output. + let x = (self.output_width - width) / 2; + let y = (self.output_height - height) / 2; + + let z_index = self.next_z; + self.next_z += 1; + + self.windows.insert( + id, + WindowState { + position: (x, y), + size: (width, height), + z_index, + visible: true, + }, + ); + self.focus_stack.push(id); + + ManageResponse { + size: (width, height), + position: (x, y), + focus: true, + decoration: DecorationMode::ServerSide, + } + } + + fn on_close_toplevel(&mut self, id: WindowId) { + self.windows.remove(&id); + self.focus_stack.retain(|&w| w != id); + } + + fn on_fullscreen_request(&mut self, id: WindowId) -> bool { + if let Some(state) = self.windows.get_mut(&id) { + state.position = (0, 0); + state.size = (self.output_width, self.output_height); + self.raise_to_top(id); + true + } else { + false + } + } + + fn on_render(&mut self, windows: &[WindowId]) -> Vec { + windows + .iter() + .filter_map(|&id| { + let state = self.windows.get(&id)?; + Some(RenderCommand { + id, + position: state.position, + z_order: ZOrder::Preserve, + visible: state.visible, + }) + }) + .collect() + } + + fn on_key_binding(&mut self, _key: u32, _modifiers: u32) -> bool { + // TODO: Alt+F4 close, Alt+Tab cycle + false + } + + fn on_pointer_focus(&mut self, id: WindowId) -> Option { + if self.windows.contains_key(&id) { + self.raise_to_top(id); + Some(id) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_info() -> WindowInfo { + WindowInfo { + title: None, + app_id: None, + output_width: 1280, + output_height: 720, + min_size: None, + max_size: None, + } + } + + #[test] + fn new_window_is_centered() { + let mut wm = BuiltinFloatingWm::new(1280, 720); + let id = WindowId::from_raw(1); + let resp = wm.on_new_toplevel(id, make_info()); + + assert_eq!(resp.size, (800, 600)); + // Centered: (1280 - 800) / 2 = 240, (720 - 600) / 2 = 60 + assert_eq!(resp.position, (240, 60)); + assert!(resp.focus); + } + + #[test] + fn small_output_clamps_size() { + let mut wm = BuiltinFloatingWm::new(640, 480); + let id = WindowId::from_raw(1); + let info = WindowInfo { + output_width: 640, + output_height: 480, + ..make_info() + }; + let resp = wm.on_new_toplevel(id, info); + + assert_eq!(resp.size, (640, 480)); + assert_eq!(resp.position, (0, 0)); + } + + #[test] + fn focus_stack_ordering() { + let mut wm = BuiltinFloatingWm::new(1280, 720); + let id1 = WindowId::from_raw(1); + let id2 = WindowId::from_raw(2); + let id3 = WindowId::from_raw(3); + + wm.on_new_toplevel(id1, make_info()); + wm.on_new_toplevel(id2, make_info()); + wm.on_new_toplevel(id3, make_info()); + + // id3 is on top (last created) + assert_eq!(wm.focus_stack.last(), Some(&id3)); + + // Click on id1 — should raise it + wm.on_pointer_focus(id1); + assert_eq!(wm.focus_stack.last(), Some(&id1)); + assert!(wm.windows[&id1].z_index > wm.windows[&id3].z_index); + } + + #[test] + fn close_removes_from_focus_stack() { + let mut wm = BuiltinFloatingWm::new(1280, 720); + let id1 = WindowId::from_raw(1); + let id2 = WindowId::from_raw(2); + + wm.on_new_toplevel(id1, make_info()); + wm.on_new_toplevel(id2, make_info()); + + wm.on_close_toplevel(id2); + assert!(!wm.focus_stack.contains(&id2)); + assert!(!wm.windows.contains_key(&id2)); + assert_eq!(wm.focus_stack.last(), Some(&id1)); + } + + #[test] + fn fullscreen_covers_output() { + let mut wm = BuiltinFloatingWm::new(1280, 720); + let id = WindowId::from_raw(1); + wm.on_new_toplevel(id, make_info()); + + let granted = wm.on_fullscreen_request(id); + assert!(granted); + assert_eq!(wm.windows[&id].position, (0, 0)); + assert_eq!(wm.windows[&id].size, (1280, 720)); + } +} diff --git a/crates/wrsrvd/src/wm/mod.rs b/crates/wrsrvd/src/wm/mod.rs new file mode 100644 index 0000000..4d4746b --- /dev/null +++ b/crates/wrsrvd/src/wm/mod.rs @@ -0,0 +1,79 @@ +#[allow(dead_code)] +pub mod floating; +pub mod protocol; +#[allow(dead_code)] +pub mod types; + +use types::{ManageResponse, RenderCommand, WindowId, WindowInfo}; + +use self::floating::BuiltinFloatingWm; +use self::protocol::WmProtocolState; + +/// Trait abstracting window management behavior. +/// +/// Both the built-in floating WM and the external protocol adapter implement +/// this trait, so the compositor core doesn't care which is active. +#[allow(dead_code)] +pub trait WindowManager { + /// Called when a new toplevel surface is mapped. Returns policy decisions + /// (size, position, focus, decorations) for the window. + fn on_new_toplevel(&mut self, id: WindowId, info: WindowInfo) -> ManageResponse; + + /// Called when a toplevel is destroyed. + fn on_close_toplevel(&mut self, id: WindowId); + + /// Called when a client requests fullscreen. Returns true to grant. + fn on_fullscreen_request(&mut self, id: WindowId) -> bool; + + /// Called each frame before rendering. Returns placement commands for all + /// managed windows so the compositor can apply positions/z-order atomically. + fn on_render(&mut self, windows: &[WindowId]) -> Vec; + + /// Called when a key press occurs. Returns true if the WM consumed the + /// binding (preventing delivery to the focused client). + fn on_key_binding(&mut self, key: u32, modifiers: u32) -> bool; + + /// Called when the user clicks on a window. The WM should update focus + /// and z-order. Returns the id of the window that should receive focus, + /// if any. + fn on_pointer_focus(&mut self, id: WindowId) -> Option; +} + +/// Coordinator holding both the built-in WM and the protocol server state. +/// Routes calls to whichever is active. +pub struct WmState { + pub builtin: BuiltinFloatingWm, + pub protocol: Option, +} + +impl WmState { + /// Create a new WmState with the built-in floating WM active. + pub fn new(output_width: i32, output_height: i32) -> Self { + Self { + builtin: BuiltinFloatingWm::new(output_width, output_height), + protocol: None, + } + } + + /// Initialize the protocol server (call after Display is set up). + pub fn init_protocol(&mut self, protocol_state: WmProtocolState) { + self.protocol = Some(protocol_state); + } + + /// Whether an external WM is connected via the protocol. + #[allow(dead_code)] + pub fn external_connected(&self) -> bool { + self.protocol.as_ref().is_some_and(|p| p.is_wm_connected()) + } + + /// Get a mutable reference to the currently active WM. + /// Returns the built-in WM when no external WM is connected. + pub fn active_wm(&mut self) -> &mut dyn WindowManager { + // When an external WM is connected, we still use the built-in WM + // for the trait interface. The external WM's render commands are + // applied separately via the protocol state. + // TODO: In a future iteration, create a ProtocolAdapter that + // implements WindowManager and bridges to the protocol. + &mut self.builtin + } +} diff --git a/crates/wrsrvd/src/wm/protocol.rs b/crates/wrsrvd/src/wm/protocol.rs new file mode 100644 index 0000000..728b6ad --- /dev/null +++ b/crates/wrsrvd/src/wm/protocol.rs @@ -0,0 +1,541 @@ +//! Wayland protocol server for the WayRay WM protocol. +//! +//! Implements `GlobalDispatch` and `Dispatch` for the four custom WM interfaces, +//! allowing external WM clients to connect and control window layout. + +use std::collections::HashMap; + +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, + backend::{ClientId, GlobalId}, +}; +use tracing::{info, warn}; +use wayray_wm_protocol::server::{ + wayray_wm_manager_v1::{self, WayrayWmManagerV1}, + wayray_wm_seat_v1::{self, WayrayWmSeatV1}, + wayray_wm_window_v1::{self, WayrayWmWindowV1}, + wayray_wm_workspace_v1::{self, WayrayWmWorkspaceV1}, +}; + +use super::types::{RenderCommand, WindowId, ZOrder}; + +/// Window info tuple for sending to a newly connected WM. +/// (window_id, title, app_id, width, height) +pub type WindowSnapshot = (WindowId, Option, Option, i32, i32); + +/// Per-window data associated with a `wayray_wm_window_v1` protocol object. +#[derive(Debug, Clone)] +pub struct WmWindowData { + pub window_id: WindowId, +} + +/// Data associated with the WM manager global. +pub struct WmGlobalData; + +/// State for the WM protocol server. +/// +/// Tracks the currently connected WM, pending phase operations, and +/// the mapping between WindowIds and protocol objects. +#[allow(dead_code)] +pub struct WmProtocolState { + global: GlobalId, + /// The currently bound WM manager resource (only one allowed). + wm_client: Option, + /// Mapping from WindowId to the protocol window object sent to the WM. + window_objects: HashMap, + /// Pending render commands collected during the render phase. + pending_render_commands: Vec, + /// Whether a manage phase is currently in progress. + manage_phase_active: bool, + /// Whether a render phase is currently in progress. + render_phase_active: bool, + /// Display handle for creating resources. + dh: DisplayHandle, +} + +impl std::fmt::Debug for WmProtocolState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WmProtocolState") + .field("wm_connected", &self.wm_client.is_some()) + .field("windows", &self.window_objects.len()) + .finish() + } +} + +#[allow(dead_code)] +impl WmProtocolState { + /// Create the WM global and register it with the display. + pub fn new(dh: &DisplayHandle) -> Self + where + D: GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + 'static, + { + let global = dh.create_global::(1, WmGlobalData); + + Self { + global, + wm_client: None, + window_objects: HashMap::new(), + pending_render_commands: Vec::new(), + manage_phase_active: false, + render_phase_active: false, + dh: dh.clone(), + } + } + + /// Whether an external WM is currently connected. + pub fn is_wm_connected(&self) -> bool { + self.wm_client.is_some() + } + + /// Send a `window_new` event to the connected WM for a new toplevel. + pub fn notify_new_window( + &mut self, + window_id: WindowId, + title: Option<&str>, + app_id: Option<&str>, + width: i32, + height: i32, + ) where + D: Dispatch + Dispatch + 'static, + { + let Some(manager) = &self.wm_client else { + return; + }; + + let Ok(client) = self.dh.get_client(manager.id()) else { + return; + }; + + let data = WmWindowData { window_id }; + + let Ok(window_obj) = + client.create_resource::(&self.dh, manager.version(), data) + else { + warn!("failed to create WM window resource"); + return; + }; + + // Send window_new event with the new protocol object. + manager.window_new(&window_obj); + + // Send initial properties. + if let Some(title) = title { + window_obj.title(title.to_string()); + } + if let Some(app_id) = app_id { + window_obj.app_id(app_id.to_string()); + } + window_obj.dimensions(width, height); + window_obj.done(); + + self.window_objects.insert(window_id, window_obj); + } + + /// Send a `window_closed` event to the connected WM. + pub fn notify_window_closed(&mut self, window_id: WindowId) { + let Some(manager) = &self.wm_client else { + return; + }; + + if let Some(window_obj) = self.window_objects.remove(&window_id) { + manager.window_closed(&window_obj); + } + } + + /// Send `manage_start` to begin the manage phase. + pub fn start_manage_phase(&mut self) { + if let Some(manager) = &self.wm_client { + manager.manage_start(); + self.manage_phase_active = true; + } + } + + /// Send `render_start` to begin the render phase. + pub fn start_render_phase(&mut self) { + if let Some(manager) = &self.wm_client { + self.pending_render_commands.clear(); + manager.render_start(); + self.render_phase_active = true; + } + } + + /// Take the collected render commands from the last render phase. + pub fn take_render_commands(&mut self) -> Vec { + std::mem::take(&mut self.pending_render_commands) + } + + /// Send the full window list to a newly connected WM. + fn send_full_window_list( + &mut self, + manager: &WayrayWmManagerV1, + existing_windows: &[WindowSnapshot], + ) where + D: Dispatch + Dispatch + 'static, + { + let Ok(client) = self.dh.get_client(manager.id()) else { + return; + }; + + for (window_id, title, app_id, width, height) in existing_windows { + let data = WmWindowData { + window_id: *window_id, + }; + + let Ok(window_obj) = + client.create_resource::(&self.dh, manager.version(), data) + else { + continue; + }; + + manager.window_new(&window_obj); + + if let Some(title) = title { + window_obj.title(title.clone()); + } + if let Some(app_id) = app_id { + window_obj.app_id(app_id.clone()); + } + window_obj.dimensions(*width, *height); + window_obj.done(); + + self.window_objects.insert(*window_id, window_obj); + } + } + + /// Look up a WindowId from a protocol window object. + fn window_id_for_resource(&self, resource: &WayrayWmWindowV1) -> Option { + self.window_objects + .iter() + .find(|(_, obj)| *obj == resource) + .map(|(id, _)| *id) + } +} + +// ============================================================================= +// Dispatch implementations +// ============================================================================= + +/// Helper trait for the WayRay compositor state to provide WM protocol state. +pub trait WmProtocolHandler: + GlobalDispatch + + Dispatch + + Dispatch + + Dispatch + + Dispatch + + 'static +{ + fn wm_protocol_state(&mut self) -> &mut WmProtocolState; + + /// Return the list of existing windows for sending to a newly connected WM. + /// Each tuple is (window_id, title, app_id, width, height). + fn existing_windows(&self) -> Vec; +} + +// --- Manager --- + +impl GlobalDispatch for WmProtocolState { + fn bind( + state: &mut D, + _dh: &DisplayHandle, + _client: &Client, + resource: New, + _global_data: &WmGlobalData, + data_init: &mut DataInit<'_, D>, + ) { + let instance = data_init.init(resource, ()); + + // Collect existing windows before mutating protocol state. + let existing = state.existing_windows(); + + let proto = state.wm_protocol_state(); + + // Enforce single-WM: replace old WM if present. + if let Some(old_manager) = proto.wm_client.take() { + old_manager.replaced(); + proto.window_objects.clear(); + info!("external WM replaced by new connection"); + } + + proto.wm_client = Some(instance.clone()); + + // Send the full window list so the new WM can reconstruct state. + if !existing.is_empty() { + proto.send_full_window_list::(&instance, &existing); + info!( + window_count = existing.len(), + "sent existing window list to new WM" + ); + } + + info!("external WM connected"); + } +} + +impl Dispatch for WmProtocolState { + fn request( + state: &mut D, + _client: &Client, + _resource: &WayrayWmManagerV1, + request: wayray_wm_manager_v1::Request, + _data: &(), + _dh: &DisplayHandle, + data_init: &mut DataInit<'_, D>, + ) { + let proto = state.wm_protocol_state(); + match request { + wayray_wm_manager_v1::Request::ManageDone => { + proto.manage_phase_active = false; + } + wayray_wm_manager_v1::Request::RenderDone => { + proto.render_phase_active = false; + } + wayray_wm_manager_v1::Request::GetSeat { id } => { + data_init.init(id, ()); + } + wayray_wm_manager_v1::Request::GetWorkspaceManager { id } => { + data_init.init(id, ()); + } + wayray_wm_manager_v1::Request::Destroy => { + // WM disconnecting gracefully. + } + _ => {} + } + } + + fn destroyed(state: &mut D, _client: ClientId, resource: &WayrayWmManagerV1, _data: &()) { + let proto = state.wm_protocol_state(); + if proto.wm_client.as_ref().is_some_and(|wm| wm == resource) { + proto.wm_client = None; + proto.window_objects.clear(); + proto.manage_phase_active = false; + proto.render_phase_active = false; + info!("external WM disconnected"); + } + } +} + +// --- Window --- + +impl Dispatch for WmProtocolState { + fn request( + state: &mut D, + _client: &Client, + _resource: &WayrayWmWindowV1, + request: wayray_wm_window_v1::Request, + data: &WmWindowData, + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + let proto = state.wm_protocol_state(); + let window_id = data.window_id; + + match request { + wayray_wm_window_v1::Request::ProposeDimensions { + width: _, + height: _, + } => { + // TODO: implement dimension proposal tracking + } + wayray_wm_window_v1::Request::SetFocus => { + // TODO: queue focus change + } + wayray_wm_window_v1::Request::UseSsd => { + // TODO: set decoration mode + } + wayray_wm_window_v1::Request::UseCsd => { + // TODO: set decoration mode + } + wayray_wm_window_v1::Request::GrantFullscreen => { + // TODO: grant fullscreen + } + wayray_wm_window_v1::Request::DenyFullscreen => { + // TODO: deny fullscreen + } + wayray_wm_window_v1::Request::Close => { + // TODO: send close to toplevel + } + wayray_wm_window_v1::Request::SetPosition { x, y } => { + proto.pending_render_commands.push(RenderCommand { + id: window_id, + position: (x, y), + z_order: ZOrder::Preserve, + visible: true, + }); + } + wayray_wm_window_v1::Request::SetZTop => { + // Update the last command for this window, or add a new one. + if let Some(cmd) = proto + .pending_render_commands + .iter_mut() + .find(|c| c.id == window_id) + { + cmd.z_order = ZOrder::Top; + } else { + proto.pending_render_commands.push(RenderCommand { + id: window_id, + position: (0, 0), // Will use existing position + z_order: ZOrder::Top, + visible: true, + }); + } + } + wayray_wm_window_v1::Request::SetZBottom => { + if let Some(cmd) = proto + .pending_render_commands + .iter_mut() + .find(|c| c.id == window_id) + { + cmd.z_order = ZOrder::Bottom; + } else { + proto.pending_render_commands.push(RenderCommand { + id: window_id, + position: (0, 0), + z_order: ZOrder::Bottom, + visible: true, + }); + } + } + wayray_wm_window_v1::Request::SetZAbove { .. } => { + // TODO: relative z-ordering + } + wayray_wm_window_v1::Request::SetZBelow { .. } => { + // TODO: relative z-ordering + } + wayray_wm_window_v1::Request::SetBorders { .. } => { + // TODO: border rendering + } + wayray_wm_window_v1::Request::Show => { + if let Some(cmd) = proto + .pending_render_commands + .iter_mut() + .find(|c| c.id == window_id) + { + cmd.visible = true; + } else { + proto.pending_render_commands.push(RenderCommand { + id: window_id, + position: (0, 0), + z_order: ZOrder::Preserve, + visible: true, + }); + } + } + wayray_wm_window_v1::Request::Hide => { + if let Some(cmd) = proto + .pending_render_commands + .iter_mut() + .find(|c| c.id == window_id) + { + cmd.visible = false; + } else { + proto.pending_render_commands.push(RenderCommand { + id: window_id, + position: (0, 0), + z_order: ZOrder::Preserve, + visible: false, + }); + } + } + wayray_wm_window_v1::Request::SetOutput { .. } => { + // TODO: multi-output + } + wayray_wm_window_v1::Request::Destroy => {} + _ => {} + } + } + + fn destroyed( + state: &mut D, + _client: ClientId, + _resource: &WayrayWmWindowV1, + data: &WmWindowData, + ) { + state + .wm_protocol_state() + .window_objects + .remove(&data.window_id); + } +} + +// --- Seat --- + +impl Dispatch for WmProtocolState { + fn request( + _state: &mut D, + _client: &Client, + _resource: &WayrayWmSeatV1, + request: wayray_wm_seat_v1::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wayray_wm_seat_v1::Request::BindKey { + key, + modifiers, + mode, + } => { + // TODO: register keybinding + info!(key, modifiers, mode, "WM registered keybinding"); + } + wayray_wm_seat_v1::Request::UnbindKey { .. } => { + // TODO: unregister keybinding + } + wayray_wm_seat_v1::Request::CreateMode { .. } => { + // TODO: create binding mode + } + wayray_wm_seat_v1::Request::ActivateMode { .. } => { + // TODO: activate binding mode + } + wayray_wm_seat_v1::Request::StartMove { .. } => { + // TODO: interactive move + } + wayray_wm_seat_v1::Request::StartResize { .. } => { + // TODO: interactive resize + } + wayray_wm_seat_v1::Request::Destroy => {} + _ => {} + } + } +} + +// --- Workspace --- + +impl Dispatch for WmProtocolState { + fn request( + _state: &mut D, + _client: &Client, + resource: &WayrayWmWorkspaceV1, + request: wayray_wm_workspace_v1::Request, + _data: &(), + _dh: &DisplayHandle, + _data_init: &mut DataInit<'_, D>, + ) { + match request { + wayray_wm_workspace_v1::Request::CreateWorkspace { name } => { + // TODO: create workspace + resource.workspace_created(name); + } + wayray_wm_workspace_v1::Request::DestroyWorkspace { name } => { + // TODO: destroy workspace + resource.workspace_destroyed(name); + } + wayray_wm_workspace_v1::Request::SetActiveWorkspace { .. } => { + // TODO: set active workspace + } + wayray_wm_workspace_v1::Request::AssignWindow { .. } => { + // TODO: assign window to workspace + } + wayray_wm_workspace_v1::Request::SetWindowTags { .. } => { + // TODO: set window tags + } + wayray_wm_workspace_v1::Request::Destroy => {} + _ => {} + } + } +} diff --git a/crates/wrsrvd/src/wm/types.rs b/crates/wrsrvd/src/wm/types.rs new file mode 100644 index 0000000..c46f017 --- /dev/null +++ b/crates/wrsrvd/src/wm/types.rs @@ -0,0 +1,72 @@ +/// Unique identifier for a window managed by the WM. +/// +/// Wraps a monotonically increasing u64 assigned when a toplevel is first mapped. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct WindowId(u64); + +impl WindowId { + /// Create a new WindowId from a raw u64. + pub fn from_raw(id: u64) -> Self { + Self(id) + } + + /// Get the raw u64 value. + pub fn raw(self) -> u64 { + self.0 + } +} + +/// Information about a newly created toplevel, sent to the WM during the manage phase. +#[derive(Debug, Clone)] +pub struct WindowInfo { + pub title: Option, + pub app_id: Option, + /// Output dimensions the window is being mapped on. + pub output_width: i32, + pub output_height: i32, + /// Size hints from the client (min/max size). + pub min_size: Option<(i32, i32)>, + pub max_size: Option<(i32, i32)>, +} + +/// The WM's policy response for a new or reconfigured window. +#[derive(Debug, Clone)] +pub struct ManageResponse { + /// Suggested dimensions (width, height) for the window. + pub size: (i32, i32), + /// Position (x, y) to place the window on the output. + pub position: (i32, i32), + /// Whether this window should receive keyboard focus. + pub focus: bool, + /// Decoration mode preference. + pub decoration: DecorationMode, +} + +/// Decoration mode the WM wants for a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DecorationMode { + /// Server draws title bar and borders. + ServerSide, + /// Client draws its own decorations. + ClientSide, +} + +/// A command from the WM specifying visual placement for a window during the render phase. +#[derive(Debug, Clone)] +pub struct RenderCommand { + pub id: WindowId, + pub position: (i32, i32), + pub z_order: ZOrder, + pub visible: bool, +} + +/// Z-ordering directive for a window. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ZOrder { + /// Place at the top of the stack. + Top, + /// Place at the bottom of the stack. + Bottom, + /// Keep current z-order (no change). + Preserve, +}