feat(servo-shell): implement weft-shell-protocol Wayland client

Adds src/shell_client.rs: connects to the WEFT compositor via
WAYLAND_DISPLAY, binds zweft_shell_manager_v1, and calls create_window
for the system UI shell slot (app_id org.weft.system.shell, role panel,
wl_surface null until Servo surface is wired in a later task).

Implements Dispatch for WlRegistry, ZweftShellManagerV1, and
ZweftShellWindowV1. Handles all four window events: configure,
focus_changed, window_closed (calls destroy), and presentation_feedback.

run() in main.rs calls ShellClient::connect() best-effort before
embed_servo; logs a warning if the compositor is not running rather than
propagating the error.
This commit is contained in:
Marco Allegretti 2026-03-11 14:59:58 +01:00
parent 2a9f034815
commit d425fa8328
2 changed files with 213 additions and 0 deletions

View file

@ -5,6 +5,7 @@ use anyhow::Context;
#[cfg(feature = "servo-embed")] #[cfg(feature = "servo-embed")]
mod embedder; mod embedder;
mod protocols; mod protocols;
mod shell_client;
fn main() -> anyhow::Result<()> { fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
@ -29,6 +30,17 @@ fn run() -> anyhow::Result<()> {
let ws_port = appd_ws_port(); let ws_port = appd_ws_port();
tracing::info!(ws_port, "appd WebSocket port"); tracing::info!(ws_port, "appd WebSocket port");
let _shell = match shell_client::ShellClient::connect() {
Ok(c) => {
tracing::info!("shell window registered with compositor");
Some(c)
}
Err(e) => {
tracing::warn!(error = %e, "shell protocol unavailable; continuing without compositor registration");
None
}
};
embed_servo(&wayland_display, &html_path, ws_port) embed_servo(&wayland_display, &html_path, ws_port)
} }

View file

@ -0,0 +1,201 @@
use anyhow::Context;
use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle,
protocol::{wl_registry, wl_surface::WlSurface},
};
use crate::protocols::{
ZweftShellManagerV1, ZweftShellWindowV1,
client::{zweft_shell_manager_v1, zweft_shell_window_v1},
};
// ── Window state ──────────────────────────────────────────────────────────────
pub struct ShellWindowState {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub state_flags: u32,
pub focused: bool,
pub closed: bool,
}
// ── Internal Wayland dispatch state ──────────────────────────────────────────
struct AppData {
manager: Option<ZweftShellManagerV1>,
window: Option<ZweftShellWindowV1>,
window_state: ShellWindowState,
}
impl AppData {
fn new() -> Self {
Self {
manager: None,
window: None,
window_state: ShellWindowState {
x: 0,
y: 0,
width: 0,
height: 0,
state_flags: 0,
focused: false,
closed: false,
},
}
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for AppData {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
&& interface == "zweft_shell_manager_v1"
{
let mgr = registry.bind::<ZweftShellManagerV1, _, _>(name, version.min(1), qh, ());
state.manager = Some(mgr);
}
}
}
impl Dispatch<ZweftShellManagerV1, ()> for AppData {
fn event(
_: &mut Self,
_: &ZweftShellManagerV1,
_event: zweft_shell_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<ZweftShellWindowV1, ()> for AppData {
fn event(
state: &mut Self,
window: &ZweftShellWindowV1,
event: zweft_shell_window_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zweft_shell_window_v1::Event::Configure {
x,
y,
width,
height,
state: flags,
} => {
let ws = &mut state.window_state;
ws.x = x;
ws.y = y;
ws.width = width;
ws.height = height;
ws.state_flags = flags;
tracing::debug!(x, y, width, height, flags, "shell window configure");
}
zweft_shell_window_v1::Event::FocusChanged { focused } => {
state.window_state.focused = focused != 0;
tracing::debug!(focused, "shell window focus changed");
}
zweft_shell_window_v1::Event::WindowClosed => {
tracing::info!("shell window closed by compositor");
state.window_state.closed = true;
window.destroy();
}
zweft_shell_window_v1::Event::PresentationFeedback {
tv_sec,
tv_nsec,
refresh,
} => {
tracing::trace!(tv_sec, tv_nsec, refresh, "shell presentation feedback");
}
}
}
}
// ── Public client ─────────────────────────────────────────────────────────────
#[allow(dead_code)]
pub struct ShellClient {
event_queue: EventQueue<AppData>,
data: AppData,
}
impl ShellClient {
pub fn connect() -> anyhow::Result<Self> {
let conn =
Connection::connect_to_env().context("failed to connect to Wayland compositor")?;
let mut event_queue = conn.new_event_queue::<AppData>();
let qh = event_queue.handle();
conn.display().get_registry(&qh, ());
let mut data = AppData::new();
event_queue
.roundtrip(&mut data)
.context("Wayland globals roundtrip")?;
anyhow::ensure!(
data.manager.is_some(),
"zweft_shell_manager_v1 not advertised; WEFT compositor must be running"
);
let manager = data.manager.as_ref().unwrap();
let window = manager.create_window(
"org.weft.system.shell".to_string(),
"WEFT Shell".to_string(),
"panel".to_string(),
None::<&WlSurface>,
0,
0,
0,
0,
&qh,
(),
);
data.window = Some(window);
event_queue
.roundtrip(&mut data)
.context("Wayland create_window roundtrip")?;
tracing::info!(
x = data.window_state.x,
y = data.window_state.y,
width = data.window_state.width,
height = data.window_state.height,
state_flags = data.window_state.state_flags,
"shell window registered with compositor"
);
Ok(Self { event_queue, data })
}
#[allow(dead_code)]
pub fn dispatch_pending(&mut self) -> anyhow::Result<bool> {
self.event_queue
.dispatch_pending(&mut self.data)
.context("Wayland dispatch")?;
self.event_queue.flush().context("Wayland flush")?;
Ok(!self.data.window_state.closed)
}
#[allow(dead_code)]
pub fn window_state(&self) -> &ShellWindowState {
&self.data.window_state
}
}