feat(servo-embed): wire Servo deps and share Wayland surface with shell client

- Add servo/winit/softbuffer as optional deps in weft-servo-shell and
  weft-app-shell Cargo.toml, gated on servo-embed feature
- Replace ShellClient::connect() and connect_as_app() with
  connect_with_display() and connect_as_app_with_display(), using
  Backend::from_foreign_display to share the winit wl_display connection
- Move ShellClient construction into resumed() in both embedders after
  winit window and wl_surface are available
- Pass actual wl_surface to create_window instead of None
- Fix pre-existing field name bug: wayland-scanner generates _type for
  the reserved keyword arg name=type, not r#type
This commit is contained in:
Marco Allegretti 2026-03-12 15:16:17 +01:00
parent 4edfa00e22
commit 34359acf3f
9 changed files with 7248 additions and 176 deletions

7231
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,11 +9,10 @@ name = "weft-app-shell"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
# Enable actual Servo rendering. Requires manually adding the deps listed in # Enable actual Servo rendering. Deps are declared as optional below and only
# weft-servo-shell/SERVO_PIN.md to this file before building; they are not # fetched when this feature is active. The Servo monorepo (~1 GB) is not
# included here to avoid pulling the Servo monorepo (~1 GB) into every # downloaded during a plain `cargo check` or `cargo build` without this feature.
# `cargo check` cycle. servo-embed = ["dep:servo", "dep:winit", "dep:softbuffer", "dep:serde", "dep:toml"]
servo-embed = ["dep:serde", "dep:toml"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
@ -25,3 +24,18 @@ wayland-scanner = "0.31"
bitflags = "2" bitflags = "2"
serde = { version = "1", features = ["derive"], optional = true } serde = { version = "1", features = ["derive"], optional = true }
toml = { version = "0.8", optional = true } toml = { version = "0.8", optional = true }
[dependencies.servo]
git = "https://github.com/marcoallegretti/servo"
branch = "servo-weft"
optional = true
default-features = false
[dependencies.winit]
version = "0.30"
optional = true
features = ["wayland"]
[dependencies.softbuffer]
version = "0.4"
optional = true

View file

@ -14,6 +14,7 @@ use winit::{
event::{ElementState, MouseButton, WindowEvent}, event::{ElementState, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
keyboard::ModifiersState, keyboard::ModifiersState,
platform::wayland::{ActiveEventLoopExtWayland, WindowExtWayland},
window::{Window, WindowAttributes, WindowId}, window::{Window, WindowAttributes, WindowId},
}; };
@ -74,6 +75,7 @@ impl RenderingCtx {
struct App { struct App {
url: ServoUrl, url: ServoUrl,
app_id: String,
session_id: u64, session_id: u64,
ws_port: u16, ws_port: u16,
window: Option<Arc<Window>>, window: Option<Arc<Window>>,
@ -92,13 +94,14 @@ struct App {
impl App { impl App {
fn new( fn new(
url: ServoUrl, url: ServoUrl,
app_id: String,
session_id: u64, session_id: u64,
ws_port: u16, ws_port: u16,
waker: WeftEventLoopWaker, waker: WeftEventLoopWaker,
shell_client: Option<crate::shell_client::ShellClient>,
) -> Self { ) -> Self {
Self { Self {
url, url,
app_id,
session_id, session_id,
ws_port, ws_port,
window: None, window: None,
@ -111,7 +114,7 @@ impl App {
ready_signalled: false, ready_signalled: false,
modifiers: ModifiersState::default(), modifiers: ModifiersState::default(),
cursor_pos: servo::euclid::default::Point2D::zero(), cursor_pos: servo::euclid::default::Point2D::zero(),
shell_client, shell_client: None,
} }
} }
@ -160,6 +163,22 @@ impl ApplicationHandler<ServoWake> for App {
let size = window.inner_size(); let size = window.inner_size();
self.window = Some(Arc::clone(&window)); self.window = Some(Arc::clone(&window));
if self.shell_client.is_none() {
if let (Some(disp), Some(surf)) =
(event_loop.wayland_display(), window.wayland_surface())
{
match crate::shell_client::ShellClient::connect_as_app_with_display(
&self.app_id,
self.session_id,
disp,
surf,
) {
Ok(sc) => self.shell_client = Some(sc),
Err(e) => tracing::warn!(error = %e, "shell protocol unavailable"),
}
}
}
let servo = ServoBuilder::default() let servo = ServoBuilder::default()
.event_loop_waker(Box::new(self.waker.clone())) .event_loop_waker(Box::new(self.waker.clone()))
.build(); .build();
@ -421,7 +440,6 @@ pub fn run(
app_id: &str, app_id: &str,
session_id: u64, session_id: u64,
ws_port: u16, ws_port: u16,
shell_client: Option<crate::shell_client::ShellClient>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let url = resolve_weft_app_url(app_id) let url = resolve_weft_app_url(app_id)
.ok_or_else(|| anyhow::anyhow!("no ui/index.html found for app {app_id}"))?; .ok_or_else(|| anyhow::anyhow!("no ui/index.html found for app {app_id}"))?;
@ -434,7 +452,7 @@ pub fn run(
proxy: Arc::new(Mutex::new(event_loop.create_proxy())), proxy: Arc::new(Mutex::new(event_loop.create_proxy())),
}; };
let mut app = App::new(url, session_id, ws_port, waker, shell_client); let mut app = App::new(url, app_id.to_owned(), session_id, ws_port, waker);
event_loop event_loop
.run_app(&mut app) .run_app(&mut app)
.map_err(|e| anyhow::anyhow!("event loop run: {e}")) .map_err(|e| anyhow::anyhow!("event loop run: {e}"))

View file

@ -28,18 +28,7 @@ fn main() -> anyhow::Result<()> {
let ws_port = appd_ws_port(); let ws_port = appd_ws_port();
let shell = match shell_client::ShellClient::connect_as_app(&app_id, session_id) { embed_app(&app_id, session_id, ws_port)
Ok(c) => {
tracing::info!("app shell window registered with compositor");
Some(c)
}
Err(e) => {
tracing::warn!(error = %e, "shell protocol unavailable; continuing without compositor registration");
None
}
};
embed_app(&app_id, session_id, ws_port, shell)
} }
fn appd_ws_port() -> u16 { fn appd_ws_port() -> u16 {
@ -63,14 +52,13 @@ fn embed_app(
app_id: &str, app_id: &str,
session_id: u64, session_id: u64,
ws_port: u16, ws_port: u16,
shell_client: Option<shell_client::ShellClient>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
#[cfg(feature = "servo-embed")] #[cfg(feature = "servo-embed")]
return embedder::run(app_id, session_id, ws_port, shell_client); return embedder::run(app_id, session_id, ws_port);
#[cfg(not(feature = "servo-embed"))] #[cfg(not(feature = "servo-embed"))]
{ {
let _ = (app_id, session_id, ws_port, shell_client); let _ = (app_id, session_id, ws_port);
println!("READY"); println!("READY");
use std::io::Write; use std::io::Write;
let _ = std::io::stdout().flush(); let _ = std::io::stdout().flush();

View file

@ -129,9 +129,26 @@ pub struct ShellClient {
} }
impl ShellClient { impl ShellClient {
pub fn connect_as_app(app_id: &str, session_id: u64) -> anyhow::Result<Self> { /// Connect using winit's existing Wayland display handle.
let conn = ///
Connection::connect_to_env().context("failed to connect to Wayland compositor")?; /// See `weft-servo-shell/src/shell_client.rs` for the rationale on
/// `Backend::from_foreign_display`. The `surface_ptr` is the `wl_surface*`
/// from the same winit connection, enabling the compositor to associate the
/// application window with the rendered surface.
#[cfg(feature = "servo-embed")]
pub fn connect_as_app_with_display(
app_id: &str,
session_id: u64,
display_ptr: *mut std::ffi::c_void,
surface_ptr: *mut std::ffi::c_void,
) -> anyhow::Result<Self> {
use wayland_client::Proxy;
use wayland_client::backend::{Backend, ObjectId};
// Safety: display_ptr is winit's wl_display*, valid for the event loop lifetime.
let conn = unsafe {
Connection::from_backend(Backend::from_foreign_display(display_ptr as *mut _))
};
let mut event_queue = conn.new_event_queue::<AppData>(); let mut event_queue = conn.new_event_queue::<AppData>();
let qh = event_queue.handle(); let qh = event_queue.handle();
@ -148,13 +165,20 @@ impl ShellClient {
"zweft_shell_manager_v1 not advertised; WEFT compositor must be running" "zweft_shell_manager_v1 not advertised; WEFT compositor must be running"
); );
// Safety: surface_ptr is winit's wl_surface* on the same wl_display connection.
let surface = unsafe {
let id = ObjectId::from_ptr(WlSurface::interface(), surface_ptr as *mut _)
.context("wl_surface ObjectId import")?;
WlSurface::from_id(&conn, id).context("wl_surface from_id")?
};
let manager = data.manager.as_ref().unwrap(); let manager = data.manager.as_ref().unwrap();
let title = format!("{app_id}/{session_id}"); let title = format!("{app_id}/{session_id}");
let window = manager.create_window( let window = manager.create_window(
app_id.to_string(), app_id.to_string(),
title, title,
"application".to_string(), "application".to_string(),
None::<&WlSurface>, Some(&surface),
0, 0,
0, 0,
0, 0,

View file

@ -9,10 +9,10 @@ name = "weft-servo-shell"
path = "src/main.rs" path = "src/main.rs"
[features] [features]
# Enable actual Servo rendering. Requires manually adding the deps listed in # Enable actual Servo rendering. Deps are declared as optional below and only
# SERVO_PIN.md to this file before building; they are not included here to # fetched when this feature is active. The Servo monorepo (~1 GB) is not
# avoid pulling the Servo monorepo (~1 GB) into every `cargo check` cycle. # downloaded during a plain `cargo check` or `cargo build` without this feature.
servo-embed = [] servo-embed = ["dep:servo", "dep:winit", "dep:softbuffer"]
[dependencies] [dependencies]
anyhow = "1.0" anyhow = "1.0"
@ -24,3 +24,18 @@ wayland-scanner = "0.31"
bitflags = "2" bitflags = "2"
serde_json = "1" serde_json = "1"
tungstenite = "0.24" tungstenite = "0.24"
[dependencies.servo]
git = "https://github.com/marcoallegretti/servo"
branch = "servo-weft"
optional = true
default-features = false
[dependencies.winit]
version = "0.30"
optional = true
features = ["wayland"]
[dependencies.softbuffer]
version = "0.4"
optional = true

View file

@ -14,6 +14,7 @@ use winit::{
event::{ElementState, MouseButton, WindowEvent}, event::{ElementState, MouseButton, WindowEvent},
event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy},
keyboard::ModifiersState, keyboard::ModifiersState,
platform::wayland::{ActiveEventLoopExtWayland, WindowExtWayland},
window::{Window, WindowAttributes, WindowId}, window::{Window, WindowAttributes, WindowId},
}; };
@ -102,7 +103,6 @@ impl App {
url: ServoUrl, url: ServoUrl,
waker: WeftEventLoopWaker, waker: WeftEventLoopWaker,
ws_port: u16, ws_port: u16,
shell_client: Option<crate::shell_client::ShellClient>,
) -> Self { ) -> Self {
Self { Self {
url, url,
@ -116,7 +116,7 @@ impl App {
shutting_down: false, shutting_down: false,
modifiers: ModifiersState::default(), modifiers: ModifiersState::default(),
cursor_pos: servo::euclid::default::Point2D::zero(), cursor_pos: servo::euclid::default::Point2D::zero(),
shell_client, shell_client: None,
} }
} }
@ -164,6 +164,17 @@ impl ApplicationHandler<ServoWake> for App {
let size = window.inner_size(); let size = window.inner_size();
self.window = Some(Arc::clone(&window)); self.window = Some(Arc::clone(&window));
if self.shell_client.is_none() {
if let (Some(disp), Some(surf)) =
(event_loop.wayland_display(), window.wayland_surface())
{
match crate::shell_client::ShellClient::connect_with_display(disp, surf) {
Ok(sc) => self.shell_client = Some(sc),
Err(e) => tracing::warn!(error = %e, "shell protocol unavailable"),
}
}
}
let servo = ServoBuilder::default() let servo = ServoBuilder::default()
.event_loop_waker(Box::new(self.waker.clone())) .event_loop_waker(Box::new(self.waker.clone()))
.build(); .build();
@ -380,7 +391,6 @@ fn app_store_roots() -> Vec<PathBuf> {
pub fn run( pub fn run(
html_path: &Path, html_path: &Path,
ws_port: u16, ws_port: u16,
shell_client: Option<crate::shell_client::ShellClient>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let url_str = format!("file://{}", html_path.display()); let url_str = format!("file://{}", html_path.display());
let raw_url = let raw_url =
@ -395,7 +405,7 @@ pub fn run(
proxy: Arc::new(Mutex::new(event_loop.create_proxy())), proxy: Arc::new(Mutex::new(event_loop.create_proxy())),
}; };
let mut app = App::new(url, waker, ws_port, shell_client); let mut app = App::new(url, waker, ws_port);
event_loop event_loop
.run_app(&mut app) .run_app(&mut app)
.map_err(|e| anyhow::anyhow!("event loop run: {e}")) .map_err(|e| anyhow::anyhow!("event loop run: {e}"))

View file

@ -32,18 +32,7 @@ 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() { embed_servo(&wayland_display, &html_path, ws_port)
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, shell)
} }
fn system_ui_html_path() -> anyhow::Result<PathBuf> { fn system_ui_html_path() -> anyhow::Result<PathBuf> {
@ -84,14 +73,12 @@ fn embed_servo(
_wayland_display: &str, _wayland_display: &str,
html_path: &std::path::Path, html_path: &std::path::Path,
ws_port: u16, ws_port: u16,
shell_client: Option<shell_client::ShellClient>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
#[cfg(feature = "servo-embed")] #[cfg(feature = "servo-embed")]
return embedder::run(html_path, ws_port, shell_client); return embedder::run(html_path, ws_port);
#[cfg(not(feature = "servo-embed"))] #[cfg(not(feature = "servo-embed"))]
{ {
let _ = shell_client;
tracing::warn!( tracing::warn!(
path = %html_path.display(), path = %html_path.display(),
ws_port, ws_port,

View file

@ -132,14 +132,14 @@ impl Dispatch<ZweftShellWindowV1, ()> for AppData {
tracing::trace!(tv_sec, tv_nsec, refresh, "shell presentation feedback"); tracing::trace!(tv_sec, tv_nsec, refresh, "shell presentation feedback");
} }
zweft_shell_window_v1::Event::NavigationGesture { zweft_shell_window_v1::Event::NavigationGesture {
r#type, _type,
fingers, fingers,
dx, dx,
dy, dy,
} => { } => {
tracing::debug!(r#type, fingers, dx, dy, "navigation gesture from compositor"); tracing::debug!(_type, fingers, dx, dy, "navigation gesture from compositor");
state.pending_gestures.push(PendingGesture { state.pending_gestures.push(PendingGesture {
gesture_type: r#type, gesture_type: _type,
fingers, fingers,
dx, dx,
dy, dy,
@ -157,9 +157,23 @@ pub struct ShellClient {
} }
impl ShellClient { impl ShellClient {
pub fn connect() -> anyhow::Result<Self> { /// Connect to the compositor using winit's existing Wayland display handle.
let conn = ///
Connection::connect_to_env().context("failed to connect to Wayland compositor")?; /// Using `Backend::from_foreign_display` shares winit's `wl_display` connection so
/// that `surface_ptr` (a `wl_surface*` owned by winit) is a valid object on the same
/// client, enabling the compositor to link the shell window to the actual surface.
#[cfg(feature = "servo-embed")]
pub fn connect_with_display(
display_ptr: *mut std::ffi::c_void,
surface_ptr: *mut std::ffi::c_void,
) -> anyhow::Result<Self> {
use wayland_client::Proxy;
use wayland_client::backend::{Backend, ObjectId};
// Safety: display_ptr is winit's wl_display*, valid for the event loop lifetime.
let conn = unsafe {
Connection::from_backend(Backend::from_foreign_display(display_ptr as *mut _))
};
let mut event_queue = conn.new_event_queue::<AppData>(); let mut event_queue = conn.new_event_queue::<AppData>();
let qh = event_queue.handle(); let qh = event_queue.handle();
@ -176,12 +190,19 @@ impl ShellClient {
"zweft_shell_manager_v1 not advertised; WEFT compositor must be running" "zweft_shell_manager_v1 not advertised; WEFT compositor must be running"
); );
// Safety: surface_ptr is winit's wl_surface* on the same wl_display connection.
let surface = unsafe {
let id = ObjectId::from_ptr(WlSurface::interface(), surface_ptr as *mut _)
.context("wl_surface ObjectId import")?;
WlSurface::from_id(&conn, id).context("wl_surface from_id")?
};
let manager = data.manager.as_ref().unwrap(); let manager = data.manager.as_ref().unwrap();
let window = manager.create_window( let window = manager.create_window(
"org.weft.system.shell".to_string(), "org.weft.system.shell".to_string(),
"WEFT Shell".to_string(), "WEFT Shell".to_string(),
"panel".to_string(), "panel".to_string(),
None::<&WlSurface>, Some(&surface),
0, 0,
0, 0,
0, 0,