feat(compositor): implement weft-shell-protocol server side

Add the WEFT compositor-shell Wayland protocol and wire it into the
compositor state.

Protocol definition:
- protocol/weft-shell-unstable-v1.xml: defines zweft_shell_manager_v1
  (global, bound once by servo-shell) and zweft_shell_window_v1
  (per-window slot). Requests: destroy, create_window,
  update_metadata, set_geometry. Events: configure, focus_changed,
  window_closed, presentation_feedback.

Generated code + bindings:
- crates/weft-compositor/src/protocols/mod.rs: uses wayland-scanner
  generate_interfaces! inside a __interfaces sub-module and
  generate_server_code! at the server module level, following the
  wayland-protocols-wlr crate structure. Exports WeftShellState
  (holds the GlobalId) and WeftShellWindowData (per-window user data).

Server-side dispatch (state.rs):
- GlobalDispatch<ZweftShellManagerV1, ()>: binds the global, inits
  each bound resource with unit user data.
- Dispatch<ZweftShellManagerV1, ()>: handles create_window by
  initialising a ZweftShellWindowV1 and sending an initial configure.
- Dispatch<ZweftShellWindowV1, WeftShellWindowData>: handles
  update_metadata (stores advisory data) and set_geometry (echoes
  compositor-adjusted configure back to client).

WeftCompositorState.weft_shell_state initialised in new() alongside
all other protocol globals.

New direct deps in weft-compositor: wayland-scanner, wayland-server,
wayland-backend, bitflags (all version-matched to Smithay 0.7).
This commit is contained in:
Marco Allegretti 2026-03-11 07:59:56 +01:00
parent c7ad2116a0
commit 18f92cc341
6 changed files with 335 additions and 1 deletions

4
Cargo.lock generated
View file

@ -2258,11 +2258,15 @@ name = "weft-compositor"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.11.0",
"sd-notify", "sd-notify",
"smithay", "smithay",
"smithay-drm-extras", "smithay-drm-extras",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"wayland-backend",
"wayland-scanner",
"wayland-server",
] ]
[[package]] [[package]]

View file

