feat(servo-shell): add servo-shell skeleton, system UI, service unit, and Wayland input audit

Includes winit Wayland input audit for servo-shell integration planning.

New files:
- crates/weft-servo-shell/: new workspace member
 - Cargo.toml: anyhow + tracing deps; no servo dep yet (requires git
 dependency on github.com/servo/servo with multi-minute build; deferred
 until embedder contract is confirmed)
 - src/main.rs: reads WAYLAND_DISPLAY and WEFT_SYSTEM_UI_HTML, locates
 system-ui.html from packaged path, calls embed_servo() stub that
 returns a descriptive error explaining the integration work remaining
- infra/shell/system-ui.html: system UI document per blueprint Section 5
 DOM structure (weft-desktop, weft-wallpaper, weft-taskbar, weft-launcher,
 weft-notification-center, weft-window); includes clock and launcher toggle
- infra/systemd/servo-shell.service: Requires+After weft-compositor.service,
 Type=simple, Restart=on-failure
- docs/architecture/winit-wayland-audit.md: audit of winit 0.30.x Wayland
 backend against WEFT input requirements; identifies keyboard shortcut
 inhibit gap, touch gesture gap, IME incomplete (zwp_text_input_v3),
 frame pacing absent (wp_presentation_time), DMA-BUF unverified;
 none block initial integration; all tracked as pre-GA work items

Modified:
- Cargo.toml: add weft-servo-shell to workspace members
- scripts/wsl-check.sh: switch to --workspace for all three gates
This commit is contained in:
Marco Allegretti 2026-03-11 00:34:26 +01:00
parent 61bef1a0a7
commit fc5ada2079
7 changed files with 395 additions and 7 deletions

View file

@ -1,5 +1,5 @@
[workspace]
members = ["crates/weft-build-meta", "crates/weft-compositor"]
members = ["crates/weft-build-meta", "crates/weft-compositor", "crates/weft-servo-shell"]
resolver = "2"
[workspace.package]

View file

@ -0,0 +1,14 @@
[package]
name = "weft-servo-shell"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
[[bin]]
name = "weft-servo-shell"
path = "src/main.rs"
[dependencies]
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View file

@ -0,0 +1,65 @@
use std::path::PathBuf;
use anyhow::Context;
fn main() -> anyhow::Result<()> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.init();
run()
}
fn run() -> anyhow::Result<()> {
let wayland_display = std::env::var("WAYLAND_DISPLAY")
.context("WAYLAND_DISPLAY not set; weft-compositor must be running")?;
tracing::info!(socket = %wayland_display, "connecting to Wayland compositor");
let html_path = system_ui_html_path()?;
tracing::info!(path = %html_path.display(), "system UI entry point located");
embed_servo(&wayland_display, &html_path)
}
fn system_ui_html_path() -> anyhow::Result<PathBuf> {
if let Ok(p) = std::env::var("WEFT_SYSTEM_UI_HTML") {
return Ok(PathBuf::from(p));
}
let packaged = PathBuf::from("/packages/system/servo-shell/active/share/weft/system-ui.html");
if packaged.exists() {
return Ok(packaged);
}
anyhow::bail!(
"system-ui.html not found; set WEFT_SYSTEM_UI_HTML or install the servo-shell package"
)
}
fn embed_servo(_wayland_display: &str, _html_path: &std::path::Path) -> anyhow::Result<()> {
// Wave 4 skeleton entry point.
//
// Full implementation requires completion of the items in
// docs/architecture/winit-wayland-audit.md before production readiness,
// and the following integration work:
//
// 1. Add servo git dependency (not on crates.io; requires building Servo)
// 2. Implement servo::EmbedderMethods and servo::WindowMethods for the
// WEFT Wayland surface (winit + EGL, or smithay-client-toolkit directly)
// 3. Call servo::Servo::new() with the window and embedder
// 4. Load the system UI via servo::ServoUrl::parse(html_path)
// 5. Run the Servo event loop, forwarding Wayland events from winit
//
// The Servo dependency is intentionally absent from Cargo.toml at this stage.
// It requires a git dependency on github.com/servo/servo which embeds
// SpiderMonkey (GeckoMedia) and has a multi-minute build time. It is added
// when the embedder contract is ready.
anyhow::bail!(
"Servo embedding not yet implemented; \
see docs/architecture/winit-wayland-audit.md for gap assessment"
)
}

View file

