iced_servo
Embed a Servo webview as a regular widget inside an Iced application.
Servo renders into an offscreen SoftwareRenderingContext; the resulting frame is read back via read_to_image and drawn through the generic iced_frame widget (a wgpu-textured quad) that sits in the widget tree like any other element. ServoWebViewController implements the FrameSource trait from iced_frame. Mouse, keyboard, touch, IME, and window-focus events flow the other way: iced's native event system delivers them to the widget, which translates and forwards them to Servo via WebView::notify_input_event. Cross-platform by construction — there are no platform-specific child windows, WndProcs, NSView subviews, or X11/Wayland subsurfaces to maintain.
Requirements
- An iced application using the
wgpurenderer (the default). The widget relies oniced_wgpu::primitive::Rendererto draw its textured quad, soiced_tiny_skia-only apps won't compile against this crate. - Only one
ServoRuntimeper process. Servo's startup options live in a process-wide singleton; constructing a second runtime panics atset_opts. Build one runtime at app startup and pass it into every controller.
Architecture
ServoRuntime — one per process: owns Servo + SoftwareRenderingContext
│
├── ServoWebViewController (tab 1) — one WebView, frame slot, delegate
├── ServoWebViewController (tab 2) — …
└── ServoWebViewController (tab 3) — …
Each controller owns one servo::WebView. All controllers share the same runtime's rendering context, so only the active tab should be tick()-pumped on a given frame; calling activate() / deactivate() when switching tabs shows/hides the underlying webview so Servo's paint goes to the right pixels.
When page content calls window.open or clicks a target="_blank" link, Servo fires request_create_new. The crate satisfies the request with a throwaway popup whose only job is to capture the popup's first navigation URL and hand it to the embedder via the on_new_webview_requested callback. The app decides what to do with it — the basic example opens a new browser tab.
Usage
use ;
// Once per app:
let runtime = new?;
// Per tab / view:
let controller = new?;
controller.activate;
controller.on_new_webview_requested;
// In view():
frame.width.height
// In update(), drive the active tab's controller on every tick:
controller.tick;
See examples/browser.rs for a full tabbed browser with URL bar, back/forward/reload buttons, and per-tab session history. examples/basic.rs is the minimal "load a page in a window" version.
Host ↔ page interaction
Servo 0.1 exposes asynchronous JavaScript evaluation through the WebView API; iced_servo surfaces it both as a callback and as an iced::Task so it composes naturally with update():
use ;
// Returns a Task<Result<JSValue, JavaScriptEvaluationError>>.
ReadTitle => controller
.evaluate_javascript_task
.map,
TitleLoaded =>
JSValue is a structured enum (Number, String, Array, Object, plus opaque DOM-handle variants), so nested data round-trips losslessly without a JSON intermediate. There is currently no Rust-side DOM AST — every host→page interaction is "compose a JS expression, eval, parse the returned JSValue". Use Servo's UserContentManager to inject document-start scripts if you need page→host messaging.
Build prerequisites
Servo is a heavy dependency. The first build downloads hundreds of crates and several GB; expect 30+ minutes. Each platform needs Servo's system toolchain prerequisites — see the official setup guide:
https://book.servo.org/hacking/building-servo.html
Summary for each platform:
Windows:
- Visual Studio 2022 with Win10/11 SDK ≥ 10.0.19041, MSVC v143 build tools, C++ ATL
- LLVM/Clang 19+ on PATH with
LIBCLANG_PATHpointing at thebindir containinglibclang.dll - MozillaBuild (needed by
mozjs_sysfor SpiderMonkey) - Python 3
macOS:
- Xcode Command Line Tools, Homebrew packages listed in the Servo book
Linux:
- See the Servo book for distro-specific package lists (build-essential, cmake, libssl-dev, etc.)
Optional: enable the no-wgl feature on Windows to use ANGLE (D3D11) instead of WGL — matches what servoshell ships with:
= { = "0.1", = ["no-wgl"] }
Examples
The smallest possible integration: one runtime, one controller, one frame widget. Loads https://servo.org and renders it. ~70 lines total.
A real tabbed browser built on the same crate: URL bar, back/forward/reload, close/new-tab, and target="_blank" links that open new tabs via on_new_webview_requested.
Performance notes
SoftwareRenderingContext renders on Microsoft's WARP software rasterizer on Windows and Mesa/EGL elsewhere; pages are laid out by Servo but every frame goes through CPU rasterization and a GPU→CPU→GPU round-trip per paint. Heavy, JS-driven sites (Google Search, Gmail) are noticeably slower than a GPU-backed browser. The OffscreenRenderingContext path (when available in Servo's public API) is the future upgrade target, with zero pixels crossing the CPU.