@ -21,6 +21,10 @@ smithay = { version = "0.7", default-features = false, features = [
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
anyhow = "1" anyhow = "1"
wayland-scanner = "0.31"
wayland-server = "0.31"
wayland-backend = "0.3"
bitflags = "2"
# DRM/KMS and hardware input depend on Linux kernel interfaces; compile only on Linux. # DRM/KMS and hardware input depend on Linux kernel interfaces; compile only on Linux.
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]

View file

@ -2,6 +2,7 @@ use tracing_subscriber::EnvFilter;
mod backend; mod backend;
mod input; mod input;
mod protocols;
mod state; mod state;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {

View file

@ -0,0 +1,42 @@
#[allow(dead_code, non_camel_case_types, unused_unsafe, unused_variables)]
#[allow(non_upper_case_globals, non_snake_case, unused_imports)]
#[allow(missing_docs, clippy::all)]
pub mod server {
use wayland_server;
use wayland_server::protocol::*;
pub mod __interfaces {
use wayland_server::protocol::__interfaces::*;
wayland_scanner::generate_interfaces!("../../protocol/weft-shell-unstable-v1.xml");
}
use self::__interfaces::*;
wayland_scanner::generate_server_code!("../../protocol/weft-shell-unstable-v1.xml");
}
pub use server::zweft_shell_manager_v1::ZweftShellManagerV1;
pub use server::zweft_shell_window_v1::ZweftShellWindowV1;
use wayland_server::{DisplayHandle, GlobalDispatch, backend::GlobalId};
pub struct WeftShellState {
_global: GlobalId,
}
#[allow(dead_code)]
pub struct WeftShellWindowData {
pub app_id: String,
pub title: String,
pub role: String,
}
impl WeftShellState {
pub fn new<D>(display: &DisplayHandle) -> Self
where
D: GlobalDispatch<ZweftShellManagerV1, ()>,
D: 'static,
{
let global = display.create_global::<D, ZweftShellManagerV1, ()>(1, ());
Self { _global: global }
}
}

View file

@ -1,5 +1,9 @@
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::backend::drm_device::WeftDrmData; use crate::backend::drm_device::WeftDrmData;
use crate::protocols::{
WeftShellState, WeftShellWindowData, ZweftShellManagerV1, ZweftShellWindowV1,
server::{zweft_shell_manager_v1, zweft_shell_window_v1},
};
use smithay::{ use smithay::{
backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler}, backend::{input::TabletToolDescriptor, renderer::utils::on_commit_buffer_handler},
@ -15,7 +19,7 @@ use smithay::{
reexports::{ reexports::{
calloop::{LoopHandle, LoopSignal}, calloop::{LoopHandle, LoopSignal},
wayland_server::{ wayland_server::{
Client, DisplayHandle, Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New,
backend::{ClientData, ClientId, DisconnectReason}, backend::{ClientData, ClientId, DisconnectReason},
protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_surface::WlSurface}, protocol::{wl_buffer::WlBuffer, wl_output::WlOutput, wl_surface::WlSurface},
}, },
@ -89,6 +93,9 @@ pub struct WeftCompositorState {
// Set to false when the compositor should exit the event loop. // Set to false when the compositor should exit the event loop.
pub running: bool, pub running: bool,
// WEFT compositorshell protocol global.
pub weft_shell_state: WeftShellState,
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
pub drm: Option<WeftDrmData>, pub drm: Option<WeftDrmData>,
} }
@ -113,6 +120,7 @@ impl WeftCompositorState {
InputMethodManagerState::new::<Self, _>(&display_handle, |_client| true); InputMethodManagerState::new::<Self, _>(&display_handle, |_client| true);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle); let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle); let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let weft_shell_state = WeftShellState::new::<Self>(&display_handle);
let mut seat_state = SeatState::new(); let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, seat_name); let mut seat = seat_state.new_wl_seat(&display_handle, seat_name);
@ -136,6 +144,7 @@ impl WeftCompositorState {
input_method_state, input_method_state,
pointer_constraints_state, pointer_constraints_state,
cursor_shape_state, cursor_shape_state,
weft_shell_state,
space: Space::default(), space: Space::default(),
popups: PopupManager::default(), popups: PopupManager::default(),
seat_state, seat_state,
@ -404,3 +413,81 @@ impl TabletSeatHandler for WeftCompositorState {
// CursorShapeManagerState has no handler trait; it calls SeatHandler::cursor_image directly. // CursorShapeManagerState has no handler trait; it calls SeatHandler::cursor_image directly.
delegate_cursor_shape!(WeftCompositorState); delegate_cursor_shape!(WeftCompositorState);
// --- weft-shell-protocol ---
impl GlobalDispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &Client,
resource: New<ZweftShellManagerV1>,
_global_data: &(),
data_init: &mut DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl Dispatch<ZweftShellManagerV1, ()> for WeftCompositorState {
fn request(
_state: &mut Self,
_client: &Client,
_resource: &ZweftShellManagerV1,
request: zweft_shell_manager_v1::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut DataInit<'_, Self>,
) {
match request {
zweft_shell_manager_v1::Request::Destroy => {}
zweft_shell_manager_v1::Request::CreateWindow {
id,
app_id,
title,
role,
x,
y,
width,
height,
} => {
let window = data_init.init(
id,
WeftShellWindowData {
app_id,
title,
role,
},
);
window.configure(x, y, width, height, 0);
}
}
}
}
impl Dispatch<ZweftShellWindowV1, WeftShellWindowData> for WeftCompositorState {
fn request(
_state: &mut Self,
_client: &Client,
resource: &ZweftShellWindowV1,
request: zweft_shell_window_v1::Request,
_data: &WeftShellWindowData,
_dh: &DisplayHandle,
_data_init: &mut DataInit<'_, Self>,
) {
match request {
zweft_shell_window_v1::Request::Destroy => {}
zweft_shell_window_v1::Request::UpdateMetadata { title, role } => {
let _ = (title, role);
}
zweft_shell_window_v1::Request::SetGeometry {
x,
y,
width,
height,
} => {
resource.configure(x, y, width, height, 0);
}
}
}
}

View file

@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="weft_shell_unstable_v1">
<copyright>
Copyright (C) 2026 WEFT OS contributors
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice (including the
next paragraph) shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
</copyright>
<description summary="WEFT compositorshell protocol">
This protocol defines the boundary between weft-compositor and
weft-servo-shell. It allows the shell to manage window slots on behalf
of application surfaces that the compositor owns.
The shell binds the weft_shell_manager_v1 global once per connection.
The manager creates weft_shell_window_v1 objects, each representing one
visual window slot. The compositor is authoritative for geometry,
focus, and stacking. The shell is authoritative for window metadata,
chrome layout, and the decision to create or destroy a window slot.
Warning: This protocol is unstable and subject to change before version
1 is finalized. Clients must check the version advertised by the
compositor and handle unknown events gracefully.
</description>
<!-- ───────────────────────────── Manager ───────────────────────────── -->
<interface name="zweft_shell_manager_v1" version="1">
<description summary="shell session manager">
Bound once by weft-servo-shell. The manager owns the shell session.
If the binding is destroyed the compositor invalidates all window
objects that were created through it.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the manager and end the shell session">
Destroys the manager. All weft_shell_window_v1 objects created
through this manager become inert. The compositor will emit
window_closed on each before processing the destructor.
</description>
</request>
<request name="create_window">
<description summary="create a window slot">
Creates a new window slot. The compositor assigns the protocol
object identity; no separate window_id is exchanged. The
compositor will emit configure on the new object to communicate the
effective initial geometry and state.
</description>
<arg name="id" type="new_id" interface="zweft_shell_window_v1"
summary="new window object"/>
<arg name="app_id" type="string"
summary="application identifier, reverse-DNS format"/>
<arg name="title" type="string"
summary="initial window title"/>
<arg name="role" type="string"
summary="window role hint (normal, dialog, panel, overlay)"/>
<arg name="x" type="int"
summary="requested x position in compositor logical coordinates"/>
<arg name="y" type="int"
summary="requested y position in compositor logical coordinates"/>
<arg name="width" type="int"
summary="requested width in compositor logical coordinates"/>
<arg name="height" type="int"
summary="requested height in compositor logical coordinates"/>
</request>
</interface>
<!-- ───────────────────────────── Window ────────────────────────────── -->
<interface name="zweft_shell_window_v1" version="1">
<description summary="a compositor-managed window slot">
Represents one visual window. The compositor controls the effective
geometry, stacking, and focus state. The shell controls metadata and
may request geometry changes; the compositor may reject or adjust
such requests.
A window object becomes inert after window_closed is received. The
shell must call destroy on an inert object as soon as possible.
</description>
<enum name="state" bitfield="true">
<description summary="window state bitmask"/>
<entry name="maximized" value="1" summary="window occupies full output area"/>
<entry name="fullscreen" value="2" summary="window covers the entire output including chrome"/>
<entry name="activated" value="4" summary="window has input focus"/>
<entry name="resizing" value="8" summary="an interactive resize is in progress"/>
</enum>
<enum name="error">
<description summary="protocol errors"/>
<entry name="defunct_window" value="0"
summary="request sent after window_closed was received"/>
</enum>
<request name="destroy" type="destructor">
<description summary="destroy the window object">
Destroys the window object. If the window is still alive the
compositor will remove the corresponding window slot from the
display. The shell must not send requests on this object after
calling destroy.
</description>
</request>
<request name="update_metadata">
<description summary="update window title and role">
Updates advisory metadata. The compositor may use these values in
window decorations or accessibility trees. Updates are not
authoritative for policy decisions.
</description>
<arg name="title" type="string" summary="new window title"/>
<arg name="role" type="string" summary="new window role hint"/>
</request>
<request name="set_geometry">
<description summary="request a geometry change">
Asks the compositor to change the window geometry. The compositor
validates the request against output bounds and current policy. The
compositor is not required to honor the exact values. A configure
event will be sent with the effective geometry that results.
</description>
<arg name="x" type="int" summary="requested x position"/>
<arg name="y" type="int" summary="requested y position"/>
<arg name="width" type="int" summary="requested width"/>
<arg name="height" type="int" summary="requested height"/>
</request>
<event name="configure">
<description summary="compositor sets effective geometry and state">
Sent by the compositor when the effective geometry or state
changes. The shell must treat this as authoritative and update its
DOM layout to match. The shell must ack each configure event by
sending an update_metadata request or by doing nothing if no
metadata changed; no explicit ack request is defined in version 1.
</description>
<arg name="x" type="int" summary="effective x position"/>
<arg name="y" type="int" summary="effective y position"/>
<arg name="width" type="int" summary="effective width"/>
<arg name="height" type="int" summary="effective height"/>
<arg name="state" type="uint" summary="bitmask of state enum values"/>
</event>
<event name="focus_changed">
<description summary="input focus state changed">
Sent when surface-level focus changes for this window slot. The
shell updates visual focus state in its DOM but does not override
compositor focus policy.
</description>
<arg name="focused" type="uint" summary="1 if focused, 0 if not"/>
</event>
<event name="window_closed">
<description summary="window slot is no longer valid">
Sent by the compositor when the window object is being invalidated.
After this event the shell must call destroy on the object. Any
request sent after window_closed is a protocol error.
</description>
</event>
<event name="presentation_feedback">
<description summary="frame presentation timing">
Sent after a frame containing this window's content is presented
to the output. Allows the shell to align animations and resize
flows with compositor timing. tv_sec and tv_nsec are wall-clock
values from the compositor's monotonic clock. refresh is the
output refresh interval in nanoseconds; 0 if unknown.
</description>
<arg name="tv_sec" type="uint" summary="seconds component of presentation time"/>
<arg name="tv_nsec" type="uint" summary="nanoseconds component of presentation time"/>
<arg name="refresh" type="uint" summary="output refresh interval in nanoseconds"/>
</event>
</interface>
</protocol>