Files
designcraft/docs/egui-037-migration-plan.md
T

107 KiB
Raw Blame History

egui 0.37 migration plan for DesignCraft

Status update (2026-10-25): implementation is ready locally for review and includes main 7ca39c6c10f123d0cd0176eda1511e59667c89d0. Managed run egui037-design-main7c-ci-20261025-0817 passed all nine Linux CI stages: 3,344 test executions passed, 0 failed and 6 ignored across 81 summaries. Counts include repeated executions. The current-main integration received independent review. Dependency pins and lockfile are unchanged. Current-source Windows/packages, native IME and paired application performance remain open. Publication requires separate explicit approval; human review and human merge are required.

The original planning record below is historical.

Status: planning only. No production change, build, test, GUI run, browser run, benchmark, push, pull request or publication was performed while writing this document. Every future test or build runs through maintenance/task.py run/status/finish with its own task, source and evidence; never run a build directly. Every future push or new pull request needs fresh explicit human approval after the exact contents and validation are shown. The contributor credit preference remains username-only (@skylartaylor); do not add display or real names or any agent/model/tool attribution.

This plan covers only storytold/designcraft. It records the work needed to move the DesignCraft desktop, web and UI crates from the current coherent 0.36.2 dependency set to egui 0.37.0, the final reviewed suite fork and the matching final reviewed winit revision. Shared-library work remains in storytold/craft-libs; this plan consumes it but does not redefine it. Candidate aiongg revisions below are comparison inputs only. A production manifest must name the final reviewed storytold revisions, which do not exist until the fork/winit/craft-libs publication is reviewed and approved.

Capture acceptance follows the suite contract: a queued-not-issued hidden request and each settling frame must request RequestPaintWhileHidden from logic; only an issued screenshot command forces paint itself. Callback arrival has no implicit wake, so the live request owns the viewport waker. Record known issuance UI bounds and a geometry epoch, reject/retry any intervening resize/DPI/zoom/surface change even if it changes back, and derive pixel scale from returned dimensions only under an unchanged epoch. Default limits are 4 requests, 128 MiB/image and 256 MiB aggregate. Count reserved in-flight and completed images together.

1. Baseline and provenance

1.1 Exact source baseline