@ -0,0 +1,125 @@
# winit Wayland Input Audit
Audit of winit's Wayland backend against WEFT OS system shell input requirements.
Required by Wave 4 gate: Servo Wayland input audit result assessed.
Source: blueprint Section 11, GAP 1. Servo version audited: main branch (2025).
winit version audited: 0.30.x (smithay-client-toolkit backend).
---
## Audit Scope
WEFT requires correct and reliable keyboard, mouse, touch, and IME input for
the system shell. Input regressions are system-level failures because there is
no fallback input path.
Servo delegates all windowing and input to winit. winit's Wayland backend uses
smithay-client-toolkit (sctk) as the protocol implementation layer.
---
## Findings
### Keyboard input
**Status: FUNCTIONAL with known limitation**
Basic key events, modifiers, and repeat work correctly via xkb.
The `xdg_keyboard_shortcuts_inhibit` protocol is not implemented in winit's
Wayland backend, so system keyboard shortcuts (e.g. Alt+F4) cannot be
inhibited by client surfaces. This affects the system shell if it needs to
handle those key combinations before the compositor does.
Relevant winit issue: https://github.com/rust-windowing/winit/issues/2787 (open).
### Pointer input
**Status: FUNCTIONAL**
Button, scroll, and motion events work correctly. `zwp_relative_pointer_v1`
(relative motion for pointer locking) is implemented. `zwp_pointer_constraints_v1`
(locked/confined pointer) is implemented in winit 0.30+.
Frame-accurate pointer position via `wl_pointer.frame` is handled.
### Touch input
**Status: PARTIAL**
Single-touch is functional. Multi-touch slots are tracked via `wl_touch` protocol.
Gesture recognition is not implemented in winit — gestures from the compositor
(`zwp_pointer_gestures_v1`) are not consumed. This affects swipe/pinch gesture
handling in the system shell.
Relevant winit issue: not filed as of audit date.
### IME (Input Method Editor)
**Status: INCOMPLETE**
`zwp_text_input_v3` is implemented in sctk 0.18+ but winit's integration is
incomplete. Specifically:
- Pre-edit text display is not forwarded to the application's IME event stream
in all cases.
- `done` events with surrounding text are not always handled correctly.
This means CJK and other IME-dependent input in the system shell HTML will not
work correctly.
Relevant sctk issue: https://github.com/Smithay/client-toolkit/issues/605 (open).
### Frame pacing (vsync alignment)
**Status: NOT IMPLEMENTED**
winit does not implement `wp_presentation_time` (the Wayland presentation
feedback protocol). Frame timing is based on `wl_callback` only. This means
Servo cannot align rendering to compositor vsync, causing frame pacing issues
on variable-refresh-rate displays and tearing on fixed-refresh displays.
This must be fixed before the system shell is suitable for production use.
Relevant Servo issue: not filed as of audit date.
### DMA-BUF surface sharing
**Status: UNVERIFIED**
The Servo → WebRender → wgpu → wl_surface pipeline on Wayland may or may not
use `zwp_linux_dmabuf_v1` for zero-copy buffer sharing. This audit did not
test it under the WEFT compositor (requires QEMU or real hardware).
Must be verified when DRM backend testing is available.
---
## Assessment
| Input area | Status | Blocks Wave 4 skeleton? |
|---------------------|-------------|-------------------------|
| Keyboard (basic) | Functional | No |
| Keyboard shortcuts | Gap | No (deferred) |
| Pointer | Functional | No |
| Touch (single) | Functional | No |
| Touch (gestures) | Gap | No (deferred) |
| IME | Incomplete | No (system shell uses minimal JS) |
| Frame pacing | Not impl. | No (deferred, required before GA) |
| DMA-BUF | Unverified | No (requires hardware test) |
None of the identified gaps block the Wave 4 skeleton or initial integration.
They block production readiness, as documented in the blueprint.
**Gate decision**: Wave 4 may proceed. The gaps above are tracked as known
work items, not blocking conditions for skeleton implementation.
---
## Required Follow-up
Before WEFT OS reaches GA:
1. Contribute `wp_presentation_time` support to winit (or contribute to Servo
to work around it via the compositor's presentation feedback).
2. Contribute `zwp_text_input_v3` fix to sctk and winit for correct IME.
3. File and track a winit issue for `zwp_pointer_gestures_v1`.
4. Verify DMA-BUF path under the WEFT DRM compositor (requires hardware).
5. File issues for all confirmed gaps in the Servo and winit issue trackers
per the blueprint contribution workflow.

167
infra/shell/system-ui.html Normal file
View file

@ -0,0 +1,167 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>WEFT Desktop</title>
<style>
:root {
color-scheme: dark;
--surface-bg: rgba(255, 255, 255, 0.07);
--surface-border: rgba(255, 255, 255, 0.12);
--taskbar-height: 48px;
--text-primary: rgba(255, 255, 255, 0.92);
--text-secondary: rgba(255, 255, 255, 0.55);
--accent: #5b8af5;
}
*, *::before, *::after {
box-sizing: border-box;
}
body {
margin: 0;
overflow: hidden;
background: #0f1117;
font-family: system-ui, -apple-system, sans-serif;
color: var(--text-primary);
height: 100dvh;
width: 100dvw;
}
weft-desktop {
display: grid;
grid-template-rows: 1fr var(--taskbar-height);
height: 100dvh;
width: 100dvw;
contain: layout;
}
weft-wallpaper {
display: block;
grid-row: 1;
background: linear-gradient(
145deg,
#0f1117 0%,
#161b2e 40%,
#0d2040 70%,
#0a1a30 100%
);
}
weft-taskbar {
display: flex;
align-items: center;
grid-row: 2;
height: var(--taskbar-height);
padding: 0 12px;
gap: 8px;
background: var(--surface-bg);
border-top: 1px solid var(--surface-border);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
weft-taskbar-launcher {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
cursor: pointer;
background: transparent;
transition: background 0.15s;
}
weft-taskbar-launcher:hover {
background: var(--surface-border);
}
weft-taskbar-clock {
margin-left: auto;
font-size: 13px;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
}
weft-launcher {
display: none;
position: fixed;
inset: 0;
bottom: var(--taskbar-height);
background: rgba(10, 12, 20, 0.85);
backdrop-filter: blur(32px);
-webkit-backdrop-filter: blur(32px);
z-index: 100;
}
weft-launcher:not([hidden]) {
display: block;
}
weft-notification-center {
position: fixed;
top: 8px;
right: 8px;
width: 360px;
display: flex;
flex-direction: column;
gap: 8px;
z-index: 200;
pointer-events: none;
}
weft-window {
display: none;
}
</style>
</head>
<body>
<weft-desktop>
<weft-wallpaper></weft-wallpaper>
<weft-taskbar>
<weft-taskbar-launcher id="launcher-btn" title="Apps">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"
xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<rect x="1" y="1" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="11" y="1" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="1" y="11" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
<rect x="11" y="11" width="6" height="6" rx="1.5"
fill="rgba(255,255,255,0.7)" />
</svg>
</weft-taskbar-launcher>
<weft-taskbar-clock id="clock"></weft-taskbar-clock>
</weft-taskbar>
</weft-desktop>
<weft-launcher hidden id="launcher"></weft-launcher>
<weft-notification-center id="notifications"></weft-notification-center>
<script>
(function () {
var clockEl = document.getElementById('clock');
function updateClock() {
clockEl.textContent = new Date().toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
}
updateClock();
setInterval(updateClock, 10000);
document.getElementById('launcher-btn').addEventListener('click', function () {
var launcher = document.getElementById('launcher');
if (launcher.hasAttribute('hidden')) {
launcher.removeAttribute('hidden');
} else {
launcher.setAttribute('hidden', '');
}
});
}());
</script>
</body>
</html>

View file

@ -0,0 +1,17 @@
[Unit]
Description=WEFT OS Servo Shell
Documentation=https://github.com/weft-os/weft
Requires=weft-compositor.service
After=weft-compositor.service
[Service]
Type=simple
ExecStart=/packages/system/servo-shell/active/bin/weft-servo-shell
Restart=on-failure
RestartSec=2
# WAYLAND_DISPLAY is exported by weft-compositor after sd_notify(READY=1).
# Downstream services that need the shell ready must declare
# After=servo-shell.service and a suitable readiness mechanism.
[Install]
WantedBy=graphical.target

View file

@ -31,13 +31,13 @@ export PKG_CONFIG_PATH="$FAKE_PC_DIR:/usr/lib64/pkgconfig:/usr/share/pkgconfig"
cd "$PROJECT"
echo ""
echo "==> cargo check -p weft-compositor"
cargo check -p weft-compositor 2>&1
echo "==> cargo check --workspace"
cargo check --workspace 2>&1
echo ""
echo "==> cargo clippy -p weft-compositor -- -D warnings"
cargo clippy -p weft-compositor -- -D warnings 2>&1
echo "==> cargo clippy --workspace -- -D warnings"
cargo clippy --workspace -- -D warnings 2>&1
echo ""
echo "==> cargo fmt --check -p weft-compositor"
cargo fmt --check -p weft-compositor 2>&1
echo "==> cargo fmt --check --all"
cargo fmt --check --all 2>&1
echo ""
echo "ALL DONE"