11. The webview
The page follows the SoftForge page contract of Project Board (docs/webview.md there): native ES modules, custom elements, no framework, no bundler, no stylesheet, styles set one property per line…
The page follows the SoftForge page contract of Project Board (docs/webview.md there): native ES modules,
custom elements, no framework, no bundler, no stylesheet, styles set one property per line in JavaScript,
themeColor(token) and themeFont(role), codicons, emit() events upward, syncChildren for lists,
accessibility rules, checked by npm run check:webview (UI-2, UI-3). This section lists what UI Designer adds.
11.1 Folders#
| Folder | Holds |
|---|---|
media/components/ |
App.js (root, x-app), the editor shell, panels (AppLayersPanel.js, AppDesignPanel.js, AppPrototypePanel.js, AppInspectPanel.js, AppAssetsPanel.js, …), dialogs that need the model |
media/views/ |
reusable elements that know nothing about documents: fields with expressions and scrubbing (UI-5), colour picker, menus, popovers, tabs, toasts |
media/models/ |
AppModel.js: the replica, the selection, the route, runCommand, request |
media/tools/ |
one class per canvas tool (MoveTool.js, FrameTool.js, PenTool.js, TextTool.js, …): pointer and keyboard state machines that produce previews and, on commit, one runCommand |
media/utils/ |
one function per file (emit, themeColor, themeFont, syncChildren, createIcon, …) |
media/workers/ |
render.worker.js: the render worker’s entry (ADR-10). It imports media/lib/* like the page, holds its own replica, owns the scene OffscreenCanvas and runs text shaping, layout and tile rasterisation off the main thread |
media/resources/icons/ |
codicon SVGs and the tool and layer-type icons |
media/lib/ |
generated: src/core, src/text, src/layout, src/render, src/commands compiled to ESM (§15.1). Not checked by the page contract, never edited. |
media/wasm/, media/fonts/ |
harfbuzz.wasm, icu_segmenter.wasm; the bundled fonts |
11.2 The canvas element#
AppCanvasView (x-app-canvas-view) holds two stacked <canvas> elements: the scene and the overlay
(selection, handles, smart guides and measurements (CAN-4), auto-layout handles (LAY-7), prototype noodles
(PRO-8), comment pins, agent flashes (UI-7)). The scene canvas is transferred to the render worker with
transferControlToOffscreen() (ADR-10); the worker runs Canvas2DRenderer and the tile cache (§7.3), and the
text and layout engines that feed them, so shaping a long paragraph or relaying out a large auto-layout tree
never blocks pointer events or panel input. The main thread keeps the overlay, the viewport (pan, zoom, device
pixel ratio), the tools and hit testing: it posts viewport changes and preview patches to the worker and
receives the derived geometry it needs for handles and measurements (a per-node bounds table, updated with each
layout). Text editing (TXT-7) uses a hidden contenteditable element positioned over the caret for IME
composition and clipboard, while the glyphs are drawn by the renderer; caret stops come from the worker’s
TextLayout. Where OffscreenCanvas is unavailable (the dev page in an old browser) the same code runs on the
main thread behind one interface, which is also how the layout identity tests drive it.
Viewport, zoom, active page, active tool and panel state are written with vscode.setState() on every change
(debounced), so a tab that was hidden and shown again, or restored after a window reload, comes back where it
was although the webview itself is rebuilt (§10.1).
The canvas is the one place where pixels are drawn imperatively; everything around it is custom elements.
Because a canvas has no accessibility tree, every node is reachable through the Layers panel, which is a real
tree (role="tree") synchronised with the canvas selection (UI-4).
11.3 Gestures and previews#
A gesture (drag, resize, rotate, pen, gradient handle) runs locally on the replica as a preview: the tool
computes the new properties, AppModel.preview(nodeIds, patch) applies them to a temporary overlay of the
replica (relayout included, so auto layout reflows live), and the affected tiles are redrawn. Nothing is sent to
the host and nothing is recorded. On pointer-up the tool calls runCommand once with the final parameters
(CMD-18) and the preview overlay is dropped when the commit arrives. Escape drops the preview.
11.4 The replica and prediction#
AppModel holds a replica of the document (media/lib/core) built from the init snapshot and kept current
by events messages (the same fold as on the host); the render worker holds a second replica fed with the same
init and events (forwarded by AppModel, snapshot transferred, not copied), so each thread folds the same
deltas and neither waits for the other. To make commits feel instant, runCommand predicts:
it runs the command on a draft of the replica with the request’s seed (§3.6) and shows the draft immediately.
When the host’s ack arrives:
- with events: the draft is dropped and the host’s events are folded (normally identical, so nothing moves);
- refused: the draft is dropped and
Appshows the error as a toast.
Commands flagged hostOnly (export, import, file and clipboard access) are not predicted. Licence gating is
the host’s decision; a locked page disables editing controls but predictions are never the authority.
11.5 Message protocol#
| Direction | Message | Payload |
|---|---|---|
| host → page | init |
{ snapshot, seq, hash, fonts (index), settings, licence, actor, selection, info (status, warnings) } |
| host → page | events |
{ seq, hash, events, group { key, label, actor, source }, requestId? } — every commit, from any source; hash is the state hash after the fold (§4.1), which the page shows in the history panel and the webview’s “Copy version” uses for --if-version |
| host → page | ack |
{ requestId, ok, result?, error? { code, message, hint } } |
| host → page | selection |
{ ids, by } — set by an agent (CMD-26) |
| host → page | flash |
{ ids, actor, color } (UI-7) |
| host → page | licence, settings, theme |
changes |
| host → page | flush |
commit unsent text now (before save) |
| host → page | response / progress |
answers to request |
| page → host | ready |
the page loaded |
| page → host | run |
{ requestId, id, params, seed } |
| page → host | transaction |
{ requestId, steps [{ id, params }], label, seed } |
| page → host | selection |
{ ids } — so ud selection sees it |
| page → host | request |
{ requestId, name, payload } — reaches outside the document: font.get, asset.url, export.save, clipboard.*, dialog.*, ai.*, license.*, openExternal, openTextEditor |
Images are not sent as messages: the assets folder is in localResourceRoots, asset.url returns a
webview.asWebviewUri URL, and the page decodes it with createImageBitmap. Fonts are sent as ArrayBuffer
once per face.
11.6 Page contract rules specific to UI Designer#
- No document mutation outside
AppModel.runCommandandAppModel.preview. The checker rejects calls tomedia/lib/coremutators (fold,Draft.set, …) anywhere else (CMD-18). - Canvas tools live in
media/tools/, are plain classes (not elements), one per file, and talk to the page only through the tool interface (down,move,up,key,cancel,drawOverlay). - Document colours (fills, strokes, guides chosen by the user) are data and may be literal; chrome colours come
from
themeColoronly. The canvas background is a document property (UI-2). - Every toolbar button, menu item and panel action names its registry command id; tooltips and accessible
names come from the command’s
titleandsummary(CMD-21).
11.7 The development server#
npm run dev (dev/server.ts) serves media/ to a browser against a real DesignController with an in-memory
document backed by --file, like the siblings’ servers (PLT-11). It also starts a bridge entry, so ud works
against the page in the browser. Test endpoints: POST /save, POST /undo, POST /external (simulates a
change on disk), GET /text, GET /screenshot?node=… (canvas pixels, for the golden suite), POST /bench
(§12).