# CLAUDE.md -- Rust Project Conventions
## Philosophy
Functional, type-driven, domain-driven.
## Architecture
- Modules by domain context.
- One module per concern (error, pipeline, script, frame).
- Thin lib.rs that exposes `render(html, css, viewport)` and
`run_script(html, css, js, viewport)`.
## Types
- Newtypes for `RenderInput`, `ScriptInput`, `Frame`.
- Sum types for error variants.
- No public struct fields.
- `#[must_use]` on getters and constructors.
## Error Handling
- Single project-wide `Error` enum.
- Display + std::error::Error impls by hand; no thiserror, no anyhow.
- Never panic.
## Style
- Prefer match over if/else, except on bool.
- No `return` keyword.
- No `mut`.
- Combinators over loops.
- Never match on Option<_>; use combinators.
- No unwrap()/expect() anywhere.
- No loop or for.
- No scan.
- No Rc/Arc.
- No naked `as` casts.
- Exhaustive matches; no `_` wildcard arm on enums.
## Traits
- No dyn Trait.
- Implement standard traits over ad-hoc methods.
## Linting
```toml
[lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
needless_pass_by_value = "warn"
manual_map = "warn"
```
## Verification
- Always run `RUSTFLAGS="-D warnings" cargo clippy --all-targets`.
- Always run `cargo fmt`.
## Testing
- Tests return `Result<(), Error>` and propagate failures with `?`.
- No assert!, assert_eq!, panic!, unreachable!, unwrap, expect.
## Dependencies
- The full cat-stack: `html-cat`, `css-cat`, `dom-cat`, `layout-cat`,
`paint-cat`, `net-cat`, `boa-cat`, `ecma-runtime-cat`, `web-api-cat`,
`ecma-lex-cat`, `ecma-parse-cat`.
- `proptest` dev-dep.
- No path dependencies.
## Documentation
- `///` doc comments on every public item.
- `# Examples` with runnable code blocks.
## Layer context
This is the **meta-crate** at the top of the cat-stack:
1. `ecma-syntax-cat`, `ecma-lex-cat`, `ecma-parse-cat` -- JS AST/parse.
2. `boa-cat`, `ecma-runtime-cat` -- JS engine + built-ins.
3. `html-cat`, `css-cat` -- web-spec parsers.
4. `dom-cat`, `layout-cat`, `paint-cat` -- rendering pipeline.
5. `net-cat` -- HTTP transport.
6. `web-api-cat` -- JS<->DOM/fetch bridge.
7. **`tauri-runtime-servocat`** -- meta-crate wiring all of the above
into a single pipeline targeting Tauri integration.
## v2.3.0 scope (current)
Headless pipeline + script driver + tiny-skia rasterizer + cosmic-text
glyph raster + winit/softbuffer window + IPC bridge + DOM back-prop +
real winit-backed `tauri_runtime::Runtime` window lifecycle + webview
navigation + script eval + per-window softbuffer presentation +
state-query round-trip from worker threads + `eval_script_with_callback`
result delivery + monitor enumeration + `run_on_main_thread` +
percent-decoded `data:` / `file://` / `http://` URL loading + Rust- and
JS-side `ipc_handler` bridge + cursor / size / drag / theme / icon /
user-attention / center plumbing + per-window `WindowFlags`
bookkeeping + tracked webview bounds + per-webview zoom / background
color / auto-resize / in-memory cookie jar + PNG snapshot
(`WebviewDispatch::print`) + main-thread `with_webview` callback +
stable `WebviewId` so `reparent(new_window_id)` follows the dispatcher +
visual `background_color` in the softbuffer compositor + `Cookie:`
header attached to net-cat HTTP `navigate` requests + visual `zoom`
applied via bilinear sampling in the compositor + `Set-Cookie`
response headers parsed back into the webview's jar so the cookie
loop is bidirectional.
- `render(html, css, viewport)` -- parses HTML+CSS, builds the DOM, runs
cascade + block layout, emits the paint-cat `DisplayList`. Returns a
[`Frame`] carrying the document, layout tree, and display list.
- `run_script(html, css, js, viewport)` -- as above, plus an inline JS
step driven by `boa-cat` + `ecma-runtime-cat` + `web-api-cat`. Script
value lands on the returned `Frame`; layout is the pre-script one.
- `run_script_with_commands(.., &HostCommands)` -- same but installs
the registry as the `__TAURI__` global.
- `run_script_with_backprop(.., &HostCommands)` -- additionally walks
the post-script JS-side DOM via `web_api_cat::extract_document` and
re-runs layout + paint so the returned `Frame`'s display list
reflects scripted mutations.
- `HostCommands` -- registry of `NativeFn` host commands callable from
JS via `__TAURI__.invoke('cmd', ...args)` (dispatcher) or
`__TAURI__.cmd(...args)` (direct method). Function-pointer-only
(no captured state); host state threads through the boa-cat heap.
- `render_to_pixels(frame, w, h)` / `render_to_pixels_with(.., &mut TextRenderer)`
-- tiny-skia rasterization with cosmic-text + swash glyph rendering
for `FillText`.
- `run_window(frame, viewport)` -- opens a winit window and presents
the rasterized frame via softbuffer; composites premultiplied RGBA
over white before writing to softbuffer's 0RGB word format.
- `cargo run --bin demo` -- v0.4 demo (windowed render).
- `cargo run --bin demo_ipc` -- v0.5 demo (registers a host command,
invokes from JS, mutates the DOM, back-props, shows the updated
render in a window).
- `ServocatRuntime` / `ServocatHandle` / `ServocatWindowDispatch` /
`ServocatWebviewDispatch` / `ServocatEventLoopProxy` /
`ServocatWindowBuilder` -- implementations of the
`tauri_runtime::Runtime`, `RuntimeHandle`, `WindowDispatch`,
`WebviewDispatch`, `EventLoopProxy`, and `WindowBuilder` traits.
- v1.1 wires the window lifecycle through a real winit event loop:
`Runtime::new` builds an `EventLoop<RuntimeEvent<T>>`; window
lifecycle / mutation methods (`set_title`, `set_size`,
`set_position`, `show`, `hide`, `close`, `destroy`, `maximize`,
`unmaximize`, `minimize`, `unminimize`, `set_resizable`,
`set_decorations`, `set_fullscreen`, `set_focus`) on
`WindowDispatch` / `RuntimeHandle` send `RuntimeEvent`s through the
winit `EventLoopProxy`; `Runtime::run` drives an
`ApplicationHandler` that creates queued windows, dispatches
`WindowEvent`s back to Tauri's callback, and exits when the last
window closes. Webview lifecycle and most getters still return
errors / zero values.
- `cargo run --bin demo_tauri_runtime` -- v1.1 demo (constructs the
Runtime on the main thread, queues a window via `create_window`,
renames it from a worker thread via `set_title`, runs the loop
until the window closes).
- v1.2 wires the webview lifecycle: `Runtime::create_webview` /
`RuntimeHandle::create_webview` / `WindowDispatch::create_webview`
send a `CreateWebview` event carrying the URL; the handler parses
`data:text/html,...` URLs, runs `pipeline::render`, stores the
result as a per-window `WebviewState`, and triggers a redraw.
`WebviewDispatch::navigate` / `reload` / `eval_script` /
`eval_script_with_callback` send the matching events; eval is
routed through `run_script_with_backprop` so DOM mutations
back-propagate into layout before the next paint. On
`WindowEvent::RedrawRequested` the handler converts the current
frame to a softbuffer surface and presents it on the window.
- `cargo run --bin demo_tauri_webview` -- v1.2 demo (opens a window,
attaches a webview with `data:text/html,...` content, navigates to
a second page after 1.5s, then runs a `setAttribute` JS script that
back-propagates into the displayed frame).
- v1.3 wires state-query round-trip + eval result delivery: the
`RuntimeEvent` enum gains `QueryInnerSize` / `QueryOuterSize` /
`QueryInnerPosition` / `QueryOuterPosition` / `QueryScaleFactor` /
`QueryTitle` / `QueryIsVisible` / `QueryIsFocused` /
`QueryIsMaximized` / `QueryIsMinimized` / `QueryIsFullscreen` /
`QueryIsDecorated` / `QueryIsResizable` / `QueryTheme` variants,
each carrying an `mpsc::Sender<R>` reply channel; the matching
`WindowDispatch` getters call a blocking `query()` helper
(500 ms timeout) that returns the value the handler sends back.
`EvalScriptWithCallback` carries a `Box<dyn Fn(String) + Send>` so
the JS return value lands in the user's callback after eval
completes.
- `cargo run --bin demo_tauri_queries` -- v1.3 demo (queries
title/inner_size/scale_factor/is_focused/is_visible/is_maximized/
is_fullscreen from a worker thread, then runs
`eval_script_with_callback("1 + 41")` and prints the result `42`).
- v1.4 wires monitor enumeration + `run_on_main_thread` + URL scheme
expansion: new `QueryPrimaryMonitor` / `QueryAvailableMonitors` /
`QueryMonitorFromPoint` / `QueryCurrentMonitor` /
`RunOnMainThread` variants on `RuntimeEvent`; the handler
enumerates monitors through `ActiveEventLoop` and forwards each as
a `tauri_runtime::monitor::Monitor` (with the full monitor rect
doubling as `work_area` since winit doesn't expose work-area
separately). `html_from_url` now handles percent-decoded
`data:text/html,...`, `file:///...` via `std::fs::read_to_string`,
and `http://...` via net-cat's blocking `fetch`. Cursor position
remains `FailedToGetCursorPosition` (winit 0.30 doesn't expose
desktop-relative cursor pos).
- `cargo run --bin demo_tauri_monitors` -- v1.4 demo (enumerates
monitors from a worker thread via `RuntimeHandle::primary_monitor`
and `available_monitors`, dispatches a closure back to the main
thread via `run_on_main_thread`, loads a page from a
percent-encoded `data:text/html,...` URL).
- v1.5 wires the Rust side of the `WebviewIpcHandler` bridge:
`RuntimeEvent::CreateWebview` now carries `Option<WebviewIpcHandler<T,
ServocatRuntime<T>>>`, which `WebviewState` stores; the new
`RuntimeEvent::IpcRequest` variant + inherent
`ServocatWebviewDispatch::send_ipc(request)` fire a stored handler.
When invoked, the handler receives the same `DetachedWebview`
shape it would in a real Tauri runtime, and can send a reply back
through `dispatcher.eval_script(...)` (which Tauri's command system
already does internally). `AppHandler` now retains a proxy clone
so it can rebuild dispatchers for handler invocations.
- `cargo run --bin demo_tauri_ipc_handler` -- v1.5 demo (registers a
`WebviewIpcHandler` on a webview that prints the request URI/body
and replies via `eval_script` running `console.log(...)`, then
fires it from a worker thread via `send_ipc(ipc://greet)`). Run
output confirms the full round-trip: worker -> proxy -> main
thread -> handler -> boa-cat eval -> console.log).
- v1.5.1 wires the JS side: every `EvalScript` /
`EvalScriptWithCallback` arm wraps the eval in `with_ipc_dispatch`
which stashes a type-erased `IpcDispatchImpl<T>` in a thread-local
for the duration of the eval. `WebviewState::eval` registers
`post_ipc_message_impl` as a `NativeFn` under `__TAURI__`, so a
JS call `__TAURI__.post_ipc_message(payload)` reads the
thread-local, builds an `http::Request<String>` with the
`ipc://post-message` URI, and fires a `RuntimeEvent::IpcRequest`
through the proxy -- the same handler invocation path
`send_ipc(request)` uses. The bare-fn-pointer constraint on
`NativeFn` is the only place we use thread-local state.
- `cargo run --bin demo_tauri_ipc_js` -- v1.5.1 demo (registers an
`ipc_handler` and runs `eval_script("__TAURI__.post_ipc_message(
'{...}')")` from a worker thread; the handler fires through the
thread-local proxy, prints the request body, and replies via
`console.log`).
- v1.6 wires the previously-no-op `WindowDispatch` methods that
map cleanly onto winit's `Window` API:
`set_cursor_visible` / `set_cursor_grab` / `set_cursor_icon` /
`set_cursor_position` / `set_ignore_cursor_events`,
`set_min_size` / `set_max_size`, `set_always_on_top`,
`set_theme` (window-level), `start_dragging`,
`request_user_attention`, `center`, and `set_icon`. Each adds a
matching `RuntimeEvent` variant + handler arm; the handler calls
the corresponding `winit::window::Window` method. `center`
computes the offset against `current_monitor().size()`.
`convert_cursor_icon` maps every Tauri `CursorIcon` variant to a
winit equivalent (collapsing `Default | Arrow` since both target
winit's `Default`).
- `cargo run --bin demo_tauri_window_methods` -- v1.6 demo (calls
each of the 10 newly-wired methods from a worker thread; the
window resizes to the min/max bounds, snaps to monitor center,
bounces the dock for `request_user_attention(Informational)`,
hides + grabs the cursor, and prints `Ok(())` for each call).
- v1.7 adds per-window `WindowFlags` bookkeeping
(`enabled`/`maximizable`/`minimizable`/`closable`/
`always_on_top`/`focusable`/`skip_taskbar`, defaults match
winit's; flags map cleared on window destroy) and routes
`set_enabled`/`set_maximizable`/`set_minimizable`/`set_closable`/
`set_focusable`/`set_skip_taskbar` through `SetEnabled`/
`SetMaximizable`/... events; `is_enabled`/`is_maximizable`/
`is_minimizable`/`is_closable`/`is_always_on_top` round-trip via
`QueryIs*` reply channels. Webview-side, `WebviewState` now
tracks `bounds: Rect` (initialized to the parent window's
viewport); `set_bounds`/`set_size`/`set_position` update it via
`SetWebviewBounds`/`SetWebviewSize`/`SetWebviewPosition` events,
and `bounds()`/`position()`/`size()` round-trip via the matching
query variants. `Position::Logical` and `Size::Logical` are
collapsed to physical at read time with `#[allow]`-gated
narrowing.
- `cargo run --bin demo_tauri_state_tracking` -- v1.7 demo (prints
defaults for the 5 flag getters + webview position/size, flips
every flag and the bounds, then re-reads them so the change
shows up).
- v1.8 adds per-webview tracking for the remaining
`WebviewDispatch` methods that don't need real webview hooks:
`zoom` (f64, default 1.0), `background_color` (`Option<Color>`,
default None), `auto_resize` (bool, default false), and an
in-memory `cookies: Vec<Cookie<'static>>` jar.
`set_zoom` / `set_background_color` / `set_auto_resize` /
`set_cookie` (replace-by-name) / `delete_cookie` (remove-by-name)
/ `clear_all_browsing_data` (clear the jar) update the state via
new events; `cookies()` and `cookies_for_url(url)` query it.
When auto-resize is on, `WinitWindowEvent::Resized` updates the
webview's tracked bounds to the new window inner size.
`with_webview` continues to return `Err(FailedToSendMessage)`
since we have no native webview handle worth boxing as `Any`.
- `cargo run --bin demo_tauri_webview_state` -- v1.8 demo (calls
every newly-wired method from a worker thread; adds two cookies,
reads them back, deletes one by name, then clears the jar).
- v1.9 implements `WebviewDispatch::print` and `with_webview`:
* `print()` sends `PrintWebview` through the proxy; the handler
renders the current frame through `render_to_pixels_with`,
copies the RGBA bytes into a `tiny_skia::Pixmap`, calls
`encode_png()`, and writes the result to
`$TMPDIR/tauri-runtime-servocat-print-{label}.png` (with a
safe-character filter on the label). Returns `Ok(())` on
success; the path is logged to stdout.
* `with_webview(f)` ships the user closure to the main thread
via `RuntimeEvent::RunOnMainThread { thunk }`, where it's
called with `Box::new(())` since the cat-stack runtime has no
native webview handle. Lets Tauri callers use `with_webview`
for host-side scheduling even when a platform handle isn't
available.
- `cargo run --bin demo_tauri_print` -- v1.9 demo (from a worker
thread, calls `with_webview` (closure logs on the main thread)
then `print()`; the runtime writes a 1280x720 PNG to the
temp directory and stdout prints the path).
- v1.10 adds a stable `WebviewId` separate from `WindowId`.
Webview-targeted `RuntimeEvent` variants carry `webview_id:
WebviewId` instead of `window_id`; the dispatcher stores
`webview_id`, and the handler maintains `webview_to_window`
(webview -> current parent) and `window_to_webview` (parent ->
attached webview) maps. `reparent(new_window_id)` updates both
maps and requests redraws on the old and new parents.
`WebviewDispatch::close` and `set_focus` route through new
`CloseWebviewParentWindow` / `FocusWebviewParentWindow` events
that resolve the current parent via `webview_to_window`.
Window destruction now evicts any attached webview from all
three maps. The remaining `dyn` sites
(`EvalScriptWithCallback`, `RunOnMainThread`, `with_webview`'s
`Box<dyn Any>`, `IpcDispatch`) are now documented with
explicit FFI carve-out comments explaining why static dispatch
isn't an option (concrete `winit::EventLoop<RuntimeEvent<T>>`
enum + bare-fn-pointer `NativeFn` + tauri trait shapes).
- `cargo run --bin demo_tauri_reparent` -- v1.10 demo (opens two
windows side-by-side, attaches a webview to the primary, then
from a worker thread calls `dispatcher.reparent(secondary.id)`;
the webview's state moves and the next paint lands in the
secondary window).
- v2.0 applies the v1.8 `background_color` state visually:
`present_webview` reads `webview.background_color`, defaults to
opaque white when unset, and the softbuffer compositor blends
premultiplied RGBA over the chosen colour (`out = src + ((1 -
src_a / 255) * bg)`) using full-precision `u32` intermediate.
`try_http_url` (used by `navigate(http://...)` and the initial
`CreateWebview` URL fetch) now accepts the webview's cookies and
attaches them as a single `Cookie: name=value; ...` header
built by `cookie_header_value`, filtering by host match.
- `cargo run --bin demo_tauri_bg_color` -- v2.0 demo (loads a page
with white text, then from a worker thread calls
`set_background_color(navy)` and `reload()`; the next paint
shows the white text on a navy background instead of the prior
white background).
- v2.1 applies the v1.8 `zoom` state visually in the softbuffer
compositor. For each destination pixel `(x, y)`, `write_pixels`
samples the source frame at `(x / zoom, y / zoom)` via
nearest-neighbour lookup; the bounds-check + `f64`-to-`u32` cast
are isolated in `sample_source_word` with the lossy cast
documented as an FFI-style carve-out. `zoom == 1` is the
identity mapping; `zoom > 1` magnifies the top-left `1/zoom` of
the source over the full window (zoom-in); `zoom < 1` lets the
source extend past `src_w/src_h` and the uncovered area falls
back to the tracked `background_color` (zoom-out). Pipeline-side
layout-with-zoom is deferred -- this is bitmap nearest-neighbour
scaling so re-rasterized text isn't yet crisp.
`SetWebviewZoom` now requests a redraw on the parent window so
the next paint reflects the new factor.
- `cargo run --bin demo_tauri_zoom` -- v2.1 demo (loads a page
with a red heading box, then from a worker thread calls
`set_zoom(2.0)`; the next paint shows the top-left half of the
source frame magnified 2x).
- v2.2 closes the cookie loop. `html_from_url` now returns
`(String, Vec<Cookie<'static>>)`. `try_http_url` reads
`response.headers().iter()`, filters for case-insensitive
`Set-Cookie`, parses each value with `Cookie::parse(value)`,
and `Cookie::into_owned`s the result. The `CreateWebview` and
`Navigate` handler arms call a new `merge_cookies(jar, new)`
helper that evicts any existing entry with the same name before
pushing, so the host's `Vec<Cookie>` reflects what the server
most recently set. Two inline unit tests
(`merge_replaces_existing_by_name`, `merge_appends_unrelated_names`)
lock in the replace-by-name semantics without needing a live
HTTP server.
- v2.3 upgrades the zoom compositor from nearest-neighbour to
bilinear sampling. `sample_source_word` now reads the four
source pixels surrounding the fractional sample point via a new
`sample_pixel` helper, computes weights
`((1 - x_frac), x_frac) x ((1 - y_frac), y_frac)`, and feeds
per-channel `blend_channel(channels, weights)` for a weighted
average. `zoom == 1.0` collapses to the floor-pixel lookup
(fractional parts are zero). Three inline unit tests
(`bilinear_corner_weight_selects_single_pixel`,
`bilinear_horizontal_midpoint_averages_two_pixels`,
`bilinear_centroid_averages_all_four`) lock in the formula.
Crisp glyph re-rasterization at zoom (where text would be
rasterized at the larger size instead of being bitmap-scaled)
still waits on a layout-cat font-scale parameter; see v3.0+.
## Roadmap (3.x+)
- v3.0+ -- pipeline-side scale parameter: extend
`pipeline::render` / `script::run_script_with_backprop` to
accept a `zoom` multiplier; layout-cat applies it to all CSS
length resolutions and paint-cat scales `FillText.font_size`.
Combined with v2.3's bilinear sampling that gives true crisp
text at zoom.
## Deferred to post-1.x
- Live re-render in the open window after IPC calls or user input
(currently the window renders the v0.5 back-propped frame once at
startup; mutations during the event loop don't trigger a redraw).
- Async / Promise-returning host commands (needs comp-cat-rs
scheduler integration).
- Multiple `__TAURI__` invocation flavours that don't rely on `this`
(e.g. a globally-bound `invoke` function).
The Servo no-AI policy disqualifies upstream contribution; this stack
is the AI-built parallel.