|
|
Donner SVG Editor & Engine
SVG-native editor and embeddable SVG2 + CSS3 engine in C++20, with GPU (WebGPU) and compact CPU renderers, built for correctness, security, and performance.
|
The Donner editor uses a neutral Graphite visual system so SVG artwork remains the most colorful object on screen. Signal Teal identifies focus, selection, and primary interactive state. Amber, coral, and green are reserved for semantic status.
The implementation lives in donner/editor/EditorTheme.{h,cc}. The active theme configures ImGui and supplies tokens to custom ImDrawList chrome, the canvas overlay, and source-editor colors.
| Role | Token | Value |
|---|---|---|
| Canvas surround | surfaceCanvas | #111215 |
| Inset well | surfaceSunken | #151619 |
| Panel background | surfaceBase | #1B1D20 |
| Menu, tab, field | surfaceRaised | #24272B |
| Floating chrome | surfaceOverlay | #2B2F34 |
| Hover | surfaceHover | #343940 |
| Active | surfaceActive | #3C424A |
| Subtle rule | borderSubtle | #30343A |
| Strong rule | borderStrong | #464C55 |
| Primary text | textPrimary | #F1F2F4 |
| Secondary text | textMuted | #A8ADB5 |
| Disabled text | textDisabled | #656B74 |
| Focus and selection | accentDefault | #31C6B3 |
| Warning | warning | #E3B341 |
| Error or destructive | destructive | #F0616A |
| Success | success | #3FB984 |
The shipped accent is Accent::SignalTeal. Other accent variants remain test-covered theme inputs, but product chrome should not choose a different accent per widget.
Spacing uses 4, 8, 12, 16, 24, and 32 px tokens. Controls use a 4 px radius; floating containers use a 6 px radius. Tool buttons are 32 px square on desktop and 44 px square in the compact touch profile. New fixed-format controls should use stable dimensions and reserve enough room for their longest label.
Roboto is the UI face and Fira Code is the source face. UI text uses regular weight for values and bold weight for identity or compact section emphasis. Large display type does not belong in panels, toolbars, dialogs, or diagnostics surfaces.
The top application bar uses a teal identity mark, DONNER, a muted SVG EDITOR descriptor, and compact menus. Dock tabs use neutral surfaces and a teal selected overline. They do not use ImGui's stock blue tab fill.
The canvas toolbar is a floating overlay with one subtle shadow and one strong edge. Tool buttons use Donner-rendered two-tone SVG artwork with a black core and white halo, a translucent teal selected fill, and a teal selected outline. Toolbar art rasterizes at 80 px before display at 20 logical px so 1x and 2x output use exact downsampling ratios. The Fill/Stroke widget shares the same container and token set. Only ready tools appear in the toolbar; unfinished interactions do not occupy disabled slots. Acute pointer artwork uses concentric silhouettes so the black core cannot leak through the white halo during downsampling.
The Inspector contains document and selection controls only. Viewport implementation telemetry does not occupy the Inspector. Zoom state lives on the canvas in a compact control that resets to 100 percent when clicked.
ComputeEditorAdaptiveUiLayout selects a canvas-first compact profile for constrained windows and touch-preferred builds. Native windows enter it below 760 px wide or 520 px high. WebAssembly enters it at those constraints or when the browser reports touch points or a coarse pointer, so desktop browsers retain the full editor while iOS starts with stable touch geometry.
The compact profile keeps the same Graphite language but changes the command hierarchy:
Opening a right-side sheet recenters the tool palette within the unobscured canvas region. Compact and desktop modes use separate DockSpace roots, so resizing into the touch profile cannot rewrite a custom desktop panel arrangement. Source visibility and desktop sidebar width remain preferences, not transient compact state.
The UI profile consumes ordinary ImGui pointer and resize events. The WebAssembly bootstrap maps one primary touch pointer into the existing mouse-compatible ImGui stream, with pointer capture and cancel handling so taps and direct drags use the desktop mutation paths. Viewport sizing, multi-touch gestures, virtual-keyboard behavior, and Safari lifecycle integration remain platform responsibilities; they must continue to feed existing editor seams rather than add touch-specific document mutation paths.
New text keeps the current authoring fill instead of resetting to a tool-local color. When one text element is selected or edited, font family, size, and emphasis controls appear in a compact rounded toolbar centered below the canvas tool palette. It floats over the canvas and does not consume a full-width layout strip.
Point text uses font em-box extents for frame height so ascenders, descenders, and the typed glyph mix cannot make the frame jump. A newly placed point-text frame starts hidden. Pointer movement reveals it immediately; subsequent text input fades the frame and handles away while leaving the caret visible. Drag-created box text keeps its authored frame visible.
Text frame handles use the same handle-box calculation as select-tool handles at every display scale. During frame resize, pointer moves update only local frame chrome. DOM attributes, text rewrap, source writeback, and document rendering occur once on release.
The source pane starts collapsed so the document canvas is the primary workspace. A persistent 32 px rail remains on the left edge with a source glyph and line motif. Clicking the rail opens the source pane; the View menu and source-navigation actions also reveal it. The rail reserves layout space rather than overlaying document content, and its hover state uses the standard focus color.
The Transform section uses stable rows for Position, Size, and Rotation. A five-column grid gives X/W, Y/H, and Rotation fixed field starts and matching widths; variable label glyphs cannot shift the input rectangles. Rotation includes its unit, and the advanced matrix remains behind a disclosure with the same fixed axis/value alignment. Fields fill the available Inspector width without changing row geometry during hover or editing.
Numeric drag fields support both interaction styles. Dragging adjusts a value at fine resolution; a click-release without movement enters text input directly, without requiring a modifier or double-click. Enter commits typed input and field deactivation records one undo step.
Transform activation must not hold a document read scope while reading transform() or worldBounds(). Those SVG accessors may materialize lazy layout state and acquire write access in concurrent-DOM mode. Each accessor owns its required lock so field activation cannot attempt a non-recursive read-to-write lock upgrade.
The Inspector presents XML attributes and computed CSS as compact property lists:
These lists are read-only in the visual MVP. Attribute editing must remain DOM-first and use the editor's structured source-writeback path when it is added.
Interactive chrome must stay on the UI thread while expensive document work is coalesced, deferred, or moved to an isolated worker:
Source reveal and annotation application should each remain within a 16.7 ms UI-frame budget on the checked-in showcase document. Steady source-pane rendering should remain below that budget as well.
TextEditor::getDarkPalette() follows the same Graphite base while using semantic hues for syntax:
Selection remains Signal Teal at 50 percent alpha so focus-reference connectors and selected source ranges remain legible.
Launching without a filename, including the WebAssembly build, opens a first-run surface over the real editor workspace. It keeps Donner as the first visual signal, offers Open SVG and a fixed GitHub destination, and lists a bounded offline catalog of reviewed SVG samples. Loading a sample creates an untitled document, so Save cannot overwrite a local file without an explicit path. Each sample card uses a small bitmap rendered by Donner from the exact bundled SVG source. ImGui only blits the cached texture; it does not approximate sample artwork with hand-drawn UI geometry. The catalog has a dedicated texture cache so Layers-panel retention cannot evict welcome previews. The shell renders at most one missing preview per UI frame and keeps a fixed placeholder slot while the bounded catalog fills in, avoiding a first-frame Wasm stall or card-layout shift.
The welcome surface owns the workspace while visible rather than competing with docked Layers and Inspector panels. Its sample controls use at least 44 logical pixels of height, lay out in one column below the compact breakpoint, and use at most three columns on wider displays. Cards reserve a fixed preview slot while keeping their complete surface clickable as a touch target. The surface can be reopened through File > Open Sample and dismissed to reveal the current document.
Document replacement requested from the sample surface is deferred to the next orchestration frame. The shell waits for the renderer to become idle and detaches any prior direct-presentation callback before releasing old WebGPU resources. This ordering prevents the prior frame callback from retaining presentation handles across document replacement. If the document or source buffer has unsaved edits, replacement stops at an explicit Discard and Load confirmation; Cancel leaves both the current document and pending source text untouched.
Theme, menu, and source-palette contracts run in:
The Inspector UI fuzzer exercises real ImGui frames, Transform lifecycle transitions, selection, reload, undo, lock state, and busy snapshots under concurrent-DOM access:
For mutation-based local runs, invoke the built fuzzer with a writable corpus directory. The Bazel wrapper's checked-in corpus arguments are also useful as deterministic seed replays.
Full-shell visual verification uses the Geode replay harness with a worker-settled frame:
The visual pass checks app-bar identity, tab colors, the collapsed source rail, source contrast, toolbar fit and icon contrast, canvas framing, zoom-control placement, Transform alignment, panel density, and absence of debug-only Inspector content. Raster tests also require every custom cursor and toolbar icon to retain visible opaque black and white pixels without touching the bitmap edge. Text-tool tests additionally require no DOM mutation during resize moves, stable point-text em-box height, frame reveal/fade transitions, and high-density handle-size parity. The Inspector UI fuzzer includes text creation, active fill, typing, frame reveal, resize, release, reload, and undo.
The sample catalog is compiled into the editor and never fetches document content. Sample loading uses the same parser and full document-replacement path as a local open, produces an untitled document, and waits for renderer ownership to return before releasing presentation resources. The only external destination is the fixed Donner repository URL: browser builds open it after an explicit click with noopener, while native builds copy the same fixed URL only after an explicit click. Neither path derives a destination from SVG content.
SVG icon assets continue through Donner's renderer. Public captures and documentation use public sample documents and exclude private paths, host data, credentials, and operator content.