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 App shows 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#

  1. No document mutation outside AppModel.runCommand and AppModel.preview. The checker rejects calls to media/lib/core mutators (fold, Draft.set, …) anywhere else (CMD-18).
  2. 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).
  3. Document colours (fills, strokes, guides chosen by the user) are data and may be literal; chrome colours come from themeColor only. The canvas background is a document property (UI-2).
  4. Every toolbar button, menu item and panel action names its registry command id; tooltips and accessible names come from the command’s title and summary (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).