107 KiB
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(orcargo test), doctests,assets,layers,wasm.cargo xtask wasm: onecargo check --target wasm32-unknown-unknown --keep-goingover L0–L5 plusdesigncraft-ui-egui.cargo test -p designcraft-ui-egui --libis the package-scoped UI row. It must run with the explicitegui/default_fontsdev feature and withtheme::install_fontsbefore layout.- Clean-checkout dependency gate: after publication, a fresh clone runs
cargo fetch --lockedandcargo metadata --locked --format-version 1against the final storytold revisions. A missing/unreachable revision or a machine-local path fails the gate. - CI workflow
.github/workflows/ci.ymlrunscargo xtask cion Ubuntu 24.04 with pinned craft-fontsabb83316d96aa59c1cf64784289e378fe9fa5695, nextest, and the apt/winit build dependencies. windows-arm64.ymlpackages and installs ARM64;freebsd.ymlruns the workspace in a FreeBSD VM;release.ymluses the shared release workflow.- Manual UI evidence:
cargo run --release -p designcraft -- --sample --control 7979, control requests,ui.screenshot, andcargo run -p designcraft-ui-egui --example ui_shot -- script.jsonlfor offscreen whole-window screenshots. - Native/browser workflows are listed in §6.4;
ui_shotalone is not native focus, IME, picker, clipboard or browser-presentation evidence. - There is no performance benchmark target today.
ui.inspectexposesperf.frameMs,perf.renderMs,perf.fps,perf.patches;perf.frameMsusesnow_ms()(SystemTimeon native,0.0on 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):
- Native
EpiIntegration::update(eframe-0.37.0/src/native/epi_integration.rs:349-361) callsContext::run_ui(raw_input, |ui| { app.logic(ui.ctx(), …); app.ui(ui, …) }). WebAppRunner::logic(eframe-0.37.0/src/web/app_runner.rs:290-293) does the same. A multipass frame therefore calls host and applogiconce per pass, not once per frame. 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.run_dynpassesnew_input.take()to eachbegin_pass; the second pass has no raw events.PointerState::button_clickedscanspointer_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.- After a completed
run_ui,ContextImpl::end_passtakesviewport.output; the returned output owns the completed-pass count andcurrent_pass_index()reads the now-zero viewport output.run_logicdoes not restore a stale index. The earlier comment alleging a leftover nonzero count was wrong. ViewportInfohasminimized/occludedfields and avisible() -> Option<bool>method; there is no publicvisiblefield.visible() == Noneis unknown, and eframe forces a real UI pass forRequestPaintWhileHidden/pendingScreenshoteven when hidden.update_logic_onlystores the prepared raw input inpending_raw_input; the nextupdateprepends 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:
- Every per-frame side effect in
logicruns 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. - While minimized/occluded, eframe normally runs
Context::run_logicand no UI pass. Allctx.inputexcept viewport/window/focus information is stale. A forced hidden paint is different:run_uidoes run, with fresh prepared input, even thoughvisible()may befalseorNone. - Host
App::logiccounts passes inframes; on a multipass frameframesadvances twice, sostartup.presented()and startup error logging happen at the wrong pass. - Input buffered by
update_logic_only, and synthetic control events injected inraw_input_hookduring 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_inboxandopen_os_documentsafter fonts are ready; inbox items are consumed once.handle_quit(freshclose_requestedini.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_ctxcapture;window_maximizedupdate (lib.rs:1106).- One-time CMS activation through
color_applied(lib.rs:1107-1112). ctx.set_zoom_factorfromui.ui_scaleandzoom_with_keyboard=false(lib.rs:1114-1119).- Font installation/reinstallation from
theme::install_fonts,fonts_systemreadiness and the 250 msrequest_repaint_afterwhile the font scan runs (lib.rs:1121-1131). theme::apply/restyle(lib.rs:1132-1135).- Recovery autosave (
lib.rs:1136-1147), changed fromctx.input(i.time)to a monotonicInstantclock 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 thanInstant/now_ms(). - FPS smoothing only on a real UI pass, using a monotonic
Instanton 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/controlctx.request_repaint()while events remain (lib.rs:1160-1162).- Screenshot issue after the 120 ms/3-frame settling rule (
lib.rs:1636-1648). menus::shortcutsmoves out oflogicinto the first-passinput_stagebelow.- Native dropped files move out of
logicintopending_drops, captured inraw_input_hook;logicnever reads stalectx.inputdrops. handle_quitstays inlogic_tickbecauseclose_requestedis 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 inraw_input_hook), not read from stalectx.inputin logic; - no other input event is read twice.
canvas::showand 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_filescapture intopending_dropsand removal from the raw input, so a hiddenrun_logiccannot re-open the last drop; - a focus/document generation snapshot on each hidden logic tick;
- a filter that drops buffered
Text/Ime/Pasteevents 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 withPointerGone. 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.screenshotalready forces a paint through theScreenshotviewport command; do not add a secondRequestPaintWhileHiddenfor it. Still sendFocusas today.ui.dialog.confirmand other methods whose result depends on layout follow the same pending-gesture rule.- The control reply may be
okbefore 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_shotsrecord owns onecraft_ui::capture::CaptureSlot(provisional API; final name/API after CL-2F). The slot holds the request id, targetViewportId, deadline, a weak wake handle and at most oneArc<ColorImage>. issue_screenshotssendsctx.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 aWeakreference to the slot. It must not captureegui::Contextstrongly (no context self-retention cycle), must not encode PNGs, write files, run commands, hold a long lock or borrow mutable app state.ScreenshotCallbackis 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; userequest_repaint_of(viewport)from the live slot, not an implicit current/default viewport. Release the slot lock before waking. drain_screenshot_resultsruns inlogic_tick, matches the slot record, takes the image out under a short lock, then callscontrol::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::ROOTforui.screenshotuntil 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 handlesPreedit,Commit,DeleteSurrounding { before_chars, after_chars }.delete_surroundingloopstext.deleteup to 64 times (canvas.rs:2695-2701), and eachtext.deleteis 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 singleedit), that deletes the exact UTF-8 ranges around the caret in ones.editcall, producing one undo entry. Do not make this optional and do not approximate bytes as characters/text.deleteloops. - 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, astext.deletedoes. 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
DeleteSurroundingcharacter behavior. - E21b/P7b: add the bounded outgoing surrounding-text payload to
IMEOutput, the built-inTextEditproducer 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 toNoneas a placeholder. Audit everyIMEOutputconstructor/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-237forcesegui-winitwithfeatures = ["clipboard"]even with eframe defaults off. 0.37's eframe exposesclipboard = ["egui-winit/clipboard"]and depends on egui-winit withdefault-features = false(eframe-0.37.0/Cargo.toml:60,239-241). Without the feature,egui-winit-0.37.0/src/clipboard.rs:113-141storesset_textonly in-process. DesignCraft's Copy/Cut callsctx.copy_text(crates/ui-egui/src/lib.rs:806-824), and its focused-field Paste path sendsViewportCommand::RequestPaste(menus.rs:2717). - Enable
eframe/clipboardin 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-ownedinstall_clipboard_guardso control actions never write or read the user's clipboard, and keepServices::clipboard_textfor 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
TextEditpaste 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 regularwl_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::PasteImageis 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:
chrome::app_bar(Panel::top("app_bar"))- optional
chrome::control_bar chrome::status_bar(Panel::bottom("status_bar"))toolbar::show(Panel::left("tools"))dock::show/panel_docking::show(Panel::right("dock"),Panel::right("shared-panel-dock"), icon columns)CentralPanel::default()last among panels- second-window
show_viewport_immediate - floating panels/flyouts, story editor, dialogs/about/palette (areas/windows/modals)
- 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/ec8840atrees as a base. They are stale relative to current4919088, omitcrates/platform, and revert current security/settings/docking/printer fixes (including floating-group move/resize guardsd66e345/fd9cf54). - Apply only the API-port hunks from
8614e4a(and relevant README changes from1764bca) onto current4919088, preserving intervening fixes,crates/platformand unrelated crates. Review a diff against both5c874d51(the current DesignCraft pin) and4919088; pin the exact resulting immutable revision. WidgetType→Roleinbuttons/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
DockAreaAPI 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 callsLeftToRightexplicitly 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::newand theFontData { index, tweak, ..from_owned }constructions are source-compatible with 0.37'sFontData; only test assertions that readdefinitions.font_data[name].fontordata.as_ref()needdata.bytes()/font.as_ref().- Set
NativeOptions::system_font_fallback = falseexplicitly and keep eframe'ssystem_fontsfeature off. This preventsegui_system_font_providerfrom adding fonts ahead of DesignCraft's script-specific order and makes the policy visible in code. Document that DesignCraft's ownFontDbprovides system fallbacks. FontDefinitions::default()is empty whendefault_fontsis off (epaint-0.37.0/src/text/font_definitions.rs:111-117). Addegui/default_fontsexplicitly to thedesigncraft-ui-eguidev feature set and install DesignCraft's fonts before layout in package-scoped tests, together with explicitmonochrome_emoji_fontswhere 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 useHarnessBuilder::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 inassets/fonts/; central craft-fonts remains an optionalCRAFT_FONTS_DIRinput; 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
i18ncatalog string and every UI font family, usingFontDbfaces, not egui'shas_glyphalone (it can report false for the face that supplies the replacement glyph). Gate craft-fonts-specific CJK expectations onCRAFT_FONTS; when absent, assert the built-in coverage and skip CJK glyph assertions explicitly. Existing Ukrainian/Russian tests intheme.rsare 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::labelpass logicalLayoutJobs (with explicit language/direction where needed) tofonts.layout_job; remove the manualBidiInfo::visual_runsreordering, section reversal, neutral-character mirroring, Arabic family swap and per-rowrow.pos.x += width - row.size.xright-alignment inrtl.rs. Keeping them would process Arabic/Hebrew a second time in epaint'sBidiLevels::new(the fork'stext_layout.rs:120always runs bidi;LayoutJob::directiondefaults toAuto). The document composer's owncrates/compose/src/bidi.rs/shape.rsremains 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.textas 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
TextEditpaths 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/labelviaResponse::accessible_nameorWidgetInfo.widgets::NumField: add a label field or.accessible_name; do not leave the internalidas the only name.dialogs::text_field: pass the visible label id and useui.label_inputs_by(label.id, …)orui.name_inputs(tr(key), …); section labels added as plainui.labeldo 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.rsandobject_control.rs. Map: swatch/color well →Role::Buttonwith the swatch name/value; layer row →Role::ListItem/Role::Buttonmatching 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::DockAreaalready names tab/overflow/resize controls after CL-1/CL-3; keepRole::Tab,Role::Button,Role::Splitterand labels stable.Response::labelled_byis preferred when a visible label exists;Response::accessible_namefor icon-only controls;Ui::name_inputsonly 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_sizeandCornerRadiusintheme::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_barheights.
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.lockhas exactly oneegui,epaint,ecolor,emath,eframe,egui-winit,egui-wgpu,egui_kittestandwinitentry.- Every egui-family source is
git+https://github.com/storytold/egui?rev=<FINAL...>#<FINAL...>; noregistry+.../egui-0.36.2or-0.37.0remains and no candidateaionggsource is shipped. winitsource is the exact final storytold revision; no registry 0.30.13 and no old3d51077f...copy remains.wgpustays30.0.1;accesskitmoves to the version egui 0.37 requires (0.25.1in the reference graph).cargo metadata --locked --format-version 1resolves the same graph as the lock.cargo tree -dshows no duplicate egui-family crate and no duplicate winit. A duplicateaccesskit_consumerfrom unrelated crates is a warning to inspect, not an automatic failure.cargo xtask layersstill reportsdesigncraft-ui-eguias the only egui consumer below L7, andcargo xtask wasmincludesdesigncraft-ui-egui.cargo xtask cipasses with the exact lock and with and withoutCRAFT_FONTS_DIRfor the font tests that support both.- Clean checkout:
cargo fetch --lockedandcargo metadata --locked --format-version 1succeed 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-uiproduction source compatible with stock egui 0.37.0; - expose the same docking transaction model and
DockAreaAPI 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=1and a dedicatedDESIGNCRAFT_CONFIG_DIR, or an explicit empty override, so preferences and recovery do not leak between runs. - Build with the pinned craft-fonts ref
abb83316d96aa59c1cf64784289e378fe9fa5695for release/font evidence, and also withoutCRAFT_FONTS_DIRto 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 themesmediumDark,light,highContrast. Record the exact fixture, font directory, DPI and OS/renderer with every screenshot. - Use
CRAFT_UI_DOCKING_FIXTURESfor 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/finishwith 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::MenuItemorRole::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_byrelationship or aplaceholder; a blank label is not an acceptable name. Numeric fields exposeRole::SpinButtonand 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::Dialogor a namedArea; 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
Rectcomparisons 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.newvs.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.ps1through the scheduled-task helper. Drive control-channel edit/save/reopen, dock/float/popout andui.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 andui_shotare not browser presentation evidence. - Keep software-renderer and real-GPU results separate. Capture native screenshots with
ui.screenshotor a native window capture;ui_shotis offscreen only.
6.5 Deliberate broken-behavior probes
Before accepting the suite, temporarily break each behavior and confirm the test fails:
- Remove an
accessible_namefrom a toolbar button -> accessibility test names it. - 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. - Call
ctx.root_uiinsideDesignApp::ui-> test panics with the documented recursive borrow message (run in acatch_unwindprobe, not production). - Remove the first-pass
logic_frameguard -> forced-multipass test sees two logic-frame ticks. Do not usevisible()to "fix" it. - 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.
- Drop
DeleteSurroundingByteshandling or fall back to the 64-steptext.deleteloop -> exact-byte/one-undo test leaves replacement text or extra undo steps. - Remove the E21b surrounding-text adapter or emit display-bidi text -> logical/bounded outgoing-text test fails.
- Restore
rtl.rs's manual reorder alongside fork E5/E12 -> RTL differential test shows double processing/double alignment. - Restore
Id::new/Ui::id->clippy -D warningsfails, proving the warning gate. - Reintroduce
Event::Screenshotscanning or a strongContextcapture in the callback -> compile/test or cancel/shutdown test fails. - Move web drop reading back into
WebShell::logic-> browser drop test fails. - 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.lockSHA-256, feature list,CRAFT_FONTS_DIRand 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 (
releaseunless 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
- Startup: at least 20 fresh processes; measure wall/
Instantfrom 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. - 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.
- 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.
- 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.
- Text tessellation #8731: fixed 120-label ASCII scene plus a mixed/color-glyph
control; time
Context::tessellateseparately 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. - #8736 DX12: Windows clone; time renderer/pipeline/shader construction separately from first frame, with predictable filtering disabled/enabled as applicable. Record adapter and driver.
- #8512 fonts: cold process with bundled fonts, with
CRAFT_FONTS_DIRand 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. - #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.
- #8748 hidden child: open/update/close the second viewport while the parent is hidden/occluded/overdue; record first visible child frame and close acknowledgement.
- Screenshots: visible and hidden
ui.screenshotrequests; record reply latency, file size, retained bytes, cancellation/timeout and late-callback cleanup. - Memory: RSS after sample load, after 10 pan cycles, after 30 screenshots and after a drain/shutdown; retained image bytes must return to zero.
- 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.rsduplicate 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 completee74208ddsource/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
- Final dependency publications. The final reviewed
storytold/eguiandstorytold/winitrevisions do not exist yet. Candidateaiongghashes 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. - craft-libs CL-1/CL-2F revisions. The 0.37-compatible craft-ui revision must be the
8614e4aport onto current4919088, preservingd66e345/fd9cf54,crates/platformand unrelated fixes. PR199'sb68de8c/ec8840atrees are not bases. The capture API freezes only after the DesignCraft/GridCraft adapter review. - Clipboard/PRIMARY. D1 enables
eframe/clipboardto 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. - Windows GPU fallback. E2/E3 turns some egui-wgpu configure/renderer failures into
WgpuErrorvalues rather than panics. DesignCraft'sgpu::finishalready handleseframe::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. - macOS native evidence. macOS menu shortcuts, press-and-hold IME,
deleteSurroundingbytes,movable_by_window_background, Dock Quit and viewport screenshots need real hardware or remain explicitly unresolved. A software/VM screenshot is not native evidence. #8731text 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.#8736DX12 startup. The shader-compile cost is a Windows startup risk. Measure it separately and do not hide it inside a UI frame average.#8748hidden-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.- 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. - Text selection #8763/#8764. The source audit finds no control that reads an egui
TextEditselection 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. - 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;
Noneplaceholders are not acceptance. Native Wayland IME trace remains required. - DockArea direction. CL-3's opt-in direction is a visual policy, not a schema change.
DesignCraft passes
LeftToRightexplicitly for now; saved physical splits/rectangles and logical tab order must remain unchanged. No display-bidi text reaches shaping or clipboard. - Platform capacity/qualification. The parent can see i915 PCI
8086:3E96and/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. - Capture API freeze. CL-2F must be reviewed after both adapters. A strong
Contextcapture, unbounded pending images, a late file write/reply or a lock held across wakeup is a blocking failure, not a follow-up. Cargo.lockcoherence. Regenerate, never hand-edit. A lock with both 0.36.2 and 0.37, a candidateaionggsource, 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_kittest0.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.lockmust be regenerated through the managed workflow, not hand-edited.- The
designcraft-testkitworkspace dependency points at a missingcrates/testkitin 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
aionggsource or machine-local path remains. - Clean checkout passes
cargo fetch --lockedandcargo 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::uiroot-Uiflow preserved; native and web per-pass logic tested; no recursiveroot_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
Contextcycle. - 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
Noneplaceholder. - 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/eguicraft/0.37=a4f9a17f64a6bae61350fb85804c9df0d9865916, includingPATCHES.md(E1–E21; E21b/P7b still to be added). This is not the production pin. - Candidate winit for comparison:
aiongg/winitcraft/0.30.13-lang=8b35388bd719e8cc86a95971ba0bc026c9a8f058, includingPATCHES.md(W1–W27; W15P still to be added). This is not the production pin. storytold/craft-libsPR 199 head =b68de8c5f4f1c060904d211a7b1320aa0f7bce78; audit mergeec8840af8c5fda695cf195cdd05e5ae078415c07; API-port source8614e4a04fae516fba16c22b31ff1008cb6be10b; current craft-libs baseline49190884278f16c9a9f5823d7698a8ffb5deb062.storytold/eguipublished 0.36.2 =767b1cad294120689a514bb4bd68c077e46679b9;storytold/winitpublished 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 completedrun_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) andcraft-libs/docs/egui-037-migration-plan.md(§7.7, §7.8, §10). - Font fixtures:
egui/docs/egui-037-migration-plan.md§10.6 (craft-fonts2481a8a, 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.rscrates/idml/src/tests/facing_ltr.rscrates/idml/src/tests/frame_grid.rscrates/ui-egui/src/page_history.rscrates/ui-egui/src/panels/story.rsdocs/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.
InputGenerationandScreenshotGeometrynow compareDocState::identity_epochthroughactive_document_epoch()(crates/ui-egui/src/lib.rs), so afile.revertthat 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 reportsself.enabled == falsewhen the field is disabled, does not open its menu, and dims its chevron;disabled_numeric_presets_are_disabled_and_do_not_openpasses. - The earlier
panels/properties.rsevidence mismatch is also closed: the current file is9515f944ddbbf61bb2099324ac6825fd7603262adc605686c2a932948a81f768, 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 executablea34478f09ccae2571320b3ada489665adae450031ca24918ec4e8b165edfc47a; qualified CLI318ec6cb7521f941aaab01a63b2c0f970bd70bfb50e5545aa645f63d1589cbee. - 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.rsdiffers from that manifest only insidemod 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.