Item Value
DesignCraft checkout /home/codex/work/CraftApps/egui037-migration-20261022/designcraft
Baseline commit e74208dd7e0c801d15dfdf926d64cfe5e3b346d3 (Merge pull request #1642 from aiongg/ci-speed)
Workspace version / edition / MSRV 0.12.0 / edition 2024 / Rust 1.95
Lockfile Cargo.lock, format version 4
Current egui family egui, epaint, ecolor, emath, eframe, egui-wgpu, egui-winit, egui_kittest, epaint_default_fonts all 0.36.2 from crates.io
Current winit git+https://github.com/storytold/winit.git?rev=3d51077fd1a966f84df9ef287839dc84fd5b4ca7 (0.30.13, W1–W14)
Current craft-ui git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51...
Upstream egui 0.37.0 841a328ba2dd8f6996beb7189ebb7fc00e668a2a (release commit, not the annotated-tag object)
Candidate egui fork (comparison only) aiongg/egui craft/0.37 = a4f9a17f64a6bae61350fb85804c9df0d9865916 (E1–E21; E21b/P7b still to be added)
Candidate winit (comparison only) aiongg/winit craft/0.30.13-lang = 8b35388bd719e8cc86a95971ba0bc026c9a8f058 (W1–W27; W15P still to be added)
Final storytold egui pin Unknown until approved publication: a reviewed immutable storytold/egui revision containing the complete P8 handoff: E1–E21, E21b/P7b, E4/P2b, P7 key pairing and P7c fixes. Never ship the candidate hash.
Final storytold winit pin Unknown until approved publication: a reviewed immutable storytold/winit revision containing W1–W27 plus native W15P PRIMARY. Never ship the candidate hash.
Final craft-ui pin Unknown until approved publication: <FINAL_CRAFT_UI_REV>, the reviewed CL-2F revision (including CL-1, and CL-3 if it has landed) derived from current craft-libs, not a PR head. D1 uses the reviewed CL-1 + CL-2 provisional revision as a pilot; CL-2F follows the DesignCraft/GridCraft adapter reviews.
Published predecessor storytold/egui craft/0.36.2 = 767b1cad294120689a514bb4bd68c077e46679b9; published storytold/winit = ffae5198770d7730e718043f035a772b7dbc3b1b
craft-libs PR 199 head b68de8c5f4f1c060904d211a7b1320aa0f7bce78; audit tree ec8840af8c5fda695cf195cdd05e5ae078415c07. Both are stale trees, not bases: they omit crates/platform and revert current security/settings/docking/printer fixes. Apply only the API-port hunks from 8614e4a04fae516fba16c22b31ff1008cb6be10b (and relevant README changes from 1764bca) onto current craft-libs 49190884278f16c9a9f5823d7698a8ffb5deb062.
Related language work storytold/craft-libs#198, delivered through fork E5/E9/E12–E17/E19–E21, E21b and winit W17–W27 plus W15P

Authoritative lifecycle evidence for this plan is /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/lifecycle-source-audit.md. It was checked against upstream 841a328b and candidate a4f9a17f: both native EpiIntegration::update and web AppRunner::logic call app.logic then app.ui inside Context::run_ui, so App::logic can run once per layout pass in both hosts. After a normally completed run_ui, current_pass_index() is zero even after multiple passes. run_logic does not restore an old index. ViewportInfo::visible() is a method derived from minimized/ occluded; it is not a callback-phase signal, and None means unknown.

The IME gap is recorded in /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/ime-surrounding-gap.md. The candidate never calls W27 set_ime_surrounding_text, and IMEOutput has no logical text. E21b/P7b is a required new change, not existing capability.

The historical compiler diagnostics at /home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/designcraft.errors are clues only. They were produced against an earlier prepare state, their counts are not acceptance, and the current source may differ. They do show the first API breaks: missing Event::source, removed egui::UserData/Event::Screenshot, renamed roles in new_document.rs, font_menu.rs, panels/*, and FontData blob typing in theme.rs.

1.2 Packages, binaries, examples and feature targets

Target Kind Relevant manifest Notes
designcraft-ui-egui library crates/ui-egui/Cargo.toml The swappable L6 UI. Production depends on craft-ui, egui; dev depends on egui_kittest (wgpu, snapshot) and designcraft-fonts (testing). D1 adds an explicit egui/default_fonts dev feature (see §3.7). Build script only bakes contributor credits.
designcraft binary apps/designcraft/Cargo.toml eframe features default_fonts, wgpu, accesskit, wayland, x11, links. D1 adds clipboard to preserve Copy/Cut and Paste on 0.37 (0.36.2 forced it through eframe; see §3.5). Native-only rfd, arboard, winit; macOS muda, objc2*, fmv-macos-events; Windows winsafe.
designcraft-web wasm binary apps/designcraft-web/Cargo.toml eframe features default_fonts, wgpu, accesskit; rfd, wasm-bindgen, web-sys; trunk build. Browser clipboard/drop paths are web-specific.
designcraft-cli binary apps/designcraft-cli/Cargo.toml No egui/eframe/winit/rfd; headless and MCP.
ui_shot example crates/ui-egui/examples/ui_shot.rs egui_kittest::Harness with wgpu at 1440×900, 2×; drives control requests and saves whole-window PNGs. It steps one UI pass per harness.step() and is not native presentation evidence.
xtask workspace tool xtask/Cargo.toml cargo xtask ci, wasm, layers, assets, pdf-deps, test, stats, parity.

There are 22 workspace manifests (Cargo.toml plus apps/* and crates/*), no nested workspace, no vendor/ directory, no examples or benchmarks that use egui other than ui_shot, and no [[bench]], [[test]] or [[example]] entries in any manifest. The only workspace patch section is the root [patch.crates-io] in Cargo.toml.

Relevant platform cfgs:

Cfg Files What must remain true
target_arch = "wasm32" apps/designcraft-web/src/{main,web}.rs, crates/ui-egui/src/lib.rs (now_ms, recovery, render worker) Browser entry point and async file/drop/download paths build and run; native-only recovery, dropped-file paths and render worker stay out.
not(target_arch = "wasm32") crates/ui-egui/src/lib.rs, apps/designcraft/Cargo.toml Native recovery, arboard, rfd, control server, winit and the enabled clipboard path keep compiling.
unix and not(target_os = "macos") apps/designcraft/src/main.rs X11 event-loop builder XWayland selection and file drops still work; winit pin changes do not remove EventLoopBuilderExtX11.
windows apps/designcraft/{Cargo.toml,src/gpu.rs}, build.rs winsafe display-GPU query and Windows resource build remain; DX12 backend selection and WgpuError fallback are tested.
target_os = "macos" apps/designcraft/src/{main,native_menu,mac_window,apple_events}.rs, crates/fonts CoreText Native menu, Apple events, movable_by_window_background, IME marked-text discard and cancelable termination keep their winit W12/W14/W22/W26 behavior.
X11/Wayland egui-winit clipboard path, apps/designcraft/src/main.rs D1 enables the explicit clipboard feature; D3 adds E4/W15P native PRIMARY on the winit data device. One regular data device only; password exclusion and unchanged-selection policy preserved.
cfg(test) / cfg(all(test, unix)) crates/ui-egui/src/*, apps/designcraft/src/main.rs Test-only counters and atomic-writer tests do not leak into release; kittest strict names and missing-glyph policy are explicit.

1.3 Current CI and validation commands

All future commands below run through maintenance/task.py run with a unique --job-id, the owning --task, exact --source and durable --evidence; inspect with status and complete with finish after reviews.

  • cargo xtask ci: fmt --all -- --check, pdf-deps, clippy --workspace --all-targets -- -D warnings, nextest run --workspace --lib --bins --tests (or cargo test), doctests, assets, layers, wasm.
  • cargo xtask wasm: one cargo check --target wasm32-unknown-unknown --keep-going over L0–L5 plus designcraft-ui-egui.
  • cargo test -p designcraft-ui-egui --lib is the package-scoped UI row. It must run with the explicit egui/default_fonts dev feature and with theme::install_fonts before layout.
  • Clean-checkout dependency gate: after publication, a fresh clone runs cargo fetch --locked and cargo metadata --locked --format-version 1 against the final storytold revisions. A missing/unreachable revision or a machine-local path fails the gate.
  • CI workflow .github/workflows/ci.yml runs cargo xtask ci on Ubuntu 24.04 with pinned craft-fonts abb83316d96aa59c1cf64784289e378fe9fa5695, nextest, and the apt/winit build dependencies.
  • windows-arm64.yml packages and installs ARM64; freebsd.yml runs the workspace in a FreeBSD VM; release.yml uses the shared release workflow.
  • Manual UI evidence: cargo run --release -p designcraft -- --sample --control 7979, control requests, ui.screenshot, and cargo run -p designcraft-ui-egui --example ui_shot -- script.jsonl for offscreen whole-window screenshots.
  • Native/browser workflows are listed in §6.4; ui_shot alone is not native focus, IME, picker, clipboard or browser-presentation evidence.
  • There is no performance benchmark target today. ui.inspect exposes perf.frameMs, perf.renderMs, perf.fps, perf.patches; perf.frameMs uses now_ms() (SystemTime on native, 0.0 on wasm) and is not a monotonic acceptance timer. §7 defines the test-only raw-sample harness.

2. What 0.37 changes that actually affects DesignCraft

DesignCraft already uses eframe::App::logic/App::ui and a root Ui on 0.36.2. The 0.37 migration must preserve that structure; it is not a reason to rewrite panels into Context::root_ui calls or to move every action out of logic.

0.37 change Source anchor DesignCraft impact
Event::MouseWheel gains source: MouseWheelSource egui/src/data/input/event.rs, MouseWheelSource Every synthetic wheel event in tests must set source (Unknown for most; Wheel/Trackpad/Momentum where the behavior under test depends on it). Production matching already uses .., but the canvas's Alt-wheel state machine must be rechecked.
Event::Screenshot and egui::UserData are removed; ViewportCommand::Screenshot(ScreenshotCallback) egui/src/data/screenshot_callback.rs, viewport.rs; upstream #8489 ScreenshotCallback is one-shot and does not repaint. Replace token/UserData correlation with a per-request completion slot owned by the request record and a weak callback; see §3.4.
WidgetType is replaced by accesskit::Role (egui::Role); alias deprecated egui/src/lib.rs compatibility table; #8555 Map SelectableLabel → Button, TextEdit → TextInput, CheckBox → CheckBox, Other → Unknown. The full file list is in §3.7: new_document.rs, panels/font_menu.rs, panels/pages.rs, panels/mod.rs, panels/styles.rs, panels/cell_stroke.rs, panels/properties.rs, panels/swatches.rs, panels/layers.rs, object_control.rs, widgets.rs, toolbar.rs, chrome.rs, taskbar.rs, icons.rs, dialogs.rs.
Id::new deprecated for Id::unique; Ui::id renamed Ui::scope_id; UiBuilder::id renamed scope_id egui/src/id.rs, egui/src/ui.rs; #8534/#8542/#8586 Mechanical but warning-sensitive because clippy -D warnings treats deprecations as failures. Use Id::unique for root IDs and scope_id for child scopes. Do not change the hashed values: Id::new now delegates to Id::unique, so state keys remain stable.
Panel constructors take impl AsIdSalt; panel ID is parent_ui.scope_id().with_salt(salt); CentralPanel remains last; root Ui is the normal app surface egui/src/containers/panel.rs; #8592 Existing Panel::left("tools"), Panel::right("dock"), etc. still type-check. Panel IDs are now parent-salted. DesignCraft does not persist egui memory (eframe persistence is off); update in-memory test lookup keys and retain the actual app-owned UiState/docking preferences. No PanelState migration is needed. Panel order must not change. CentralPanel::default().show(ui, …) is correct; Context::root_ui must not be called inside App::ui.
default_fonts is no longer a default egui/epaint feature egui/Cargo.toml; #8583 eframe still enables it in the app, but cargo test -p designcraft-ui-egui does not build the app and egui_kittest disables egui defaults. Add egui/default_fonts explicitly to the ui crate's dev feature set and install DesignCraft fonts before layout; do not rely on workspace unification.
FontData.font is now a shared Blob (Arc<dyn AsRef<[u8]> + Send + Sync>), not Cow<[u8]> epaint/src/text/font_data.rs Production struct-update code in theme.rs still works; tests that compare data.as_ref() must use data.bytes() or data.font.as_ref().as_ref(). This caused the historical theme.rs:766/793 errors.
egui_kittest fails unnamed input widgets and panics on missing glyphs by default egui_kittest/src/accessibility.rs, builder.rs; #8589/#8601 The first green stage must satisfy strict names/glyph policy, not disable it globally. D1 adds names/policy for the existing test surface; D4 completes coverage. Deliberate tofu fixtures use HarnessBuilder::allow_missing_glyphs() with a named reason.
Native eframe can discover system fonts through egui_system_font_provider, and NativeOptions::system_font_fallback defaults true eframe/src/native/epi_integration.rs; #8512 DesignCraft already catalogs system fonts in designcraft-fonts with script-specific families and baseline tweaks. Set the option false and keep the feature off, or font order and line metrics change. Add the #8512 cold-start workload in §7.
Clipboard is no longer forced on by eframe; egui-winit clipboard is an explicit feature eframe-0.36.2/Cargo.toml target dep forces egui-winit/clipboard; eframe-0.37.0/Cargo.toml exposes clipboard = ["egui-winit/clipboard"] and depends on egui-winit with defaults off; egui-winit-0.37.0/src/clipboard.rs stores only in-process without arboard/smithay 0.36.2 wrote Copy/Cut to the OS clipboard. On 0.37 without the feature, ctx.copy_text is in-process only and RequestPaste cannot read the OS clipboard. D1 enables eframe/clipboard to preserve native Copy/Cut/Paste, with the control-channel guard. D3 adds E4/W15P PRIMARY on the same winit data-device path.
X11/Wayland PRIMARY selection is new in egui-winit, and the candidate's E4 path replaces arboard with winit's data device egui-winit/src/clipboard.rs, lib.rs; #8428; E4 Enabling clipboard makes PRIMARY reachable. The final fork must include E4 plus native W15P; test pointer- and keyboard-triggered ownership (including keyboard-only focus), serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfers on wlroots/KWin/Mutter, password exclusion and no second regular data device.
TextEdit selection behavior changed on click-away upstream #8763, unmerged PR #8764 The fork owns the selected fix. Source audit finds no DesignCraft control that reads an egui TextEdit selection in the same pass: formatting acts on the engine TextSel; dialog/search buttons read buffers. The regression test documents stock 0.37 click-away behavior and asserts the app's engine selection/target is unchanged; if a future control reads TextEdit state, carry #8764 or defer the command with a tested app change.
Text tessellation is reported ~60% slower for plain text upstream #8731 UI frame cost may rise even though layout is unchanged. The fix must keep color emoji correct; branch once per row, not clamp-only. Measure the fixed 120-label workload.
DX12 pipeline creation is reported ~4× slower from wrap_texel_coord upstream #8736 Windows startup can get ~40 ms slower. Measure renderer/shader construction separately from first frame; do not call shader compile time UI performance.
#8748 hidden-parent/deferred-child is closed and unmerged upstream #8748 Do not assume hidden-window child viewport updates are fixed by 0.37. The fork owns a tested solution; DesignCraft must open/update/close its immediate second viewport while the parent is hidden/occluded/overdue, with unknown visibility (visible() == None) as well as Some(false).
Context::root_ui, run_ui, run_pass, run_logic; root Ui cannot be recursively entered; both native and web App::logic run per run_ui pass egui/src/context.rs; eframe/src/native/epi_integration.rs; eframe/src/web/app_runner.rs; #8535; lifecycle audit Use the passed &mut Ui in DesignApp::ui; use run_ui for UI tests and run_logic only for side-effect tests. Recursive root_ui panics. Do not use visible() as a logic/UI phase signal; a forced hidden paint is still a UI pass.
Style defaults changed (roomier buttons, larger corner radius) #8617/#8445 Custom Tokens still drive most colors, but egui widget spacing/rounding inherited from Visuals can shift geometry. Visual baselines need an explained intended change, not a blind update.
E21b/P7b outgoing IME surrounding text is missing IMEOutput/egui-winit candidate source; ime-surrounding-gap.md Add the bounded transient payload, built-in TextEdit producer and W27 Wayland adapter; DesignCraft's custom canvas editor supplies a real model/preedit/UTF-8 adapter, not None.

3. Source audit and concrete migration design

3.1 Existing frame flow

The desktop host implements eframe::App in apps/designcraft/src/main.rs: App::logic calls startup.enter, take_notice, the frames == 2 presented check, frames += 1, startup error-log installation, macOS native menu/Apple-event polling, then DesignApp::logic; App::ui calls DesignApp::ui, then macOS IME/movable handling. App::raw_input_hook delegates to DesignApp::raw_input_hook. The web shell does the same in apps/designcraft-web/src/web.rs, with asynchronous drop-file reading and a graphics health check.

Source facts that constrain the design (verified against the pinned trees):

  1. Native EpiIntegration::update (eframe-0.37.0/src/native/epi_integration.rs:349-361) calls Context::run_ui(raw_input, |ui| { app.logic(ui.ctx(), …); app.ui(ui, …) }). Web AppRunner::logic (eframe-0.37.0/src/web/app_runner.rs:290-293) does the same. A multipass frame therefore calls host and app logic once per pass, not once per frame.
  2. Context::run_logic (egui-0.37.0/src/context.rs:1174-1202) only refreshes viewport/ window/focus input; events and time stay from the previous UI pass.
  3. run_dyn passes new_input.take() to each begin_pass; the second pass has no raw events. PointerState::button_clicked scans pointer_events, which are cleared and repopulated from that empty event list, so a button does not re-fire merely because a pass was discarded. Command-producing UI still needs a one-pass/undo test because a discarded first pass may act on pre-discard geometry; it does not need a global !will_discard() ban.
  4. After a completed run_ui, ContextImpl::end_pass takes viewport.output; the returned output owns the completed-pass count and current_pass_index() reads the now-zero viewport output. run_logic does not restore a stale index. The earlier comment alleging a leftover nonzero count was wrong.
  5. ViewportInfo has minimized/occluded fields and a visible() -> Option<bool> method; there is no public visible field. visible() == None is unknown, and eframe forces a real UI pass for RequestPaintWhileHidden/pending Screenshot even when hidden.
  6. update_logic_only stores the prepared raw input in pending_raw_input; the next update prepends it to new input. Normal pointer/key/text/IME events can therefore be replayed after hidden logic.

DesignApp::logic (crates/ui-egui/src/lib.rs:1102) currently does all of: egui_ctx capture, window_maximized, one-time CMS activation through color_applied, ctx.set_zoom_factor from ui.ui_scale, zoom_with_keyboard=false, font installation and the 250 ms system-font poll, theme::apply/restyle, FPS smoothing from i.time, recovery autosave from i.time, control drain, stale new-document preview cleanup, inbox/open-OS- documents, synthetic repaint requests, screenshot collect/issue, shortcut handling, window- command flush, native dropped-file handling and quit handling. DesignApp::ui (lib.rs:1472) does layout: app bar, control bar, status bar, Tools, dock, central canvas, second viewport, floating panels/story editor/dialogs/about/palette, resize borders, URLs and frame timing.

This is a 0.36.2-shaped flow, not a 0.37 regression. However, 0.37 makes the latent problems more important:

  1. Every per-frame side effect in logic runs again on a discarded second pass. That includes font/theme setup, recovery checks, FPS sampling, screenshot issue/collect and side effects that are not input-driven.
  2. While minimized/occluded, eframe normally runs Context::run_logic and no UI pass. All ctx.input except viewport/window/focus information is stale. A forced hidden paint is different: run_ui does run, with fresh prepared input, even though visible() may be false or None.
  3. Host App::logic counts passes in frames; on a multipass frame frames advances twice, so startup.presented() and startup error logging happen at the wrong pass.
  4. Input buffered by update_logic_only, and synthetic control events injected in raw_input_hook during a hidden tick, can replay into a later UI pass with a different focus or document state.

The correct migration is to make the frame stages explicit, not to merge logic into ui, not to add a generic Context bridge, and not to use visible() as a phase signal.

3.2 Frame-stage contract

Introduce an explicit, testable split inside DesignApp while keeping the public logic(&Context) and ui(&mut Ui) entry points that hosts and tests use.

// crates/ui-egui/src/lib.rs
pub fn logic(&mut self, ctx: &egui::Context) {
    // Every call (first pass, later discarded pass, or hidden run_logic) may poll consumed
    // queues and deadlines. It must never replay an already-consumed message.
    self.logic_tick(ctx);
    // current_pass_index is 0 on the first pass and on hidden run_logic, and >0 on later
    // passes of the same run_ui. It is not a freshness test and must not gate input reads.
    if ctx.current_pass_index() == 0 {
        self.logic_frame(ctx);
    }
}

pub fn ui(&mut self, ui: &mut egui::Ui) {
    let first_pass = ui.ctx().current_pass_index() == 0;
    self.layout(ui, first_pass);
}

The guard is deliberately narrow: it removes duplicate per-run_ui side effects, but a hidden run_logic tick still has index 0, so autosave, control commands, inbox results and deadlines continue. A command that arrives between passes is retained in its queue (or its reply is pending); the control server requests a repaint, and the next frame's first pass consumes it. Never put an unconditional return for current_pass_index() != 0 around queue draining, screenshot completion or background deadlines.

logic_tick (every call) owns work whose identity is a consumed queue item, revision or monotonic deadline:

  • drain_control; each request is taken once. Screenshot requests add a request record and do not issue from the queue.
  • drain_inbox and open_os_documents after fonts are ready; inbox items are consumed once.
  • handle_quit (fresh close_requested in i.viewport()), flush_window_commands.
  • Screenshot result draining, late-result rejection and deadline replies; take results out of the slot under a short lock before encoding/writing.
  • While a control gesture still has queued synthetic events, request a paint while hidden (§3.3). Never inject a synthetic event during a logic-only tick.
  • Filter buffered OS input after a hidden tick using the focus/document generation recorded at the tick (§3.2 input policy).

logic_frame (first pass only) owns genuine once-per-run_ui work:

  • egui_ctx capture; window_maximized update (lib.rs:1106).
  • One-time CMS activation through color_applied (lib.rs:1107-1112).
  • ctx.set_zoom_factor from ui.ui_scale and zoom_with_keyboard=false (lib.rs:1114-1119).
  • Font installation/reinstallation from theme::install_fonts, fonts_system readiness and the 250 ms request_repaint_after while the font scan runs (lib.rs:1121-1131).
  • theme::apply/restyle (lib.rs:1132-1135).
  • Recovery autosave (lib.rs:1136-1147), changed from ctx.input(i.time) to a monotonic Instant clock so it continues during hidden logic and never uses a stale frame time. Recovery is native-only (#[cfg(not(target_arch = "wasm32"))]); wasm uses its browser time source rather than Instant/now_ms().
  • FPS smoothing only on a real UI pass, using a monotonic Instant on native and the browser frame time on wasm; do not inject zero/duplicate samples from hidden logic.
  • new_document::end_stale_preview(self) (lib.rs:1157) and the synthetic/control ctx.request_repaint() while events remain (lib.rs:1160-1162).
  • Screenshot issue after the 120 ms/3-frame settling rule (lib.rs:1636-1648).
  • menus::shortcuts moves out of logic into the first-pass input_stage below.
  • Native dropped files move out of logic into pending_drops, captured in raw_input_hook; logic never reads stale ctx.input drops.
  • handle_quit stays in logic_tick because close_requested is fresh viewport data and a close request must not wait for a first pass.

layout calls input_stage(ui) only on the first pass, before the app bar:

  • menus::shortcuts(self, ui.ctx()) runs against live input;
  • native dropped files are taken from pending_drops (captured in raw_input_hook), not read from stale ctx.input in logic;
  • no other input event is read twice. canvas::show and text fields continue to use egui's normal per-widget input. Shortcut consumption happens before layout, preserving the existing order.

DesignApp::ui must never call ctx.root_ui(...): the root Ui is already borrowed by the host callback, and Context::root_ui panics in that situation (#8535). The passed &mut Ui is the one to use for Panel, CentralPanel, Area, and show_viewport_immediate.

Host guard: apps/designcraft/src/main.rs must not increment frames or run macOS menu/ Apple-event polling on every pass. Wrap the host body in the same first-pass condition (or dedupe with ctx.cumulative_frame_nr()), while always delegating to self.app.logic(ctx), which has its own narrow guard. frames then counts presented run_ui frames, preserving the frames == 2 startup.presented semantics. Add a host-level forced-multipass test through the actual App wrapper, not only DesignApp.

raw_input_hook remains the one place that sees the exact RawInput before a pass. It continues the current batching: one pointer event per host iteration, a key press/release pair together, with trailing Text retained for a later iteration (lib.rs:1456-1469). Add:

  • native raw.dropped_files capture into pending_drops and removal from the raw input, so a hidden run_logic cannot re-open the last drop;
  • a focus/document generation snapshot on each hidden logic tick;
  • a filter that drops buffered Text/Ime/Paste events when the focus target or active document changed since the hidden tick, and drops an incomplete pointer chord (press without release or release without press) across a hidden->visible transition. Complete pointer/key events are replayed in order; pointer hover is refreshed or cleared with PointerGone. Record the policy and counts in the test-only instrumentation.

On web, WebShell::raw_input_hook is called in both the visible and hidden AppRunner paths. It must capture dropped files and start the asynchronous bytes_async() read there, pushing the result into the existing inbox. WebShell::logic must not take ctx.input_mut for drops. This is a source correction: the current apps/designcraft-web/src/web.rs:351-370 reads drops in logic, while raw_input_hook (web.rs:374-379) does not.

Test-only instrumentation may record stage counts: #[cfg(test)] logic_tick_ticks, logic_frame_ticks, input_ticks, screenshot_callbacks, so forced-multipass and hidden tests can prove one side effect and one input stage per frame. The counters must not exist in release builds.

Context::run_ui is the test entry point for layout and Context::run_logic for side-effect-only tests. Do not initialize fonts inside run_logic in a UI test: no pass runs, so font installation/layout does not behave as in a visible frame. ui_shot and kittest harnesses call app.logic(ui.ctx()) and app.ui(ui) inside the UI closure as they do now, with the same pass-index guard. Tests must also drive the real host/web callback order, not a fictional once-per-frame native host.

3.3 Control input while hidden

The control channel is used for automation even when the window is minimized. Under 0.37, a hidden window normally runs only logic; synthetic pointer/key/text/IME events injected through raw_input_hook are not applied unless a UI pass runs. A ViewportCommand::RequestPaintWhileHidden holds for one frame and must be sent again for another (egui/src/viewport.rs:1239-1247); a plain request_repaint does not force a hidden paint (egui-winit/src/lib.rs:1922-1924).

Policy:

  • Do not use visible() as a phase signal. Use it only as a hint for whether to request a hidden paint. None (unknown visibility) is treated like hidden for this purpose.
  • raw_input_hook runs before both host paths, so deliberately allow its drain-based injection into pending_raw_input during a logic-only iteration. Eframe retains those events for the next full pass. Send RequestPaintWhileHidden while an admitted gesture has queued OR injected-but-unapplied events; acknowledge delivery only from known UI phase. Preserve current pointer/key batching, generation filtering and event order. Test hidden injection followed by forced paint: one action/reply, no dropped or replayed events. An empty synthetic queue alone does not mean pending input was already applied.
  • Pure engine commands (engine.execute) never request a hidden paint.
  • ui.screenshot already forces a paint through the Screenshot viewport command; do not add a second RequestPaintWhileHidden for it. Still send Focus as today.
  • ui.dialog.confirm and other methods whose result depends on layout follow the same pending-gesture rule.
  • The control reply may be ok before all events are applied (current behavior). Tests and documentation must assert the final command/undo/document result, not the number of events sent or a single paint request.

3.4 Screenshot callback migration

Current code (lib.rs:587, drain_control, issue_screenshots at 1636, collect_screenshots at 1652) stores (token, path, reply, deadline) in pending_shots, queues (token, at, frames) in queued_shots, sends ViewportCommand::Screenshot(UserData::new(token)), then scans Event::Screenshot for user_data.downcast_ref::<u64>(). All of that changes.

Shared contract: the suite selects one bounded, cancelable completion slot per admitted request. Eleven existing craft-ui creative consumers adopt its helper; DeckCraft, FigCraft and ArtCraft Native keep a small local contract-compatible adapter. DesignCraft uses craft-ui. The helper is staged as CL-2 (provisional) and frozen as CL-2F after DesignCraft and GridCraft adapters are reviewed.

DesignCraft request record:

  • Keep the existing token counter, path, reply channel, 8 s deadline and 120 ms/3-frame settling rule in DesignCraft. These are app-owned.
  • Each pending_shots record owns one craft_ui::capture::CaptureSlot (provisional API; final name/API after CL-2F). The slot holds the request id, target ViewportId, deadline, a weak wake handle and at most one Arc<ColorImage>.
  • issue_screenshots sends ctx.send_viewport_cmd(ViewportCommand::Screenshot(ScreenshotCallback::new(move |image| { if let Some(slot) = slot.upgrade() { slot.complete(image); // weak slot; no DesignApp/Context strong capture } }))). The callback captures only a Weak reference to the slot. It must not capture egui::Context strongly (no context self-retention cycle), must not encode PNGs, write files, run commands, hold a long lock or borrow mutable app state.
  • ScreenshotCallback is one-shot; clones share one callback and complete at most once.
  • Bound admission by both request count and aggregate pending image bytes before issuing a capture. Compute the allowed surface dimensions, reject an oversized/stale image on completion, release its reservation and image, and account for renderer/readback transient memory separately in peak RSS. A post-callback size check alone is not a bound.
  • Capture the target ViewportId; use request_repaint_of(viewport) from the live slot, not an implicit current/default viewport. Release the slot lock before waking.
  • drain_screenshot_results runs in logic_tick, matches the slot record, takes the image out under a short lock, then calls control::save_screenshot(self, image.as_ref(), path) on the UI thread. Late callbacks for timed-out/canceled/closed records release the image and send no reply/file.
  • Correlate only ViewportId::ROOT for ui.screenshot until the protocol grows an explicit viewport field; do not guess from the pointer.
  • Account for E2 surface fitting: the returned image may be smaller than the window. Derive x/y scale from the returned image dimensions and the recorded logical viewport rect/DPI; a resize/DPI generation mismatch retries within the deadline or returns an error. Never combine old pixels with current geometry.

Tests: two outstanding tokens/viewports resolve to their own paths; completion is one-shot; out-of-order completion; timeout plus late callback; cancel/close/shutdown; delayed readback; hidden-window wakeup; aggregate memory returns to zero after drain; a reused viewport/document generation does not bind an old capture to a new document; no file/reply from a canceled capture; ui_shot still renders whole-window PNGs.

3.5 Input, IME, clipboard and keyboard layouts

Event::MouseWheel constructors in canvas.rs, menus.rs, dialogs.rs, chrome.rs, new_document.rs, object_control.rs, panel_docking_tests.rs and lib.rs must add source. Use egui::MouseWheelSource::Unknown for synthetic wheel input unless a test is specifically about Trackpad/Momentum; the canvas's wheel_alt only depends on phase and modifiers, so behavior is preserved.

IME:

  • The canvas (canvas::ime_event, canvas::delete_surrounding) currently handles Preedit, Commit, DeleteSurrounding { before_chars, after_chars }. delete_surrounding loops text.delete up to 64 times (canvas.rs:2695-2701), and each text.delete is an ordinary undoable command (crates/engine/src/lib.rs:565-585), so the current path can create up to 64 undo entries.
  • E21 adds ImeEvent::DeleteSurroundingBytes { before_bytes, after_bytes }. Implement a single mandatory engine command, text.deleteSurroundingBytes { beforeBytes, afterBytes } (or an equivalent single edit), that deletes the exact UTF-8 ranges around the caret in one s.edit call, producing one undo entry. Do not make this optional and do not approximate bytes as characters/text.delete loops.
  • Validate both counts are bounded (default cap 4096 bytes each and no larger than the story/ selection), both ranges are valid UTF-8 boundaries via str::get, no range extends past the story, and no split grapheme is produced. Preserve track-changes behavior by marking deleted text and stepping the caret, as text.delete does. Test replacement, deletion- only batches, combining mark/flag/ZWJ, preedit/commit sequencing, tool_composing(), focus/serial changes and canceled compositions.
  • The web text agent keeps its existing DeleteSurrounding character behavior.
  • E21b/P7b: add the bounded outgoing surrounding-text payload to IMEOutput, the built-in TextEdit producer and the egui-winit W27 Wayland adapter. DesignCraft's custom canvas editor (canvas::ime_output, canvas.rs:2720-2753) supplies a real model/preedit/UTF-8 adapter: logical committed document text around the caret, preedit excluded, UTF-8 byte cursor/anchor, at most 4000 bytes, explicit Unavailable context (distinct from available-empty) when the selection cannot fit, and clearing on focus loss, no IME, reset and password/no-context. It must not pass display-bidi text or set the payload to None as a placeholder. Audit every IMEOutput constructor/destructure. Tests cover outgoing text after delete+commit and deletion-only batches, supplementary/combining/RTL text, preedit exclusion, long lines, field/window switching and the full E21b contract: owner/context generation even when unavailable, available-empty distinct, W27 committed clear boundary, latest-pass None replacing earlier payload, old-focus-epoch invalidation, and current-epoch done serial mismatch applying incoming edits once while deferring outgoing until a match. Include initially empty→non-empty and normal→password→normal protocol cases.

Clipboard:

  • Source contract: eframe-0.36.2/Cargo.toml:234-237 forces egui-winit with features = ["clipboard"] even with eframe defaults off. 0.37's eframe exposes clipboard = ["egui-winit/clipboard"] and depends on egui-winit with default-features = false (eframe-0.37.0/Cargo.toml:60,239-241). Without the feature, egui-winit-0.37.0/src/clipboard.rs:113-141 stores set_text only in-process. DesignCraft's Copy/Cut calls ctx.copy_text (crates/ui-egui/src/lib.rs:806-824), and its focused-field Paste path sends ViewportCommand::RequestPaste (menus.rs:2717).
  • Enable eframe/clipboard in the D1 source-compatibility PR to preserve native OS Copy/Cut/Paste. This activates arboard/smithay in egui-winit alongside the app's arboard read path; keep the app-owned install_clipboard_guard so control actions never write or read the user's clipboard, and keep Services::clipboard_text for the menu Paste path that must not go through egui.
  • Native evidence in D1: select text, Copy/Cut, read from an independent arboard process/handle on X11 and Wayland; test object copy and SVG/text paste; verify focused TextEdit paste and canvas paste are not doubled; verify control-channel privacy.
  • D1 includes E4/W15P in its complete fork pin; D3 expands native policy coverage. E4 routes regular clipboard through winit's Wayland data device with an arboard fallback; W15P adds a native PRIMARY path on the same connection/event queue and per-seat lifecycle using SCTK 0.19.2 PrimarySelectionManagerState/devices/sources/offers. Do not bind a second regular wl_data_device. Test PRIMARY pointer- and keyboard-triggered ownership (including keyboard-only focus), serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfers, password exclusion and no unchanged-selection reclaim on wlroots/KWin/Mutter. Native capability evidence is still required; compile-only is not acceptance.
  • Event::PasteImage is new but DesignCraft has no image-paste contract. Leave it unhandled rather than inventing a placement path. Add a test that it does not panic and does not leak into text paste.

Keyboard layouts: after the fork pin, RawInput::keyboard_layout_is_latin and RawInput::latin_layout_chars (E19) must preserve tool and command shortcuts on Russian, Arabic and Hebrew layouts. W25 covers Linux Latin-layout lookup, W26 macOS deletion and W27 Wayland surrounding text; Windows keeps its own native input path. E19 also needs a real Windows test with a non-Latin layout; a synthetic KeyboardEvent test is not enough for the OS layout table.

3.6 Panels, docking and viewports

DesignApp::ui keeps the existing order:

  1. chrome::app_bar (Panel::top("app_bar"))
  2. optional chrome::control_bar
  3. chrome::status_bar (Panel::bottom("status_bar"))
  4. toolbar::show (Panel::left("tools"))
  5. dock::show / panel_docking::show (Panel::right("dock"), Panel::right("shared-panel-dock"), icon columns)
  6. CentralPanel::default() last among panels
  7. second-window show_viewport_immediate
  8. floating panels/flyouts, story editor, dialogs/about/palette (areas/windows/modals)
  9. custom-titlebar resize borders

0.37's Panel constructors accept AsIdSalt; the string salts remain stable, but the panel Id is now parent_ui.scope_id().with_salt(salt) (egui/src/containers/panel.rs:974- 975), whereas the 0.36 panel id was the salt alone. D1 updates test helpers such as dock.rs:473 and panel_docking_tests.rs:639 to load the parent-salted key using the actual parent Ui scope. Eframe persistence is disabled: there is no saved egui PanelState migration. Preserve app-owned UiState/docking layout through save_prefs and test its save/reopen behavior and panel rectangles. Keep the docking identity designcraft-panel-docking unchanged for saved layout and live drag state.

craft-ui CL-1:

  • Do not use PR199's b68de8c/ec8840a trees as a base. They are stale relative to current 4919088, omit crates/platform, and revert current security/settings/docking/printer fixes (including floating-group move/resize guards d66e345/fd9cf54).
  • Apply only the API-port hunks from 8614e4a (and relevant README changes from 1764bca) onto current 4919088, preserving intervening fixes, crates/platform and unrelated crates. Review a diff against both 5c874d51 (the current DesignCraft pin) and 4919088; pin the exact resulting immutable revision.
  • WidgetType → Role in buttons/mod.rs, docking/surface.rs, layout/resize.rs, tabs/mod.rs; Id::new → Id::unique; Ui::id/UiBuilder::id → scope_id.
  • The corrected revision keeps the same docking transaction model and DockArea API DesignCraft uses. A model/permission change is not incidental: if intentionally changed, state it as a product decision with tests.
  • CL-3 adds the explicit opt-in DockArea::direction(DockDirection::{LeftToRight, RightToLeft}) visual policy. DesignCraft calls LeftToRight explicitly for now (no E9 global mirroring) and relies on CL-3's geometry/hit/drag/keyboard tests. Saved physical splits, rectangles and logical tab order must be unchanged by direction; never reverse saved panel order or pass display-bidi text into shaping/clipboard.

The second window uses ViewportId::from_hash_of("designcraft_second_window") and ViewportBuilder::with_title(...).with_inner_size([960, 720]). Keep the ID and the immediate viewport. Add tests for: opening/closing the second window through window.newWindow; the main window's pane focus returning on click; close-request cancellation of the main viewport; screenshot correlation to the root viewport only; CentralPanel not consuming the second window's available rect; and #8748 hidden-parent/ deferred-child behavior while the root is occluded/minimized/overdue and while visible() == None (open, update, close, and acknowledge without restoring the parent).

3.7 Fonts, glyphs, RTL and accessibility

theme::font_definitions builds FontDefinitions from bundled Source Sans 3/JetBrains Mono plus optional craft-fonts (CRAFT_FONTS_DIR) and installed system faces selected by SYSTEM_UI_FONTS. install_fonts returns false while the background font scan is running and the app reinstalls when system_fonts_ready() becomes true. This must stay the source of truth.

Concrete changes:

  • theme::SystemFont::new and the FontData { index, tweak, ..from_owned } constructions are source-compatible with 0.37's FontData; only test assertions that read definitions.font_data[name].font or data.as_ref() need data.bytes()/font.as_ref().
  • Set NativeOptions::system_font_fallback = false explicitly and keep eframe's system_fonts feature off. This prevents egui_system_font_provider from adding fonts ahead of DesignCraft's script-specific order and makes the policy visible in code. Document that DesignCraft's own FontDb provides system fallbacks.
  • FontDefinitions::default() is empty when default_fonts is off (epaint-0.37.0/src/text/font_definitions.rs:111-117). Add egui/default_fonts explicitly to the designcraft-ui-egui dev feature set and install DesignCraft's fonts before layout in package-scoped tests, together with explicit monochrome_emoji_fonts where used. This restores egui's built-in icon/emoji fallback without relying on the app binary's feature unification.
  • Production keeps MissingGlyphPolicy::Tofu (the default). Tests that deliberately draw unsupported scripts use HarnessBuilder::allow_missing_glyphs() with a named reason; tests proving coverage keep the panic policy. Do not set tofu globally in production tests merely to silence a regression.
  • Test fixtures use the licensed fonts pinned in the egui plan §10.6 (craft-fonts 2481a8a, Noto Sans Arabic/Thai/CJK, BIZ UDPGothic; Noto Sans Hebrew and Noto Color Emoji from their upstream OFL sources with the recorded SHA256/licences). DesignCraft keeps its own policy: Latin UI/document fonts stay in assets/fonts/; central craft-fonts remains an optional CRAFT_FONTS_DIR input; do not copy new font binaries into this repository. Fetch fixtures into the task's explicit fixture directory and retain licences.
  • Add a glyph-coverage test over every i18n catalog string and every UI font family, using FontDb faces, not egui's has_glyph alone (it can report false for the face that supplies the replacement glyph). Gate craft-fonts-specific CJK expectations on CRAFT_FONTS; when absent, assert the built-in coverage and skip CJK glyph assertions explicitly. Existing Ukrainian/Russian tests in theme.rs are the model.

RTL/bidi single owner:

  • At D1 the fork's E5/E12 becomes the only bidi owner for egui-rendered UI/label text. rtl::widget/rtl::label pass logical LayoutJobs (with explicit language/direction where needed) to fonts.layout_job; remove the manual BidiInfo::visual_runs reordering, section reversal, neutral-character mirroring, Arabic family swap and per-row row.pos.x += width - row.size.x right-alignment in rtl.rs. Keeping them would process Arabic/Hebrew a second time in epaint's BidiLevels::new (the fork's text_layout.rs:120 always runs bidi; LayoutJob::direction defaults to Auto). The document composer's own crates/compose/src/bidi.rs/shape.rs remains the bidi owner for document text and canvas glyph placement; this change is about the egui label path, not a rewrite of document composition.
  • Keep Galley::job.text as the logical source and use logical strings for clipboard, save, accessibility, search and commands. Never pass visual-order/display-bidi text to shaping or the clipboard. Keep the bounded layout cache (256 labels/4096 bytes) keyed by the logical job, font token and language/direction.
  • Editable TextEdit paths use the fork's logical text and grapheme/caret mapping; the custom canvas editor must not feed visual-order strings.
  • D3 differential tests: Arabic/Hebrew/mixed fixtures assert visual glyph order, logical Galley::job.text, row widths, caret/hit positions, selection across boundaries, marks, end caret and exactly one alignment (no double right-shift). ASCII fixtures must remain identical to stock 0.37 where the API is shared. Run the fixtures at D3, not "only if replaced".

Context::set_language (E17, fork only) is applied once when ui.language changes, after fonts are installed and before layout. It must not change stored document text, ids, commands or persisted workspace data. Context::language is the fallback on the web if eframe does not set it.

Do not enable Style::right_to_left (E9). DesignCraft keeps an LTR panel/docking model with RTL text inside labels, matching the persisted craft_ui::docking::Layout and existing workspaces. Enabling E9 would mirror menus, scrollbars and docking chrome without a product decision and would double-mirror shaped text. The explicit DockArea::direction(LeftToRight) above is the only docking direction policy in this migration.

Accessibility labels: kittest's check_accessibility now runs by default, and the first green stage must satisfy strict names/glyph policy. Add explicit names where a control is an input (Role::Button, TextInput, CheckBox, Slider, SpinButton, ComboBox, Tab, Splitter):

  • icons::button, widgets::icon_toggle_sized, widgets::checkbox, widgets::dropdown, toolbar/fill/stroke chips: use the existing localized tip/label via Response::accessible_name or WidgetInfo.
  • widgets::NumField: add a label field or .accessible_name; do not leave the internal id as the only name.
  • dialogs::text_field: pass the visible label id and use ui.label_inputs_by(label.id, …) or ui.name_inputs(tr(key), …); section labels added as plain ui.label do not automatically name the field.
  • panels/font_menu.rs, panels/pages.rs, panels/layers.rs, panels/library.rs, story_editor.rs, new_document.rs: bind each field to its visible label; icon-only star/expand buttons use their localized tooltip.
  • Role migration files: the plan's earlier list omitted panels/properties.rs, panels/swatches.rs, panels/layers.rs and object_control.rs. Map: swatch/color well → Role::Button with the swatch name/value; layer row → Role::ListItem/Role::Button matching its selectable row behavior; object-control label → Role::Label; combo/dropdown → Role::ComboBox; numeric field → Role::SpinButton; text field → Role::TextInput; checkbox → Role::CheckBox; tab → Role::Tab; split/resize → Role::Splitter.
  • craft_ui::docking::DockArea already names tab/overflow/resize controls after CL-1/CL-3; keep Role::Tab, Role::Button, Role::Splitter and labels stable.
  • Response::labelled_by is preferred when a visible label exists; Response::accessible_name for icon-only controls; Ui::name_inputs only for third-party/grouped widgets that cannot be named at their construction site. Do not paper over a missing label with a generic "field" name.

3.8 Theme and visual defaults

theme::apply sets Visuals, text styles and widget palette from Tokens; most colors are explicit. 0.37's roomier button defaults and larger corner radius can change layout even when tokens are unchanged. Audit:

  • v.button_padding, v.menu_margin, v.window_margin, v.item_spacing, v.interact_size and CornerRadius in theme::apply;
  • widgets::FIELD_H, BUTTON_H, SEG_W;
  • panel default sizes in dock.rs, panel_docking.rs, toolbar.rs;
  • menu popup width/height and menus::menu_button;
  • chrome::app_bar / control_bar / status_bar heights.

Do not replace explicit geometry with 0.37 defaults. A visual diff is an intended change only after review and is recorded in evidence.

4. Dependency and publication topology

4.1 Intended suite graph

For suite validation, DesignCraft resolves exactly one copy of each egui-family crate and one winit. The revisions below are placeholders until the reviewed storytold publications exist; candidate aiongg hashes are comparison inputs only.

designcraft / designcraft-web / designcraft-ui-egui
  -> egui 0.37.0          git <FINAL_STORYTOLD_EGUI_REV>   (contains E1-E21, E21b, E4/P2b)
  -> eframe 0.37.0        git <FINAL_STORYTOLD_EGUI_REV>
  -> egui-winit 0.37.0    git <FINAL_STORYTOLD_EGUI_REV>
  -> egui-wgpu 0.37.0     git <FINAL_STORYTOLD_EGUI_REV>
  -> egui_kittest 0.37.0  git <FINAL_STORYTOLD_EGUI_REV>   (dev)
  -> epaint/ecolor/emath/epaint_default_fonts  git <FINAL_STORYTOLD_EGUI_REV>
  -> winit 0.30.13        git <FINAL_STORYTOLD_WINIT_REV>  (W1-W27 plus W15P PRIMARY)
  -> craft-ui             git <FINAL_CRAFT_UI_REV>         (CL-2F after adapter review; includes CL-1/CL-3)
  -> wgpu 30.0.1          registry
  -> accesskit 0.25.1     registry

egui_extras is not used by DesignCraft; do not add or patch it. egui_plot and egui_dock are not used; do not assume version numbers for them. egui_system_font_provider is not used because the system_fonts feature stays off.

4.2 Root [patch.crates-io] additions

A dependency's own [patch] section does not propagate to this consumer. Add all of the following to the root Cargo.toml, each with the same full immutable git revision and no moving branch or machine-local path:

[patch.crates-io]
egui = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
eframe = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
egui-winit = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
egui-wgpu = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
egui_kittest = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
epaint = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
epaint_default_fonts = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
ecolor = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
emath = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
winit = { git = "https://github.com/storytold/winit", rev = "<FINAL_STORYTOLD_WINIT_REV>" }

The egui remote must be the published, reachable storytold/egui mirror containing the reviewed fork history. Pinning aiongg/egui directly is not the production plan; it would be an explicit policy decision, not a default. The winit remote must likewise be the final reviewed storytold/winit, not the candidate URL/revision. A branch name, a machine-local path, or a path override must never appear in a shipped manifest.

Workspace dependency versions change from 0.36 to 0.37 for egui, eframe and egui_kittest. egui_extras is present in the workspace dependencies but is not used by DesignCraft; bump it only if a future consumer adds a real dependency on it. apps/designcraft keeps its eframe feature list and adds clipboard in D1 (see §3.5). Do not add system_fonts.

4.3 Lockfile, clean checkout and duplicate checks

After generating a lockfile through the managed build workflow, verify:

  • Cargo.lock has exactly one egui, epaint, ecolor, emath, eframe, egui-winit, egui-wgpu, egui_kittest and winit entry.
  • Every egui-family source is git+https://github.com/storytold/egui?rev=<FINAL...>#<FINAL...>; no registry+.../egui-0.36.2 or -0.37.0 remains and no candidate aiongg source is shipped.
  • winit source is the exact final storytold revision; no registry 0.30.13 and no old 3d51077f... copy remains.
  • wgpu stays 30.0.1; accesskit moves to the version egui 0.37 requires (0.25.1 in the reference graph).
  • cargo metadata --locked --format-version 1 resolves the same graph as the lock.
  • cargo tree -d shows no duplicate egui-family crate and no duplicate winit. A duplicate accesskit_consumer from unrelated crates is a warning to inspect, not an automatic failure.
  • cargo xtask layers still reports designcraft-ui-egui as the only egui consumer below L7, and cargo xtask wasm includes designcraft-ui-egui.
  • cargo xtask ci passes with the exact lock and with and without CRAFT_FONTS_DIR for the font tests that support both.
  • Clean checkout: cargo fetch --locked and cargo metadata --locked --format-version 1 succeed without local overrides. Negative fixture: replacing the final egui/winit revision with a candidate hash or the old DesignCraft winit override must fail the gate.

The root [patch.crates-io] also contains the unrelated PDF entries. Keep krilla, hayro, hayro-interpret, hayro-syntax and hayro-jbig2 at storytold/craft-libs 89dcbb3060377a26c99a6d80caa16084366704c4 unchanged. They are not egui vendors and have their own provenance, tests and removal conditions.

4.4 craft-libs CL-1/CL-2 and language work

CL-1 is a rebased API port, not a merge of PR 199's tree. Start from current 4919088, apply only the 8614e4a hunks (plus relevant 1764bca README changes), and preserve d66e345/fd9cf54, crates/platform, security/settings/printing and unrelated crates. Review the diff against both 5c874d51 (DesignCraft's current pin) and 4919088. The corrected revision must:

  • keep craft-ui production source compatible with stock egui 0.37.0;
  • expose the same docking transaction model and DockArea API DesignCraft uses, plus the CL-3 opt-in direction API;
  • provide the CL-2 capture slot and CL-2F frozen API used by DesignCraft's screenshot adapter;
  • pass its registry-egui 0.37 check, all-feature clippy and tests;
  • publish an immutable revision that DesignCraft can pin.

DesignCraft does not take fork-only API from craft-ui: the fork lives in the dependency patch table, not in craft-ui's manifest. This keeps craft-ui publishable and lets DesignCraft choose the fork independently.

Language work (#198) enters DesignCraft only through the final fork pin and the selected app changes in §3.5/§3.7. It must not introduce ICU locale conversion, change document language semantics, mirror the whole UI, add fonts, or replace the docking model.

5. Existing patches and vendored fixes: keep/remove audit

DesignCraft has no vendor/ directory. Its relevant non-registry inputs are:

Patch / input Current revision Provenance Decision for this migration Removal condition
winit storytold/winit 3d51077f... (W1–W14) storytold/winit craft/0.30.13; X11 with_x11, macOS with_close_requested_on_terminate, mac_window::sync_movable Replace with <FINAL_STORYTOLD_WINIT_REV> (W1–W27 plus W15P). W12/W14 remain; W15/W15P/W25/W26/W27 are needed by E4/E19/E21. Upstream winit 0.30/0.31 contains the equivalent patches and the suite no longer needs the fork.
egui/eframe/egui-winit/egui-wgpu/egui_kittest/epaint/ecolor/emath registry 0.36.2 crates.io Replace by <FINAL_STORYTOLD_EGUI_REV> containing E1–E21, E21b and E4/P2b in D1 after the isolated stock-0.37 comparison, including P7c/P8. Candidate a4f9a17f is comparison input, not the pin. Upstream egui release contains the equivalent behavior (or the suite drops each patch when its PATCHES.md condition is met).
egui_extras registry 0.36.2, only in craft-ui dev-dependencies crates.io; not used by DesignCraft Leave alone. Do not add a patch entry unless a DesignCraft crate directly uses it. No removal needed.
krilla craft-libs 89dcbb30..., version =0.8.2 PDF backend vendor patch in craft-libs; unrelated to egui Keep exactly. Its own PDF backend gate.
hayro, hayro-interpret, hayro-syntax, hayro-jbig2 craft-libs 89dcbb30..., pinned versions PDF parser vendor patches in craft-libs Keep exactly. Their own PDF parser gates.
craft-ui 5c874d51... craft-libs published revision Move to the reviewed CL-1/<CL-2_PROVISIONAL_REV> in the D1 capture pilot, then <FINAL_CRAFT_UI_REV> (CL-2F/CL-3). Replaced by the final reviewed revisions.
craft-pdf, craft-atomic, craft-cms, craft-docpath, craft-print individual craft-libs revisions Shared libraries, not egui Keep. Do not conflate their revisions with the UI migration. Their own release cadence.

Do not delete an unrelated vendor or patch merely because the lockfile changes. Do not copy aiongg/egui's workspace [patch] section into DesignCraft's manifest; only the root [patch.crates-io] entries above apply.

6. Test and regression strategy

6.1 Fixtures and determinism

  • Use file.newSample / designcraft_engine::sample::magazine() for a deterministic multi-page document. Avoid user files and environment-dependent font caches.
  • Run native tests with DESIGNCRAFT_NO_PREFS=1 and a dedicated DESIGNCRAFT_CONFIG_DIR, or an explicit empty override, so preferences and recovery do not leak between runs.
  • Build with the pinned craft-fonts ref abb83316d96aa59c1cf64784289e378fe9fa5695 for release/font evidence, and also without CRAFT_FONTS_DIR to keep the bundled-font path tested. Use the §10.6 font fixtures for Arabic/Thai/CJK/Japanese/Hebrew/color-emoji coverage with their recorded SHA256/licences; keep them out of app source.
  • Use fixed window sizes 1440×900 and 1280×800, at 1× and 2× pixels_per_point, and fixed themes mediumDark, light, highContrast. Record the exact fixture, font directory, DPI and OS/renderer with every screenshot.
  • Use CRAFT_UI_DOCKING_FIXTURES for the existing explicit docking visual fixtures; do not turn those optional captures into unconditional CI jobs.
  • Every command runs through maintenance/task.py run/status/finish with an allocated target/VM and durable evidence; do not run tests directly.

6.2 Unit / kittest regressions

Area Test (new or adapted) User-observable assertion
Frame staging logic_frame_runs_once_across_a_discarded_pass Force a request_discard by making the Tools panel single-column too tall; assert one font/theme/recovery/FPS tick and one input stage, not two.
Host staging host_multipass_counts_one_presented_frame Drive the real host App wrapper through a forced discard; frames/startup.presented/error logging advance once, and macOS menu/Apple-event polling is not duplicated when available.
Hidden logic hidden_logic_ticks_after_completed_multipass_keep_working A completed multipass frame followed by repeated run_logic ticks runs font readiness/recovery/queue checks each tick with fresh viewport data and no stale index assumption.
Hidden forced paint forced_hidden_paint_is_a_ui_pass RequestPaintWhileHidden with visible() == Some(false) and with visible() == None runs logic + ui; visibility is not used as a phase guard.
Control while hidden control_gesture_requests_paint_until_all_events_apply A hidden multi-frame ui.click/ui.drag/ui.key sends RequestPaintWhileHidden on each hidden frame while events remain; the final command/undo result is correct. Pure engine.execute requests no paint.
Screenshots two_screenshot_viewports_resolve_to_their_own_paths Internal capture-helper test (the ui.screenshot control method stays ROOT-only): two outstanding requests with different tokens/viewports receive the matching image; no cross-write.
Screenshot lifecycle screenshot_cancel_timeout_and_late_callback_release Timeout/cancel/close writes no file and sends no second reply; a late callback releases its image; aggregate pending bytes return to zero.
Screenshot memory screenshot_admission_bounds_aggregate_bytes Oversized/concurrent captures are rejected or bounded; peak retained bytes stay within the admitted budget; E2 surface-fit scaling is correct.
Mouse wheel wheel_events_keep_their_source_and_phase Alt-wheel zoom, plain-wheel scroll, trackpad phase end and momentum source do not latch modifiers.
Roles/IDs role_and_id_migration_keeps_existing_state_keys A saved UiState/workspace round-trips; panel tabs, layer rows and styles resolve by the same stable IDs.
Panel state saved_panel_state_migrates_to_parent_salted_ids Old-format panel rects/widths load into the new parent-salted keys; the same outer rects and panel order result.
Panels panel_order_and_central_rect_are_unchanged App bar/control/status/tools/dock occupy the same outer rects; central canvas rect is unchanged at 1440×900 and after narrow-resize.
Docking saved_workspace_restores_tree_float_and_hidden_panel (existing plus 0.37) Docking tree, floating geometry and hidden placement survive serialization; invalid actions are atomic; direction changes do not reorder saved splits/tabs.
Second window second_window_opens_closes_and_keeps_main_focus Popout exists, close request removes it, main pane focus returns; main close can still be cancelled.
Hidden child (#8748) hidden_parent_child_viewport_updates_and_closes With the parent occluded/minimized/overdue and with visible() == None, the child opens/updates/closes and acknowledges without restoring the parent.
Accessibility every_visible_input_has_an_accessible_name Harness::unnamed_widgets() is empty; specific roles/labels for tabs, icon buttons, numeric fields, combo boxes, layer rows, swatches and text fields.
Glyphs every_catalog_character_has_a_face_or_a_declared_gap Bundled fonts cover Latin/Cyrillic catalogs; craft-fonts CJK assertions run when present; tests use panic policy for covered text and explicit tofu only for deliberate gaps.
IME exact bytes ime_replacement_deletes_exact_bytes_in_one_undo_step DeleteSurroundingBytes replaces a combining mark/letter and Undo restores the exact prior text in one step; DeleteSurrounding character path still works; caps/UTF-8/track changes are enforced.
IME surrounding text outgoing_surrounding_text_is_logical_and_bounded DesignCraft canvas emits logical model text after delete+commit/deletion-only; preedit excluded; <=4000 bytes at UTF-8 boundaries; focus/password/no-IME clears; no None placeholder; latest completed pass including None replaces the descriptor/payload atomically; owner remains present for Unavailable and W27 clear/epoch/serial behavior follows P7b.
IME composition existing canvas/story_editor preedit-commit-cancel tests Marked text does not duplicate, click-away discards on macOS, no undo step for a cancelled composition.
Text selection (#8763/#8764) click_away_then_button_acts_on_engine_selection A button reading the app's engine TextSel after clicking away from an egui TextEdit still acts on the intended range; stock 0.37's TextEdit cursor/selection loss is recorded and does not change the engine target; if a future control reads TextEdit state, the command is deferred with an explanation.
Commands/undo multipass_edit_is_one_undo_step A text edit, live-preview commit, dialog apply or toolbar/app-bar button produces exactly one history entry across discarded passes (including the second viewport).
Save/reopen save_reopen_preserves_document_and_workspace Save to a temp .designcraft, reopen through file.openBytes, compare document model/selection/style/swatches; saved workspace and panel layout restore.
Clipboard native copy_cut_write_the_system_clipboard On X11 and Wayland, select text/objects, Copy/Cut, read from an independent arboard reader; paste back; control actions never read or replace the clipboard.
CLI control privacy existing control_paste_never_reads_the_system_clipboard Control actions still never read or replace the user's clipboard.
Web drop web_drop_is_captured_in_raw_input_hook A browser drop with a synthetic File starts the async read in raw_input_hook, reaches the inbox once, and opens the document; no drop read remains in WebShell::logic.
Buffered input hidden_restore_drops_stale_text_ime_and_incomplete_pointer After a hidden tick, changed focus/document discards buffered text/IME/paste; a partial pointer chord is dropped; complete pointer/key events replay in order; two windows stay separate.
Menu/palette existing menus tests plus 0.37 role checks Menus open/close, keyboard activation, overflow within viewport, Quick Apply runs one command.
Canvas existing guide/column/frame-hit tests plus new 0.37 geometry Hit tests and smart guides do not regress from panel rect changes.

6.3 Semantic accessibility assertions

Add Harness::check_accessibility() (the default) to UI tests and explicit queries:

  • Menus: root menu items have Role::MenuItem or Role::MenuBar; disabled items remain reachable by name.
  • Panel tabs: Role::Tab, selected state and localized label; tab drag still starts from the tab rect.
  • Icon-only controls: tooltip text is the accessible name; star/expand, eye/lock, fill/ stroke chips, screen-mode toggles, toolbar overflow.
  • Text fields: each has a visible labelled_by relationship or a placeholder; a blank label is not an acceptable name. Numeric fields expose Role::SpinButton and a value.
  • Swatches/color wells, layer rows, object-control labels, combo boxes: the mappings in §3.7, with name/value/selected state.
  • Sliders and split handles: Role::Slider/Role::Splitter, value/range and accessible name; keyboard arrow changes still work.
  • Dialog modals: Role::Dialog or a named Area; the title is the name; Escape cancels.
  • Focus: Tab order reaches the control bar, panels, canvas and dialog buttons; focus ring uses Tokens::accent/focus_stroke; no invisible widget is exposed.

These are semantic assertions, not only screenshot checks. A screen reader sees the same names and roles across 0.36.2 -> 0.37 except where a deliberate mapping (e.g. SelectableLabel -> Button) requires a new role.

6.4 Visual, native and browser workflows

  • Whole-window PNGs at 1440×900/1× and 2×, medium-dark and light, with the same sample document and fonts. Compare against 0.36.2 baseline produced in the same environment.
  • Justify tolerances in advance: geometry assertions use exact Rect comparisons for panel/canvas/tab rects (within 0.01 pt), and pixel comparisons allow antialiasing noise only where the change is explained (for example 0.37 corner radius). Do not accept a golden because it changed; inspect .new vs .old, list the intended deltas, and keep the old baseline for rollback.
  • Add high-contrast, Arabic, Hebrew, CJK and color-emoji fixtures. For RTL, compare glyph order, logical Galley::job.text, row width and caret/hit positions, not only pixels.
  • Windows: use a uniquely owned clone and C:\CraftApps\Launch-CraftDesktop.ps1 through the scheduled-task helper. Drive control-channel edit/save/reopen, dock/float/popout and ui.screenshot, inspect fresh screenshots, and record the software renderer (Microsoft Basic Render Driver) as a separate row.
  • Linux: native X11 and Wayland runs with compositor identity; exercise panels, second window, copy/paste/PRIMARY, file drop, IME (Wayland) and visible edited/saved/reopened workflows. No inherited display/compositor exists at planning time; provision an isolated display/window manager and compositor before claiming this row.
  • Browser: run the Trunk build in a pinned browser with WebGPU and ?webgl; load the sample, edit, save/download, drag-drop a synthetic file, take screenshots and record the browser/ adapter/backend. Wasm compile and ui_shot are not browser presentation evidence.
  • Keep software-renderer and real-GPU results separate. Capture native screenshots with ui.screenshot or a native window capture; ui_shot is offscreen only.

6.5 Deliberate broken-behavior probes

Before accepting the suite, temporarily break each behavior and confirm the test fails:

  1. Remove an accessible_name from a toolbar button -> accessibility test names it.
  2. Change a panel salt ("dock" -> "dock2") or load the wrong parent-salted test key -> panel identity/geometry test fails; app-owned saved layout remains separately asserted.
  3. Call ctx.root_ui inside DesignApp::ui -> test panics with the documented recursive borrow message (run in a catch_unwind probe, not production).
  4. Remove the first-pass logic_frame guard -> forced-multipass test sees two logic-frame ticks. Do not use visible() to "fix" it.
  5. Correlate screenshots by arrival order instead of the per-request slot -> two-token/ two-viewport test writes the wrong file; cancel a slot and let a late callback arrive -> no file/reply.
  6. Drop DeleteSurroundingBytes handling or fall back to the 64-step text.delete loop -> exact-byte/one-undo test leaves replacement text or extra undo steps.
  7. Remove the E21b surrounding-text adapter or emit display-bidi text -> logical/bounded outgoing-text test fails.
  8. Restore rtl.rs's manual reorder alongside fork E5/E12 -> RTL differential test shows double processing/double alignment.
  9. Restore Id::new/Ui::id -> clippy -D warnings fails, proving the warning gate.
  10. Reintroduce Event::Screenshot scanning or a strong Context capture in the callback -> compile/test or cancel/shutdown test fails.
  11. Move web drop reading back into WebShell::logic -> browser drop test fails.
  12. Remove the host first-pass guard -> host multipass test sees two frame counts.

7. Performance measurement protocol

7.1 Baseline timing and environment

Record a baseline before changing production code, from the exact current SHA e74208dd..., lockfile, feature set and craft-ui revision. Run the candidate from the exact migration revision. Use separate fresh processes for each sample; do not compare a warm process with a cold one.

The benchmark harness is test-only work and must exist in both baseline and candidate trees. Stage it in a separate test-only commit, cherry-pick it into a read-only baseline worktree at e74208dd..., and run the baseline first through task.py. It records Instant-based raw samples around run_ui/operation boundaries; it must not use perf.frame_ms/now_ms() (SystemTime on native, 0.0 on wasm) as the acceptance timer. ui.inspect reports only the last frame and ui_shot prints control replies, not per-frame timings; neither is a sample source.

Record with every result:

  • source SHA (baseline/candidate/stock-0.37 control), Cargo.lock SHA-256, feature list, CRAFT_FONTS_DIR and craft-fonts ref, fixture name/hash;
  • OS and version, CPU/GPU, driver, backend (Vulkan/DX12/Metal/GL/WebGPU), renderer type (hardware vs WARP/llvmpipe), display scale, window size, pixels-per-point;
  • profile (release unless a debug-only diagnostic is named), warmups, repetitions, raw samples, median/p95/p99, peak RSS, allocations, texture upload bytes and idle CPU/repaint count.

Host capacity/qualification facts: read-only inspection finds an i915 PCI 8086:3E96 render device and a readable/writable /dev/dri/renderD128 to the parent; the worker sandbox may hide it. No display session or compositor is inherited. Availability is not a qualified Vulkan/OpenGL/native compositor result; the managed renderer probe must identify the actual backend. The Windows baseline is software-rendered (Microsoft Basic Render Driver). A 128 GiB Windows full clone may not fit current capacity; use an allocated baseline-backed overlay with an adequate reservation or retain the platform row as blocked. macOS and physical Windows/audio remain separate rows.

7.2 Operations

  1. Startup: at least 20 fresh processes; measure wall/Instant from process start to first presented frame and to first interactive sample document. Report RSS and the #8512 system-font discovery/first-missing-glyph component separately. The #8736 DX12 renderer/shader/pipeline construction is a separate Windows row, never a UI frame.
  2. Idle UI: open the sample, leave it untouched for 60 s, count frames/repaints and CPU. No continuous repaint storm; the font scan's 250 ms poll must stop once cataloged.
  3. Frame distribution: at least 1,000 individual frames per configuration, with at least 10 warmups and raw per-frame samples; report p50/p95/p99. Run the sample, the Tools panel two-column state and a floating dock. Record shapes, vertices/indices, texture set/free count/bytes, galley cache size and requested-repaint count.
  4. Steady operations: at least 30 timed iterations per operation after 10 warmups: layout, panel open/float/dock/activate latency, canvas zoom/pan/damage patches, selection and caret movement, text layout/tessellation.
  5. Text tessellation #8731: fixed 120-label ASCII scene plus a mixed/color-glyph control; time Context::tessellate separately for 0.36, stock 0.37 and the final fork. If the ~60% plain-text regression reproduces, require a color-safe row-level fix or a documented exception; clamp-only is not a fix.
  6. #8736 DX12: Windows clone; time renderer/pipeline/shader construction separately from first frame, with predictable filtering disabled/enabled as applicable. Record adapter and driver.
  7. #8512 fonts: cold process with bundled fonts, with CRAFT_FONTS_DIR and with a test-only system provider fixture; compare discovery/startup and glyph coverage. Do not enable the system provider in production to win the measurement.
  8. #8763/#8764 selection: select text then click a toolbar/context action; record command latency and assert the intended range/undo. This is correctness evidence; the tail timing is a separate sample.
  9. #8748 hidden child: open/update/close the second viewport while the parent is hidden/occluded/overdue; record first visible child frame and close acknowledgement.
  10. Screenshots: visible and hidden ui.screenshot requests; record reply latency, file size, retained bytes, cancellation/timeout and late-callback cleanup.
  11. Memory: RSS after sample load, after 10 pan cycles, after 30 screenshots and after a drain/shutdown; retained image bytes must return to zero.
  12. Stock-0.37 control: where the operation exists unchanged on stock 0.37, run the same minimal adapter/harness against 0.36, stock 0.37 and the final fork; explain semantic differences. Unavailable equivalence is an attribution limitation, not proof an upstream regression is unavoidable.

Use matched commands and the same fonts/fixture for baseline and candidate. Do not report cargo build time as UI performance.

7.3 Thresholds and noise policy

Predefine before looking at candidate numbers. The shared suite floors are minimums; an app may tighten, never loosen:

  • UI frame median: candidate <= baseline + 5%; p95 <= baseline + 10%; p99 (>=1,000 frames) <= baseline + 15%.
  • Canvas render median: <= baseline + 5%; p95 <= +10%; patch count no increase beyond fixture determinism.
  • Idle CPU: <= baseline + 20% and no more than one extra repaint per second after fonts are ready; zero paints in 2 s after settle.
  • Startup: <= baseline +5% median / +10% p95, with the #8736 DX12 shader-compilation component reported separately. A #8736-sized regression needs the upstream/fork fix or explicit approval.
  • Screenshot reply p95: <= baseline + 10%; retained bytes bounded and zero after drain.
  • Peak RSS: investigate a repeatable increase over +5% or +16 MiB, whichever is reached first; report absolute and percentage. (Use the app's existing stricter budget if one is found.)
  • Text tessellation: if the isolated probe reproduces #8731's ~60% regression, require a color-safe row-level fix or a documented exception; never accept a clamp-only shader change.
  • Sub-millisecond operations: the relative increase must also exceed 0.05 ms or be corroborated by stable instruction/allocation counts.

Sample counts: at least 10 warmups and 30 timed iterations per steady operation; at least 20 fresh processes for startup; at least 1,000 individual frames for frame distributions. Use five independent sessions of at least 200 frame samples for tail percentiles, and pool the raw samples for p99; do not report p99 from 30 samples. Alternate baseline/candidate rounds; discard a session if a build/test/profiling process competes for CPU or the coefficient of variation of medians exceeds 3% (5% for microbenchmarks), then rerun. If the difference is within 2×MAD of paired samples, call it equal and do not chase it. Re-run and profile before filing or accepting a regression. Never auto-update a snapshot or lower a threshold to hide a timing or geometry change.

8. Staged implementation PRs and rollback

The dependency DAG is:

plans land -> baseline harness + measurements
completed winit -> egui P8 (P2b/P7/P7b/P7c included)
  -> craft-libs CL-1 -> CL-2 reviewed capture prototype
      -> DesignCraft D1 / GridCraft G1 pilot adapters -> CL-2F API freeze
DesignCraft D1 -> D2 lifecycle -> D3 extended native/language qualification
craft-libs CL-3 + D1 -> D4 docking/accessibility
D2/D3/D4 -> D5 visual/workflow acceptance -> D6 final platform/performance evidence
  • D1 — atomic final-fork compatibility pilot. Pin the complete final storytold egui/winit graph and reviewed craft-ui CL-1/CL-2 prototype; regenerate the lock. Apply roles/IDs/FontData/wheel changes, explicit native clipboard and test fonts, strict minimum names/glyphs, the shared capture-slot adapter and its cancellation/ bounds/wake tests, parent-salted test lookup keys, and the required E21/E21b custom editor adapter. Remove rtl.rs duplicate visual reorder/alignment as the final fork becomes the bidi owner. Preserve app-owned UiState/docking preferences; no invented egui-memory migration. Gate: cargo xtask ci/wasm, UI/native tests, one-family graph, clean locked fetch, capture and IME contract tests, clipboard and save/reopen. This reviewed pilot plus GridCraft's adapter permits CL-2F; stock 0.37 is an isolated benchmark/control workspace, never a production handoff. Rollback restores complete e74208dd source/manifests/lock/executable and prior preferences.
  • D2 — frame stages and hidden input. Add the narrow once-per-layout-frame guard, host pacing/queue separation, monotonic deadlines, buffered synthetic-input policy and per-step hidden paint. The shared capture API is already adopted in D1; extend hidden/multipass/restore stress tests here. Do not replace it with a local interim slot. Revert D2 as a behavior/test unit only if D1's compatibility checks still pass.
  • D3 — extended native/language qualification. After D2 and CL-2F, use the same complete final fork and frozen helper; qualify E4/W15P, P7 key release, E21/E21b and P7c selection/hidden-child behavior on native platforms and add language policy as required. Required IME/bidi API adaptations already land in D1. Rollback removes only these later policy/test changes; it never strips fork patches while retaining fork-only source. A dependency rollback restores the full D1 predecessor set.
  • D4 — accessibility and glyph contracts. Add the remaining names/roles, full role file list, explicit DockArea::direction(LeftToRight), and coverage tests. Gate: unnamed_widgets() empty, explicit role/label queries, glyph coverage with and without craft-fonts, direction geometry/hit/drag and saved-layout tests. Rollback: revert the label/direction changes; no document or command behavior changes.
  • D5 — visual/geometry baselines and workflows. Review every intended 0.37 visual delta, update fixtures only with explanation, add the save/reopen/dock/popout, native X11/Wayland, Windows VM and browser workflow tests. Gate: geometry/hit tests, pixel comparisons, deliberate broken-behavior probes. Rollback: restore the previous reviewed baselines; production remains on D3/D4.
  • D6 — qualification and performance. Windows VM, Linux native, browser, macOS gate, paired performance protocol with the stock-0.37 control. No production behavior change unless a measured regression requires a separately reviewed fix. Rollback: no code rollback expected; keep the evidence and the old dependency set until acceptance.

Each PR must be one coherent outcome, use one task id per commit, and carry the validation results at the reviewed revision. No PR is published without fresh direct human approval.

9. Risks, unresolved questions and blocking acceptance criteria

Blocking unknowns

  1. Final dependency publications. The final reviewed storytold/egui and storytold/winit revisions do not exist yet. Candidate aiongg hashes are comparison inputs only. D1 cannot ship until the complete P8 handoff and winit revisions are published with E1–E21, E21b, E4/P2b, W1–W27 and W15P, and a clean checkout fetches them --locked.
  2. craft-libs CL-1/CL-2F revisions. The 0.37-compatible craft-ui revision must be the 8614e4a port onto current 4919088, preserving d66e345/fd9cf54, crates/platform and unrelated fixes. PR199's b68de8c/ec8840a trees are not bases. The capture API freezes only after the DesignCraft/GridCraft adapter review.
  3. Clipboard/PRIMARY. D1 enables eframe/clipboard to preserve OS Copy/Cut/Paste. D3 needs E4/W15P native evidence on X11 and Wayland (wlroots/KWin/Mutter): pointer- and keyboard-triggered ownership including keyboard-only focus, serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/ slow transfers, password exclusion, no unchanged-selection reclaim and no second regular data device. A compile-only or X11-only result is not acceptance.
  4. Windows GPU fallback. E2/E3 turns some egui-wgpu configure/renderer failures into WgpuError values rather than panics. DesignCraft's gpu::finish already handles eframe::Error::Wgpu(_), but the exact startup fallback behavior must be tested on the Windows clone with a deliberately failing adapter/backend. Do not claim it from unit tests only.
  5. macOS native evidence. macOS menu shortcuts, press-and-hold IME, deleteSurrounding bytes, movable_by_window_background, Dock Quit and viewport screenshots need real hardware or remain explicitly unresolved. A software/VM screenshot is not native evidence.
  6. #8731 text tessellation. The upstream report is a measured regression, not a DesignCraft result. If the candidate reproduces it, a color-safe fix (row-level branch) or an explicit exception is required before accepting the performance row.
  7. #8736 DX12 startup. The shader-compile cost is a Windows startup risk. Measure it separately and do not hide it inside a UI frame average.
  8. #8748 hidden-parent/deferred-child. Closed and unmerged. The final fork must own a tested solution; DesignCraft's immediate second viewport must open/update/close while the parent is hidden/occluded/overdue and while visibility is unknown.
  9. Accessibility scope. The exact number of unnamed inputs will only be known after D1's first strict Harness::check_accessibility() run. D1 must fix the existing test surface or record an explicit per-test policy; D4 completes the remaining controls. If the D4 remainder is large, split "core chrome/panels" and "dialogs/rare panels"; do not disable the check globally.
  10. Text selection #8763/#8764. The source audit finds no control that reads an egui TextEdit selection in the same pass, so no app workaround is planned. The fork owns the upstream fix. If implementation finds such a control, carry #8764 or defer the command with a tested app change; do not hide it in the test.
  11. E21b/P7b. The incoming E21/W27 path is not full surrounding-text integration. DesignCraft's canvas needs a real logical model/preedit/UTF-8 adapter, bounded at 4000 bytes and cleared on focus/password/no-IME; None placeholders are not acceptance. Native Wayland IME trace remains required.
  12. DockArea direction. CL-3's opt-in direction is a visual policy, not a schema change. DesignCraft passes LeftToRight explicitly for now; saved physical splits/rectangles and logical tab order must remain unchanged. No display-bidi text reaches shaping or clipboard.
  13. Platform capacity/qualification. The parent can see i915 PCI 8086:3E96 and /dev/dri/renderD128; the worker sandbox may hide it, and no display session/compositor is inherited. Windows needs a uniquely owned clone/overlay within allocator capacity (a 128 GiB full clone may not fit). Browser WebGPU/WebGL and real native X11/Wayland workflows need provisioning. These rows remain blockers until executed; availability alone is not a pass.
  14. Capture API freeze. CL-2F must be reviewed after both adapters. A strong Context capture, unbounded pending images, a late file write/reply or a lock held across wakeup is a blocking failure, not a follow-up.
  15. Cargo.lock coherence. Regenerate, never hand-edit. A lock with both 0.36.2 and 0.37, a candidate aiongg source, or a machine-local path is an immediate failure.

Non-blocking but important

  • Style default changes may move toolbar/panel geometry even with explicit tokens.
  • egui_kittest 0.37's missing-glyph panic can make an unrelated test fail first; fix the fixture/name or use a named per-test allow with a reason, not a global tofu/disable switch.
  • The web build's new glyph rasterizer can change emoji/fallback rendering. Verify with WebGPU and ?webgl, and keep texture filter/wrap expectations.
  • Cargo.lock must be regenerated through the managed workflow, not hand-edited.
  • The designcraft-testkit workspace dependency points at a missing crates/testkit in the baseline; it is unused, but the dependency audit must not mistake it for an egui consumer. Do not add or remove it as part of this migration.
  • Saved docking direction must never be implemented by reversing saved panel order or serialized tab order.

10. Acceptance checklist

  • Exact baseline SHA and dependency revisions recorded; old 0.36.2 set retained for rollback.
  • All 22 manifests, root lockfile, root patch section, build scripts, ui_shot, platform cfgs and CI workflows audited.
  • Exactly one egui family and one winit resolve from the final reviewed storytold revisions; no registry 0.36.2, candidate aiongg source or machine-local path remains.
  • Clean checkout passes cargo fetch --locked and cargo metadata --locked.
  • cargo xtask ci, cargo xtask wasm, cargo test -p designcraft-ui-egui --lib, trunk build --release, native Windows/Linux and browser workflows pass at the reviewed revision.
  • App::logic/App::ui root-Ui flow preserved; native and web per-pass logic tested; no recursive root_ui; multipass, repeated hidden ticks, forced hidden paint and host counters tested; visibility is not used as a phase.
  • No egui PanelState persistence is invented; parent-salted test keys, panel order/rects and app-owned workspace restore.
  • Screenshots use the craft-ui completion slot: weak callback, viewport-aware wakeup, one-shot, timeout/cancel/close, out-of-order, bounded aggregate bytes and no strong Context cycle.
  • Commands, undo transactions, save/reopen, workspace persistence, clipboard privacy and native Copy/Cut/Paste are unchanged.
  • E4/W15P PRIMARY is evidenced natively on X11/Wayland, or explicitly unresolved; password/unchanged-selection policy holds.
  • E21b outgoing surrounding text is logical, bounded at 4000 bytes, preedit-excluded and cleared on focus/password/no-IME; no None placeholder.
  • Fork E5/E12 is the only bidi owner for egui UI/label text (the document composer remains the document-text owner); no display-bidi text reaches UI shaping or clipboard.
  • Accessibility role/name assertions pass under strict kittest; glyph coverage is explicit with and without craft-fonts.
  • Visual baselines have explained intended deltas; geometry/hit/direction/dock tests pass; #8763/#8764 and #8748 tests have the documented expected outcome.
  • Paired performance results meet the predefined thresholds with >=10 warmups/30 steady iterations/20 fresh startups/1,000-frame distributions and a stock-0.37 control, or every regression has a reviewed fix/exception.
  • macOS, real-GPU, Wayland/GNOME and non-Latin-layout gates are either evidenced or listed as unresolved; no result is implied from a compile or software renderer.

11. References

Primary source inspected locally (read-only; no checkout switch):

  • egui upstream 0.37.0 = 841a328ba2dd8f6996beb7189ebb7fc00e668a2a: crates/egui/src/data/input/event.rs, data/input/mouse_wheel_source.rs, data/input/viewport_info.rs, data/screenshot_callback.rs, viewport.rs, context.rs, containers/panel.rs, id.rs, ui.rs, data/output.rs, accessibility.rs, input_state/mod.rs; crates/epaint/src/text/font_definitions.rs, crates/epaint/src/text/font_data.rs; crates/eframe/src/{epi.rs,native/epi_integration.rs,native/wgpu_integration.rs,web/app_runner.rs}; crates/egui-winit/src/{clipboard.rs,lib.rs}; crates/egui-wgpu/src/{setup.rs,winit.rs}; crates/egui_kittest/src/{builder.rs,accessibility.rs}.
  • Candidate fork for comparison: aiongg/egui craft/0.37 = a4f9a17f64a6bae61350fb85804c9df0d9865916, including PATCHES.md (E1–E21; E21b/P7b still to be added). This is not the production pin.
  • Candidate winit for comparison: aiongg/winit craft/0.30.13-lang = 8b35388bd719e8cc86a95971ba0bc026c9a8f058, including PATCHES.md (W1–W27; W15P still to be added). This is not the production pin.
  • storytold/craft-libs PR 199 head = b68de8c5f4f1c060904d211a7b1320aa0f7bce78; audit merge ec8840af8c5fda695cf195cdd05e5ae078415c07; API-port source 8614e4a04fae516fba16c22b31ff1008cb6be10b; current craft-libs baseline 49190884278f16c9a9f5823d7698a8ffb5deb062.
  • storytold/egui published 0.36.2 = 767b1cad294120689a514bb4bd68c077e46679b9; storytold/winit published 0.30.13 = ffae5198770d7730e718043f035a772b7dbc3b1b.
  • DesignCraft baseline = e74208dd7e0c801d15dfdf926d64cfe5e3b346d3.
  • Lifecycle evidence: /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/lifecycle-source-audit.md (native/web per-pass logic, zero pass index after completed run_ui, visible() is not a phase).
  • IME evidence: /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/ime-surrounding-gap.md (E21b/P7b gap and custom-editor inventory).
  • Shared suite contract: /home/codex/work/CraftApps/egui037-migration-20261022/craft-libs/docs/egui-037-suite-rollout.md (completion slot, DockArea direction, performance sample counts, stock-0.37 control, platform evidence) and craft-libs/docs/egui-037-migration-plan.md (§7.7, §7.8, §10).
  • Font fixtures: egui/docs/egui-037-migration-plan.md §10.6 (craft-fonts 2481a8a, Noto Sans Arabic/Thai/CJK, BIZ UDPGothic, Noto Sans Hebrew, Noto Color Emoji; exact SHA256/licences). No font binaries are added by this plan.
  • Historical diagnostics: /home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/designcraft.{errors,log,prepare.diff}.
  • Upstream reports and notes reviewed through the local snapshot /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/upstream-discussions.json: #8731, #8736, #8763 / PR #8764 (unmerged), #8748 (closed, not merged), #8631, #8535, #8489, #8555, #8583, #8512, #8428, #8657, #8577, #8641.

These links identify the behavior and reports inspected. They are not fresh DesignCraft validation, and an upstream status may have changed since the snapshot.

Local implementation update — 2026-10-23

The user clarified that reviewed plans remain local and implementation proceeds before publication. The earlier requirement to publish/land this plan first is superseded; pushes and PR publication still require explicit approval.

Canonical storytold/designcraft main now resolves to 2e36dcf909627c8f0e5662fe6b4825b1c1c24420. The owned checkout was fast-forwarded to this revision. Earlier baseline SHAs and line anchors above are retained as historical review evidence; use this new revision and its exact lockfile for current-main integration and paired measurements. The original refs remain reachable.

Preserve browser page zoom and canvas touch-action:none alongside packaging fixes.

The complete static delta review and exact old/new object identities are retained in the task evidence: canonical-diffs-refresh-20261023.json and overnight-consumer-review-20261023.md. This amendment claims no new build, native GUI or performance acceptance.

Local pinned font inputs (2026-10-23)

Parent fetched and verified all eight manifest fonts at storytold/craft-fonts 2481a8a324b986a45954e90045ed1f8f2efaaf3c into /home/codex/work/CraftApps/egui037-migration-20261022/fixtures/craft-fonts. Use this exact directory as the optional CRAFT_FONTS_DIR build input. All manifest SHA256 hashes match; licenses retained. This does not waive the required no-corpus test configuration. The separately pinned Hebrew fixture is at /home/codex/work/CraftApps/egui037-migration-20261022/fixtures/noto-sans-hebrew/NotoSansHebrew.ttf with adjacent OFL.txt/provenance.json. Keep these fonts out of app source; required test fixtures may use an explicit external input with verified hash and clear missing-input behavior, never silently disable glyph checks.

Final local landing record — 2026-10-25

This section records the current local landing state. Everything above remains historical planning and implementation evidence: the older candidate hashes, baseline anchors and "planning only" wording describe earlier states and are not the current pins. No commit, index write, ref update, fetch, push, pull request, comment, merge, build, fmt, test, Cargo, VM, resource or cleanup action was performed while writing this record; the qualification results below were produced separately by the parent through the managed task.py workflow and are cited as evidence, not rerun here.

Current dependency pins

Item Revision
egui family (egui, epaint, ecolor, emath, eframe, egui-wgpu, egui-winit, egui_kittest, egui_extras, epaint_default_fonts) storytold/egui 6d518fc799681571963842f95b55a6b68431b15a
winit storytold/winit a517b3846ee8457d84ee4975110bfda6797c44dc
craft-libs (craft-ui, craft-print) storytold/craft-libs 25da1314d443171b4adeaad2cd9454b273f35c6f
CI craft-fonts storytold/craft-fonts b076417b5880bd4154bd22f00695b1d28c6bea83

Cargo.toml and Cargo.lock resolve one egui family and one winit from these storytold revisions; the lock has no registry 0.36.2 egui family and no candidate aiongg source. crates/fonts/build.rs continues to treat craft-fonts as an optional CRAFT_FONTS_DIR input, not a manifest dependency.

Exact dirty union

Item Value
Checkout /home/codex/work/CraftApps/egui037-migration-20261022/designcraft-implementation
Git worktree HEAD (unchanged) 80d13911b2d20fbb5cd9398696627bb3a715417b
Earlier integrated main base in the dirty state d8d78851729a2c1a10e5bfad7c591f3de78651a0
Imported new main 45ed6cfce0f86e59ff66ab18a05c3ec03ccef531 (refs/migration-observations/20261024-1610-main)
New-main distance from the earlier base 141 commits, 127 changed paths
Files in the union 619 = 613 tracked + 6 untracked
Worktree status 186 entries = 180 modified + 6 untracked
Merge state no MERGE_HEAD; index empty; no conflict markers; git diff --check clean

The six untracked paths are exactly:

  • crates/compose/src/placed.rs
  • crates/idml/src/tests/facing_ltr.rs
  • crates/idml/src/tests/frame_grid.rs
  • crates/ui-egui/src/page_history.rs
  • crates/ui-egui/src/panels/story.rs
  • docs/egui-037-migration-plan.md

The 16:28 reconciliation packet recorded 185 status entries: the 92 pre-existing dirty paths plus the 127 imported paths with 34 overlaps. The current tree adds one later modified tracked path, apps/designcraft/src/native_menu.rs (the native-menu/action host-order repair), for 186. Of the 127 imported paths, 103 were adopted whole and 24 were reviewed 3-way content merges; later reviewed deviations are the Windows-only test fixtures in apps/designcraft-cli/tests/it/cli.rs and crates/engine/src/file_access.rs, the documented Id::new → Id::unique port in crates/ui-egui/src/panels/story.rs, and the host-order changes above.

80d13911 is an ancestor of 45ed6cf, so an ordinary merge would fast-forward. The eventual manual union commit therefore needs both parents, 80d13911b2d20fbb5cd9398696627bb3a715417b and 45ed6cfce0f86e59ff66ab18a05c3ec03ccef531, to preserve both the migration branch and the imported main history. No such commit exists yet.

Review-1643 findings on the current source

Both actionable defects from the 16:43 review are fixed in the current source:

  • Document generation identity. InputGeneration and ScreenshotGeometry now compare DocState::identity_epoch through active_document_epoch() (crates/ui-egui/src/lib.rs), so a file.revert that preserves the UID still invalidates buffered input, native drops and admitted or in-flight screenshots. Regression tests cover a buffered input batch, a buffered native drop and screenshots before issue and in flight; all pass in the Linux full-CI job below.
  • NumField disabled presets. The preset segment now uses Sense::hover() and reports self.enabled == false when the field is disabled, does not open its menu, and dims its chevron; disabled_numeric_presets_are_disabled_and_do_not_open passes.
  • The earlier panels/properties.rs evidence mismatch is also closed: the current file is 9515f944ddbbf61bb2099324ac6825fd7603262adc605686c2a932948a81f768, matching the later Windows-qualified manifest and the current worktree.

No other real source defect was found in this bounded audit. Residual items below are qualification limits, not claimed defects.

Current qualification evidence

  • Linux full CI egui037-designcraft-final-characterization-ci-20261025-0050: all 9 steps passed (fmt, pdf-deps, clippy, test, i18n-report, doctest, assets, layers, wasm); 77 result summaries total 3230 passed, 0 failed, 6 ignored. The job's captured unstaged patch is byte-identical to the current tracked worktree diff, so it qualifies the current tracked source. The run includes the corrected canvas characterization test over three batch shapes and the discard/no-discard matrix.
  • Windows qualification job 102: all 9 steps passed, 3198 passed across 77 result summaries on the 619-path source manifest 5208ddb75ad8d06a48c334b48e5e80dc193ae2b9739b119dd50942aa3ebd5a40. Exactly two test-only Windows fixture paths differ from the applied prior manifest (apps/designcraft-cli/tests/it/cli.rs, crates/engine/src/file_access.rs); the engine production prefix is unchanged. Qualified desktop executable a34478f09ccae2571320b3ada489665adae450031ca24918ec4e8b165edfc47a; qualified CLI 318ec6cb7521f941aaab01a63b2c0f970bd70bfb50e5545aa645f63d1589cbee.
  • Native Windows GUI native-gui-2238: edit, undo, redo, selection, save and reopen were captured with document, UI and window evidence; the native input diagnostic ordered three production click/End replays on the qualified executable, each reaching click caret 6 and End caret 12, with all three text strings unchanged and the document clean. Six screenshots were reviewed, but the state JSON is the exact caret oracle because a blinking caret can make two screenshots share pixels.
  • Native input diagnostic native-order-review0048.txt: no application event loss is shown. The trace records the End press/release pair arriving before the click pair; the app acted correctly for the order it received. The temporary diagnostic build was fully restored to the exact 619-file qualified source and binaries in the VM before the ordered production replays.
  • Exact-source boundary. The Windows 619-path manifest predates the host-only canvas characterization test. In the current tree, crates/ui-egui/src/canvas.rs differs from that manifest only inside mod tests (the first difference starts at line 6016; the test module starts at line 4046), so production bytes are unchanged. The Windows run did not execute that new test; Linux full CI did. This document is amended after the Windows manifest, so the documentation file is also not byte-identical to that earlier snapshot.

Remaining limits and regression risk

The union includes 141 upstream main commits and 127 changed paths, only part of which is egui 0.37 API work. It also imports page history/Go Back, the Story panel, notes and table work, optical-margin options, linked-file read caps, SVG preflight, IDML facing/frame-grid tests, and other current-main features. Review the 180-file tracked diff and the 24 manual overlap resolutions as product changes, not as a mechanical dependency bump; the imported tests reduce but do not remove that risk.

Still unqualified for this exact union: macOS native menus/IME/Apple events, real-GPU rendering and the DirectX 12 startup row, native Linux X11/Wayland presentation, native Windows IME and clipboard/ PRIMARY selection, physical-device input ordering, browser presentation beyond the wasm check, paired performance against the thresholds in §7, and packaging/release artifacts. The Windows VM uses a software renderer. Cross-device QMP command order is not a guest-delivery guarantee; the ordered replay gates input on an observed click state rather than zero-delay host issuance.