mirror of
https://github.com/CloudNebulaProject/wayray.git
synced 2026-04-10 13:10:41 +00:00
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:
parent
a7ad184774
commit
f2aebe04a6
18 changed files with 2054 additions and 18 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
|
@ -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]]
|
||||
|
|
|
|||
|
|
@ -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"] }
|
||||
|
|
|
|||
17
crates/wayray-wm-protocol/Cargo.toml
Normal file
17
crates/wayray-wm-protocol/Cargo.toml
Normal 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"]
|
||||
414
crates/wayray-wm-protocol/protocols/wayray-wm-v1.xml
Normal file
414
crates/wayray-wm-protocol/protocols/wayray-wm-v1.xml
Normal 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>
|
||||
48
crates/wayray-wm-protocol/src/lib.rs
Normal file
48
crates/wayray-wm-protocol/src/lib.rs
Normal 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");
|
||||
}
|
||||
12
crates/wr-wm-tiling/Cargo.toml
Normal file
12
crates/wr-wm-tiling/Cargo.toml
Normal 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
|
||||
328
crates/wr-wm-tiling/src/main.rs
Normal file
328
crates/wr-wm-tiling/src/main.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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, _>(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod compositor;
|
||||
mod input;
|
||||
mod output;
|
||||
mod wm_protocol;
|
||||
mod xdg_shell;
|
||||
|
||||
pub use compositor::ClientState;
|
||||
|
|
|
|||
140
crates/wrsrvd/src/handlers/wm_protocol.rs
Normal file
140
crates/wrsrvd/src/handlers/wm_protocol.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
231
crates/wrsrvd/src/wm/floating.rs
Normal file
231
crates/wrsrvd/src/wm/floating.rs
Normal 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));
|
||||
}
|
||||
}
|
||||
79
crates/wrsrvd/src/wm/mod.rs
Normal file
79
crates/wrsrvd/src/wm/mod.rs
Normal 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
|
||||
}
|
||||
}
|
||||
541
crates/wrsrvd/src/wm/protocol.rs
Normal file
541
crates/wrsrvd/src/wm/protocol.rs
Normal 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 => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
72
crates/wrsrvd/src/wm/types.rs
Normal file
72
crates/wrsrvd/src/wm/types.rs
Normal 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,
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue