Implement pluggable window management protocol (Phase 2.5)

Add a custom Wayland protocol (wayray_wm_v1) that allows external
window manager processes to control layout, focus, and keybindings
in the WayRay compositor, inspired by River's two-phase transaction
model.

New crates:
- wayray-wm-protocol: Wayland protocol XML + generated server/client
  bindings via wayland-scanner for four interfaces (manager, window,
  seat, workspace)
- wr-wm-tiling: Reference BSP tiling WM demonstrating the protocol

Compositor changes:
- WindowManager trait + WmState coordinator abstracts WM behavior
- Built-in floating WM (centered windows, click-to-focus, z-ordering)
- Protocol server with GlobalDispatch/Dispatch for all interfaces
- Hot-swap (replaced event) and crash resilience (fallback to built-in)
- new_toplevel delegates to WM instead of hardcoding 800x600 at (0,0)
- WM render phase integrated into headless frame pipeline
This commit is contained in:
Till Wegmueller 2026-04-07 22:29:19 +02:00
parent a7ad184774
commit f2aebe04a6
18 changed files with 2054 additions and 18 deletions

21
Cargo.lock generated
View file

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

View file

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

View file

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

View file

@ -0,0 +1,414 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wayray_wm_v1">
<copyright>
Copyright 2026 WayRay Contributors
SPDX-License-Identifier: MPL-2.0
</copyright>
<description summary="WayRay pluggable window management protocol">
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)
</description>
<!-- ================================================================== -->
<!-- Manager: global entry point -->
<!-- ================================================================== -->
<interface name="wayray_wm_manager_v1" version="1">
<description summary="global window management interface">
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.
</description>
<!-- Events: compositor -> WM -->
<event name="window_new">
<description summary="a new toplevel window appeared">
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.
</description>
<arg name="window" type="new_id" interface="wayray_wm_window_v1"/>
</event>
<event name="window_closed">
<description summary="a toplevel window was destroyed">
Sent when a toplevel is unmapped and destroyed. The wayray_wm_window_v1
object becomes inert after this event.
</description>
<arg name="window" type="object" interface="wayray_wm_window_v1"/>
</event>
<event name="manage_start">
<description summary="begin manage phase">
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.
</description>
</event>
<event name="render_start">
<description summary="begin render phase">
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.
</description>
</event>
<event name="output_new">
<description summary="a new output appeared">
Sent when a new output is available for window placement.
</description>
<arg name="output_name" type="string"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</event>
<event name="output_removed">
<description summary="an output was removed">
Sent when an output is no longer available.
</description>
<arg name="output_name" type="string"/>
</event>
<event name="replaced">
<description summary="this WM has been replaced">
Sent when another WM client has connected and taken over. The old WM
should disconnect gracefully after receiving this event.
</description>
</event>
<!-- Requests: WM -> compositor -->
<request name="manage_done">
<description summary="finish manage phase">
Signals that the WM has finished making policy decisions for this
manage phase. The compositor will send configures to affected windows.
</description>
</request>
<request name="render_done">
<description summary="finish render phase">
Signals that the WM has finished specifying visual placement. The
compositor will apply all changes atomically in one frame.
</description>
</request>
<request name="get_seat">
<description summary="get seat interface for keybindings">
Creates a wayray_wm_seat_v1 object for registering keybindings
and initiating interactive move/resize operations.
</description>
<arg name="id" type="new_id" interface="wayray_wm_seat_v1"/>
</request>
<request name="get_workspace_manager">
<description summary="get workspace management interface">
Creates a wayray_wm_workspace_v1 object for managing virtual
desktops and window-to-workspace assignments.
</description>
<arg name="id" type="new_id" interface="wayray_wm_workspace_v1"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the manager">
Destroy this manager object. The compositor returns to its built-in
window management.
</description>
</request>
</interface>
<!-- ================================================================== -->
<!-- Window: per-toplevel interface -->
<!-- ================================================================== -->
<interface name="wayray_wm_window_v1" version="1">
<description summary="per-window management interface">
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.
</description>
<!-- Property events: compositor -> WM -->
<event name="title">
<description summary="window title changed"/>
<arg name="title" type="string"/>
</event>
<event name="app_id">
<description summary="application identifier changed"/>
<arg name="app_id" type="string"/>
</event>
<event name="parent">
<description summary="parent window for dialogs">
Null if this window has no parent.
</description>
<arg name="parent" type="object" interface="wayray_wm_window_v1" allow-null="true"/>
</event>
<event name="size_hints">
<description summary="client size constraints">
A value of 0 means unconstrained.
</description>
<arg name="min_width" type="int"/>
<arg name="min_height" type="int"/>
<arg name="max_width" type="int"/>
<arg name="max_height" type="int"/>
</event>
<event name="fullscreen_request">
<description summary="client requests fullscreen">
Respond with grant_fullscreen or deny_fullscreen during manage phase.
</description>
</event>
<event name="maximize_request">
<description summary="client requests maximize"/>
</event>
<event name="close_request">
<description summary="client or user requested close"/>
</event>
<event name="dimensions">
<description summary="committed surface dimensions">
Actual committed size after client draws.
</description>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</event>
<event name="done">
<description summary="end of property batch">
Process all preceding property events atomically.
</description>
</event>
<!-- Policy requests: WM -> compositor (manage phase) -->
<request name="propose_dimensions">
<description summary="suggest window dimensions"/>
<arg name="width" type="int"/>
<arg name="height" type="int"/>
</request>
<request name="set_focus">
<description summary="give keyboard focus to this window"/>
</request>
<request name="use_ssd">
<description summary="use server-side decorations"/>
</request>
<request name="use_csd">
<description summary="use client-side decorations"/>
</request>
<request name="grant_fullscreen">
<description summary="allow fullscreen"/>
</request>
<request name="deny_fullscreen">
<description summary="deny fullscreen"/>
</request>
<request name="close">
<description summary="ask client to close"/>
</request>
<!-- Visual placement requests: WM -> compositor (render phase) -->
<request name="set_position">
<description summary="set window position relative to output origin"/>
<arg name="x" type="int"/>
<arg name="y" type="int"/>
</request>
<request name="set_z_above">
<description summary="place above another window"/>
<arg name="sibling" type="object" interface="wayray_wm_window_v1"/>
</request>
<request name="set_z_below">
<description summary="place below another window"/>
<arg name="sibling" type="object" interface="wayray_wm_window_v1"/>
</request>
<request name="set_z_top">
<description summary="place at top of z-order stack"/>
</request>
<request name="set_z_bottom">
<description summary="place at bottom of z-order stack"/>
</request>
<request name="set_borders">
<description summary="set border color (0-255) and width"/>
<arg name="red" type="uint"/>
<arg name="green" type="uint"/>
<arg name="blue" type="uint"/>
<arg name="alpha" type="uint"/>
<arg name="width" type="int"/>
</request>
<request name="show">
<description summary="make window visible"/>
</request>
<request name="hide">
<description summary="hide window (remains managed)"/>
</request>
<request name="set_output">
<description summary="assign window to an output"/>
<arg name="output_name" type="string"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy this window object"/>
</request>
</interface>
<!-- ================================================================== -->
<!-- Seat: keybinding and input management -->
<!-- ================================================================== -->
<interface name="wayray_wm_seat_v1" version="1">
<description summary="keybinding and input management for WM">
Register keybindings, create binding modes, and initiate
interactive move/resize operations.
</description>
<!-- Requests: WM -> compositor -->
<request name="bind_key">
<description summary="register a keybinding">
Empty mode string means default mode.
</description>
<arg name="key" type="uint" summary="Linux evdev keycode"/>
<arg name="modifiers" type="uint" summary="modifier bitmask"/>
<arg name="mode" type="string" summary="binding mode name"/>
</request>
<request name="unbind_key">
<description summary="remove a keybinding"/>
<arg name="key" type="uint"/>
<arg name="modifiers" type="uint"/>
<arg name="mode" type="string"/>
</request>
<request name="create_mode">
<description summary="create a named binding mode (like i3)"/>
<arg name="name" type="string"/>
</request>
<request name="activate_mode">
<description summary="switch active binding mode">
Empty string returns to default mode only.
</description>
<arg name="name" type="string"/>
</request>
<request name="start_move">
<description summary="begin interactive pointer-driven window move"/>
<arg name="window" type="object" interface="wayray_wm_window_v1"/>
</request>
<request name="start_resize">
<description summary="begin interactive pointer-driven window resize"/>
<arg name="window" type="object" interface="wayray_wm_window_v1"/>
<arg name="edges" type="uint" summary="resize edge bitmask"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy seat interface and all keybindings"/>
</request>
<!-- Events: compositor -> WM -->
<event name="binding_pressed">
<description summary="a registered keybinding was pressed"/>
<arg name="key" type="uint"/>
<arg name="modifiers" type="uint"/>
</event>
<event name="binding_released">
<description summary="a registered keybinding was released"/>
<arg name="key" type="uint"/>
<arg name="modifiers" type="uint"/>
</event>
</interface>
<!-- ================================================================== -->
<!-- Workspace: virtual desktop / tag management -->
<!-- ================================================================== -->
<interface name="wayray_wm_workspace_v1" version="1">
<description summary="workspace and virtual desktop management">
Create and manage virtual desktops. Supports both exclusive workspaces
and tag-based systems (windows on multiple tags simultaneously).
</description>
<!-- Requests: WM -> compositor -->
<request name="create_workspace">
<description summary="create a named workspace"/>
<arg name="name" type="string"/>
</request>
<request name="destroy_workspace">
<description summary="destroy a workspace (windows become unassigned)"/>
<arg name="name" type="string"/>
</request>
<request name="set_active_workspace">
<description summary="set visible workspace on an output"/>
<arg name="output_name" type="string"/>
<arg name="workspace_name" type="string"/>
</request>
<request name="assign_window">
<description summary="move window to a workspace"/>
<arg name="window" type="object" interface="wayray_wm_window_v1"/>
<arg name="workspace_name" type="string"/>
</request>
<request name="set_window_tags">
<description summary="set tag bitmask (dwm-style)">
Window is visible when any tag matches the output's active tags.
</description>
<arg name="window" type="object" interface="wayray_wm_window_v1"/>
<arg name="tags" type="uint" summary="bitmask of active tags"/>
</request>
<request name="destroy" type="destructor">
<description summary="destroy workspace manager"/>
</request>
<!-- Events: compositor -> WM -->
<event name="workspace_created">
<description summary="workspace was created"/>
<arg name="name" type="string"/>
</event>
<event name="workspace_destroyed">
<description summary="workspace was destroyed"/>
<arg name="name" type="string"/>
</event>
</interface>
</protocol>

View file

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

View file

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

View file

@ -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<WayrayWmManagerV1>,
windows: Vec<WindowState>,
/// 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<wl_registry::WlRegistry, ()> for TilingWm {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
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::<WayrayWmManagerV1, _, _>(name, version, qh, ());
state.manager = Some(manager);
}
}
}
impl Dispatch<WayrayWmManagerV1, ()> for TilingWm {
fn event(
state: &mut Self,
manager: &WayrayWmManagerV1,
event: wayray_wm_manager_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
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<WayrayWmWindowV1, ()> for TilingWm {
fn event(
state: &mut Self,
window: &WayrayWmWindowV1,
event: wayray_wm_window_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
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<WayrayWmSeatV1, ()> for TilingWm {
fn event(
_state: &mut Self,
_proxy: &WayrayWmSeatV1,
_event: <WayrayWmSeatV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<WayrayWmWorkspaceV1, ()> for TilingWm {
fn event(
_state: &mut Self,
_proxy: &WayrayWmWorkspaceV1,
_event: <WayrayWmWorkspaceV1 as wayland_client::Proxy>::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
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");
}
}

View file

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

View file

@ -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<PixmanTexture>] = &[];
let render_result = render_output::<_, _, Window, _>(

View file

@ -1,6 +1,7 @@
mod compositor;
mod input;
mod output;
mod wm_protocol;
mod xdg_shell;
pub use compositor::ClientState;

View file

@ -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<crate::wm::protocol::WindowSnapshot> {
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<WayrayWmManagerV1, WmGlobalData> for WayRay {
fn bind(
state: &mut Self,
dh: &DisplayHandle,
client: &Client,
resource: New<WayrayWmManagerV1>,
global_data: &WmGlobalData,
data_init: &mut DataInit<'_, Self>,
) {
<WmProtocolState as GlobalDispatch<WayrayWmManagerV1, WmGlobalData, Self>>::bind(
state,
dh,
client,
resource,
global_data,
data_init,
);
}
}
// Delegate Dispatch for manager requests.
impl Dispatch<WayrayWmManagerV1, ()> for WayRay {
fn request(
state: &mut Self,
client: &Client,
resource: &WayrayWmManagerV1,
request: <WayrayWmManagerV1 as Resource>::Request,
data: &(),
dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
<WmProtocolState as Dispatch<WayrayWmManagerV1, (), Self>>::request(
state, client, resource, request, data, dh, data_init,
);
}
fn destroyed(state: &mut Self, client: ClientId, resource: &WayrayWmManagerV1, data: &()) {
<WmProtocolState as Dispatch<WayrayWmManagerV1, (), Self>>::destroyed(
state, client, resource, data,
);
}
}
// Delegate Dispatch for window requests.
impl Dispatch<WayrayWmWindowV1, WmWindowData> for WayRay {
fn request(
state: &mut Self,
client: &Client,
resource: &WayrayWmWindowV1,
request: <WayrayWmWindowV1 as Resource>::Request,
data: &WmWindowData,
dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
<WmProtocolState as Dispatch<WayrayWmWindowV1, WmWindowData, Self>>::request(
state, client, resource, request, data, dh, data_init,
);
}
fn destroyed(
state: &mut Self,
client: ClientId,
resource: &WayrayWmWindowV1,
data: &WmWindowData,
) {
<WmProtocolState as Dispatch<WayrayWmWindowV1, WmWindowData, Self>>::destroyed(
state, client, resource, data,
);
}
}
// Delegate Dispatch for seat requests.
impl Dispatch<WayrayWmSeatV1, ()> for WayRay {
fn request(
state: &mut Self,
client: &Client,
resource: &WayrayWmSeatV1,
request: <WayrayWmSeatV1 as Resource>::Request,
data: &(),
dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
<WmProtocolState as Dispatch<WayrayWmSeatV1, (), Self>>::request(
state, client, resource, request, data, dh, data_init,
);
}
}
// Delegate Dispatch for workspace requests.
impl Dispatch<WayrayWmWorkspaceV1, ()> for WayRay {
fn request(
state: &mut Self,
client: &Client,
resource: &WayrayWmWorkspaceV1,
request: <WayrayWmWorkspaceV1 as Resource>::Request,
data: &(),
dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
<WmProtocolState as Dispatch<WayrayWmWorkspaceV1, (), Self>>::request(
state, client, resource, request, data, dh, data_init,
);
}
}

View file

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

View file

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

View file

@ -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<Self>,
pub clock: Clock<Monotonic>,
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::<Self>(&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::<Self>(&dh),
_xdg_decoration_state: XdgDecorationState::new::<Self>(&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<wm::types::WindowId> {
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);
}
}

View file

@ -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<WindowId, WindowState>,
/// Focus stack: last element is the focused window.
focus_stack: Vec<WindowId>,
/// 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<RenderCommand> {
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<WindowId> {
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));
}
}

View file

@ -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<RenderCommand>;
/// 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<WindowId>;
}
/// 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<WmProtocolState>,
}
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
}
}

View file

@ -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<String>, Option<String>, 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<WayrayWmManagerV1>,
/// Mapping from WindowId to the protocol window object sent to the WM.
window_objects: HashMap<WindowId, WayrayWmWindowV1>,
/// Pending render commands collected during the render phase.
pending_render_commands: Vec<RenderCommand>,
/// 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<D>(dh: &DisplayHandle) -> Self
where
D: GlobalDispatch<WayrayWmManagerV1, WmGlobalData>
+ Dispatch<WayrayWmManagerV1, ()>
+ Dispatch<WayrayWmWindowV1, WmWindowData>
+ Dispatch<WayrayWmSeatV1, ()>
+ Dispatch<WayrayWmWorkspaceV1, ()>
+ 'static,
{
let global = dh.create_global::<D, WayrayWmManagerV1, _>(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<D>(
&mut self,
window_id: WindowId,
title: Option<&str>,
app_id: Option<&str>,
width: i32,
height: i32,
) where
D: Dispatch<WayrayWmWindowV1, WmWindowData> + Dispatch<WayrayWmManagerV1, ()> + '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::<WayrayWmWindowV1, _, D>(&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<RenderCommand> {
std::mem::take(&mut self.pending_render_commands)
}
/// Send the full window list to a newly connected WM.
fn send_full_window_list<D>(
&mut self,
manager: &WayrayWmManagerV1,
existing_windows: &[WindowSnapshot],
) where
D: Dispatch<WayrayWmWindowV1, WmWindowData> + Dispatch<WayrayWmManagerV1, ()> + '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::<WayrayWmWindowV1, _, D>(&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<WindowId> {
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<WayrayWmManagerV1, WmGlobalData>
+ Dispatch<WayrayWmManagerV1, ()>
+ Dispatch<WayrayWmWindowV1, WmWindowData>
+ Dispatch<WayrayWmSeatV1, ()>
+ Dispatch<WayrayWmWorkspaceV1, ()>
+ '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<WindowSnapshot>;
}
// --- Manager ---
impl<D: WmProtocolHandler> GlobalDispatch<WayrayWmManagerV1, WmGlobalData, D> for WmProtocolState {
fn bind(
state: &mut D,
_dh: &DisplayHandle,
_client: &Client,
resource: New<WayrayWmManagerV1>,
_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::<D>(&instance, &existing);
info!(
window_count = existing.len(),
"sent existing window list to new WM"
);
}
info!("external WM connected");
}
}
impl<D: WmProtocolHandler> Dispatch<WayrayWmManagerV1, (), D> 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<D: WmProtocolHandler> Dispatch<WayrayWmWindowV1, WmWindowData, D> 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<D: WmProtocolHandler> Dispatch<WayrayWmSeatV1, (), D> 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<D: WmProtocolHandler> Dispatch<WayrayWmWorkspaceV1, (), D> 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 => {}
_ => {}
}
}
}

View file

@ -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<String>,
pub app_id: Option<String>,
/// 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,
}