* E37: migrate GridCraft to egui 0.37 Pin the reviewed fork and shared dependencies, adapt input and rendering to the new frame lifecycle, and preserve bounded capture handling and font notices. Add representative accessibility checks and meaningful control labels. Validation: full repository CI passed all ten stages, with 2,249 tests passed, zero failed and one existing timing test ignored. Native workflows and paired performance qualification remain documented separately. * egui037: record final published pins and local qualification * M0.37: fix RTL clipping and preserve screenshot budgets * test: share migrated GridCraft UI harness setup * egui037: document migrated UI and regression contracts * egui-037: align popup test fonts and avoid redundant tooltip probes
98 KiB
GridCraft — egui 0.37 migration plan
Status (2026-10-25): the local migration incorporates main
52fa4406c1ebff8b260a2922472f03aba198b9cf. Canonical pins are egui
6d518fc799681571963842f95b55a6b68431b15a, winit
a517b3846ee8457d84ee4975110bfda6797c44dc, craft-ui/craft-print
25da1314d443171b4adeaad2cd9454b273f35c6f and craft-fonts
b076417b5880bd4154bd22f00695b1d28c6bea83. The regenerated lock SHA256 is
426a766291a8246573bf57e3ab3f5f17ff812487582254a32ff5a2ec0db39f27 and passed the dependency-source audit.
egui037-grid-final-main-ci-20261025-0854 passed all ten stages: i18n-report,
fmt, strict workspace all-target clippy, workspace tests, doctest, assets, layers,
WASM, parity and keytips. Actual totals are 2,463 passed, none failed and one ignored across 35 summaries.
Independent review verified the merged main behavior and the final scroll/accessibility fixes.
The local merge preserves both migration history and the reviewed upstream changes;
final code and dependency bytes match the passing CI. Subsequent status edits are prose only.
Main advanced to 17b9f6ff0125209fe707efc919690632ff49cb0b after that CI run; this tree does
not include it. The pass above therefore does not qualify the later integration, and PR CI must
pass on the combined tree before it is described as validated.
Native/platform/performance and application E21b qualification remain open; canonical
pins and full CI do not imply that every native contract is qualified.
Human review and explicit publication approval are required. Earlier compiler logs,
source anchors, baseline inventories and proposed steps below are historical records;
this status governs current pins and acceptance. Final commit/source/CI bindings are in
final-landing/grid-publication0865 in the migration task evidence.
Scope: storytold/gridcraft only. The shared storytold/craft-libs, egui fork and winit fork are
dependencies of this plan, not files this plan changes. Application document models, commands, undo
semantics, XLSX persistence and UI policy remain owned by GridCraft.
1. Baseline, source identity and targets
1.1 Exact source baseline
| Item | Revision / value |
|---|---|
| GridCraft checkout | /home/codex/work/CraftApps/egui037-migration-20261022/gridcraft-implementation |
| GridCraft baseline SHA | b8771d51fb4da60bd48c13cf313d6d6014dc5e66 (current main merge base for this migration; the earlier ac8da7d4 release baseline is retained as historical evidence below) |
| Workspace version / edition / MSRV | 0.11.1, edition 2024, declared rust-version = "1.95" |
| Lockfile | Cargo.lock, format version 4 |
| Current egui family | selected packages at 0.37.0 from canonical6d518fc7; egui_extras declared at 0.37 but unused |
| Current winit / wgpu | winit 0.30.13 at canonical a517b384; wgpu 30.0.1 |
| Current accesskit / kittest | accesskit 0.25.1, accesskit_consumer 0.39.1, kittest 0.5.0, accesskit_winit 0.34.1 |
| Current skrifa | 0.44.0, shared by epaint and gridcraft-ui-egui |
| Shared UI dependency | craft-ui and craft-print both pin canonical 25da1314; the complete 0.37 UI/capture tree is included |
| Shared capture helper | craft-ui CL-2 provisional capture slot; GridCraft/DesignCraft adapters are the pilot and precede CL-2F. CL-1 and CL-2F are the shared gate for the eleven craft-ui consumers |
| Target upstream release | egui annotated tag 0.37.0, peeled commit 841a328ba2dd8f6996beb7189ebb7fc00e668a2a (2026-10-15) |
| Historical review candidate egui (superseded by current immutable pin) | aiongg/egui craft/0.37 at a4f9a17f64a6bae61350fb85804c9df0d9865916; inspect only, never publish this branch |
| Historical review candidate winit (superseded by current immutable pin) | aiongg/winit craft/0.30.13-lang at 8b35388bd719e8cc86a95971ba0bc026c9a8f058; published base storytold/winit craft/0.30.13 at ffae5198770d7730e718043f035a772b7dbc3b1b |
| Current implementation egui/winit pins | 173cc7375d35dc5fbc5b5d0994fdf787f547c5d1 / ae48c756a3b81125eb796c0cc6dccf60990d1181; full P8/E21b and platform/performance acceptance remain separate, explicitly unqualified gates |
| Stock comparison | egui 0.36.2 49682f8baa058bf49e011035cfbd6e825f88a5ef, 0.37.0 841a328b…; stock 0.37 is a measurement control only, never a production stage or pin |
| Existing shared update | storytold/craft-libs PR 199, branch head b68de8c5f4f1c060904d211a7b1320aa0f7bce78; open and conflicting at inspection |
| Historical diagnostics | /home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/gridcraft.errors |
The existing audit merge ec8840af8c5fda695cf195cdd05e5ae078415c07 is not a base: its
Cargo.lock still resolves egui 0.36.2 while the source expects 0.37. The PR-199 branch itself has a
0.37 registry lock but no fork/winit pin. Neither is accepted evidence for GridCraft.
rust-version = "1.90" is already inconsistent with the current eframe 0.36.2, whose manifest says
Rust 1.95. Cargo only checks the active compiler, so this has not surfaced. The migration must align
the workspace rust-version and CI toolchain to 1.95 (or the higher of the selected fork's MSRV),
because 0.37.0's egui/epaint/eframe manifests declare 1.95.
1.2 Build targets and entry points
| Target | Package | UI path | Notes |
|---|---|---|---|
| Desktop native | gridcraft (apps/gridcraft) |
App::logic / App::ui wrapping SheetApp |
eframe trait: fn logic(&mut self, ctx: &Context, frame: &mut Frame) and fn ui(&mut self, ui: &mut Ui, frame: &mut Frame) (plus raw_input_hook); SheetApp::logic(&Context)/ui(&mut Ui) take no Frame. The split exists on 0.36.2 and is not 0.37-only |
| Web | gridcraft-web (apps/gridcraft-web) |
WebShell::logic / WebShell::ui wrapping SheetApp |
wasm32, eframe web runner, WebGPU with WebGL2 fallback, rfd async picker, no TCP control channel |
| UI library/tests | gridcraft-ui-egui |
SheetApp::logic, SheetApp::ui, examples/snapshot.rs |
85 integration test files (750 #[test] in tests/) plus 153 in-crate unit tests: 903 #[test] attributes in the crate by static count |
| CLI/headless | gridcraft-cli |
none | no egui dependency; must keep building/tests |
| Shared controls | craft-ui (printing only) |
printing::State::show inside dialogs |
GridCraft does not use craft-ui docking/tabs/buttons |
| macOS native menu test | apps/gridcraft/tests/mac_menu_appkit.rs |
AppKit/muda, no egui | must still run on macOS |
| Tooling | xtask |
ci, layers, wasm, assets, parity, keytips |
cargo xtask ci is the repository gate |
There are no child ViewportIds, deferred viewports, docking areas, audio/playback queues, or
nested workspaces in GridCraft. The single root viewport is the only egui window. view.newWindow
is an engine document command, not an egui child window. The plan therefore does not add
viewport routing or docking migrations that GridCraft does not use.
1.3 Historical pre-migration manifest/feature inventory
The following inventory describes the baseline before implementation. The current root adds canonical family/winit patches, both shared-library pins are c488caa9, native clipboard is enabled, and the current lock/pins are recorded above. Baseline absence of patches is not current state.
- Root
Cargo.toml: workspace resolver 3;egui,egui_extras,eframe,egui_kittest,epaint_default_fontspins;skrifa = 0.44; no[patch]section anywhere in the repository. crates/ui-egui/Cargo.toml:egui(workspace, default features),craft-uigit revision,skrifa, Linux-onlyzbus = "=5.19.0", devegui_kittestwithwgpu+snapshot, devepaint_default_fonts,gridcraft-xlsxtest support, wasmweb-sys.apps/gridcraft/Cargo.toml: eframedefault-features = falseplusdefault_fonts,wgpu,accesskit,wayland,x11at baseline; G1 adds nativeclipboardto preserve the 0.36 integration-forced OS clipboard.craft-printgit revision; desktoprfd,arboard(wayland-data-controlon Linux/FreeBSD), Windowspollster, macOSmudaandfmv-macos-events, macOS AppKit dev-dependencies.apps/gridcraft-web/Cargo.toml:eguiworkspace; wasm-only eframedefault_fonts,wgpu,accesskit; rfd, wasm-bindgen, js-sys, web-sys feature list.- All other crates: no egui dependency.
xtask/src/layers.rsalready classifiesgridcraft-ui-eguias the only crate allowedegui/wgpu. .cargo/config.tomlonly aliasesxtask; no rustflags, target overrides or patches.- No
vendor/directory, no[patch]entries, no nested workspace, no examples or benches with egui exceptcrates/ui-egui/examples/snapshot.rs. - CI:
.github/workflows/ci.ymlrunscargo xtask ci(fmt, clippy-D warnings, workspace tests, assets, layers, wasm, parity, keytips). FreeBSD, Nix-evaluation, packaging-lint, release and Windows ARM64 workflows exist; none runs an egui snapshot job today.
The 0.37 graph also changes the egui feature contract:
eguiandepaintno longer enabledefault_fontsby default (#8583).gridcraft-ui-eguimust explicitly requestegui/default_fonts(and updateepaint_default_fontsto 0.37) to make its production fallback policy explicit.egui_kittest's normaleguidependency inheritsdefault-features = false. Only kittest's own dev-dependencies enabledefault_fonts/monochrome_emoji_fonts, and dev-dependencies do not propagate to consumers. Package-scoped kittest/snapshot builds are therefore font-empty unlessgridcraft-ui-eguiitself enablesegui/default_fontsor the harness installs pinned fonts. Inspect the resolved feature graph for each production and test root; do not infer fonts from kittest's dev-dependencies.egui_kittestmust move to 0.37/kittest0.5; itswgpuandsnapshotfeatures remain.egui_extrasis declared but unused; keep it out of the graph or pin it consistently, but do not delete the workspace entry as part of an unrelated cleanup.
2. Historical diagnostics and what they already prove
The audit logs from 2026-10-22 (not current acceptance) list five fork errors in GridCraft:
control.rs:egui::UserDatano longer exists.control.rs:egui::Event::Screenshotno longer exists.dialogs.rs:1969:Role::SelectableLabelno longer exists.ribbon.rs:146:Role::Checkboxno longer exists.theme.rs:282:FontData::fontis no longerCow<[u8]>; it isArc<dyn AsRef<[u8]> + Send + Sync>.
The stock log adds the same font-family API errors at theme.rs 993/1065/1098/1104/1119 (comparison,
to_vec, len, Cow construction). The current source confirms those anchors. These errors are
only the first compilation wall; they do not cover the behavioural work in sections 3–9.
3. Actual incompatibilities and design decisions
3.1 Root Ui, lifecycle and hidden-window logic
GridCraft already uses the correct 0.36/0.37 shape:
crates/ui-egui/src/lib.rs:1148SheetApp::logic(&Context)handles control polling, font readiness, desktop theme, zoom, close guard and the asynchronous file inbox; the screenshot deadline/slot drain moves here in 3.5.crates/ui-egui/src/lib.rs:1270SheetApp::ui(&mut Ui)lays out the title bar, ribbon, backstage, formula bar, status bar, tabs, panes and central grid.apps/gridcraft/src/main.rsimpl eframe::App for Appcallsself.0.logic(ctx)inlogicandself.0.ui(ui)inui;apps/gridcraft-web/src/web.rsdoes the same forWebShell.egui_kittestharnesses callapp.logic(ui.ctx()); app.ui(ui)throughHarness::build_ui_state(for exampletests/interaction.rs:10).examples/snapshot.rsuses the same closure andharness.render().
Do not rewrite this into Context::run_pass/root_ui calls. Context::root_ui panics while
App::ui/run_ui already holds the root Ui (#8535). Panels are already created on the passed
root Ui; CentralPanel::show(ui, …) in SheetApp::ui stays. Tests keep Context::run_ui
(#8535 keeps it as a wrapper over run_pass + root_ui).
Actual lifecycle contract. Both native (eframe/src/native/epi_integration.rs::update) and web
(eframe/src/web/app_runner.rs::logic) call App::logic and then App::ui inside the
Context::run_ui closure, once per layout pass. A multi-pass frame therefore calls logic more
than once. When nothing is painted, eframe calls App::logic through Context::run_logic instead;
that call leaves events, input time and layout stale and must not consume UI input. After a normally
completed run_ui, current_pass_index() is zero even after several passes (end_pass takes the
output and only run_dyn restores the count before the next pass), so it cannot mark hidden work.
ViewportInfo::visible() reports minimized/occluded (None unknown), not the phase: an
issued screenshot command or ViewportCommand::RequestPaintWhileHidden forces a full hidden UI
pass and paint. A screenshot merely queued in logic has not reached that point (section 3.5).
Use consumed queues, request/slot identity and monotonic deadlines, not a visibility flag or pass
index, to decide background work; input handling belongs in ui.
Only these background paths move into logic: the control poll (already there), printing feedback
polling, screenshot result drain/deadlines, autosave, the window title and the existing queued file
inbox. Do not move ribbon_keys, pointer handling, ctx.text_edit_focused() or event-list scans.
Two lifecycle defects must be fixed while the graph changes:
- Autosave currently runs in
ui(lib.rs:1331). Move the decision intologic, keepstore_view_state()/file.savethere, and callctx.request_repaint_after(2 s)while AutoSave is on and the active workbook is dirty and has a path. Keep one monotoniclast_autosavecheck so a forced hidden UI pass (logic called again) cannot save twice. Preserve the existing guards: no cell editor, no text-box editor,.xlsxpath,last_autosave > 2000 ms. - Printing feedback is polled twice per frame, and neither path is
SheetApp::logic.apps/gridcraft/src/main.rs:43callsself.0.poll_printing(ctx)immediately afterself.0.logic(ctx), andSheetApp::uicallspoll_printingagain atlib.rs:1272. Move the one poll intoSheetApp::logic, delete theuicall and delete the desktop app's extra call. The web shell has no extra poll.printing::State::pollandprint_feedback_seenremain idempotent; the direct calls intests/native_print.rs:40,121,124move to a reallogicdrive, or stay as explicit helper tests with a name that says they bypass the lifecycle.
Also move the window-title update (lib.rs:1342) into logic, next to the close/autosave state.
ViewportCommand::Title is valid outside a pass. This makes a control-channel rename while hidden
update the native title when the window returns. Keep last_title in GridState/SheetApp so it
is sent only on change.
Multiple UI passes and synthetic input. Context::run_dyn takes RawInput fields once per
frame, so a second pass sees no pointer/key events and must not replay a click. It can still re-run
non-input side effects. App::raw_input_hook runs once per frame, before run_ui or run_logic,
not once per pass; in a hidden logic-only tick it can run with no UI to consume its events. Do not
dequeue-and-discard a gesture in the hook. Keep one unapplied gesture step in app state and carry it
across repeated hidden logic ticks until a full ui pass consumes it (eframe also retains an
unconsumed RawInput for the next pass); do not offer the next step while one is unapplied, and
keep press and release as distinct steps rather than collapsing them. While a step is unapplied,
logic sends ViewportCommand::RequestPaintWhileHidden every frame (the command holds for one
frame) so a UI pass runs. An issued screenshot command requests paint for its own capture, but it
does not make synthetic input run, nor does it issue itself. Audit SheetApp::ui for
pass-dependent effects:
control::issue_screenshotsruns after every widget but beforecollect_screenshots, autosave and the title update; it is the last UI-only command issuer, so no GridCraft widget can request a discard after it. It issues once per admitted request (section 3.5). Do not gate it onwill_discard(): that query only reports a discard already requested and cannot predict a later one.control::collect_screenshotsis replaced by a logic-side slot drain (section 3.5).poll_printingmoves tologic(above).ctx.send_viewport_cmd(ViewportCommand::Title)andstore_view_state/autosave becomelogiccalls.backstage, dialog and popup state changes are input-driven; the second pass has no events, so they do not repeat.grid::showwritesgrid.rect,grid.geo,grid.cells_rectand scroll state. These are recomputed on every pass from the sameSheetApp/session; a discarded pass may leave values from the discarded layout, but the final pass overwrites them.
The discard-injection hook cannot be a #[cfg(test)] field used by crates/ui-egui/tests/*:
integration tests compile the library as a normal dependency, so cfg(test) is off there. Put the
private force_discard_once field and the discard tests inside a #[cfg(test)] mod in
crates/ui-egui/src/lib.rs, where unit tests can set it and call ui/control directly; keep
integration tests to observable commands without the hook. Tests:
- click a ribbon command, then a hook requests
request_discardafter screenshot issuance in that pass → exactly one journal entry/undo step, one title command, one autosave, and the screenshot request completes exactly once (its command is appended to the combined pass output; no duplicate command is issued); - a forced hidden UI pass after a completed multi-pass frame still runs autosave/printing once and
applies queued synthetic input, while a plain hidden logic tick runs no
ui; - repeated hidden logic-only ticks with a queued gesture retain one unapplied step (press and
release still distinct) and a later full UI pass applies it exactly once; a queued-but-not-issued
hidden/occluded screenshot receives
RequestPaintWhileHiddenand completes once a current size is reported (a minimized request waits for restore or reports its timeout); logiconly (noui) with a dirty autosave workbook → exactly one save after the delay and no duplicate when a forceduipass follows.
3.2 WidgetType → accesskit::Role
Sixteen call sites use the deprecated WidgetType alias:
| File | Sites | Mapping |
|---|---|---|
crates/ui-egui/src/backstage.rs |
5 | WidgetType::Button → Role::Button |
crates/ui-egui/src/border_preview.rs |
2 | Button and Label → Role::Button, Role::Label |
crates/ui-egui/src/dialogs.rs:1969 |
1 | SelectableLabel → Role::Button with WidgetInfo::selected(Role::Button, enabled, selected, label); keep and assert the selected-state claim |
crates/ui-egui/src/grid.rs:3421 |
1 | Button |
crates/ui-egui/src/ribbon.rs |
6 | Checkbox → Role::CheckBox; the rest Button |
crates/ui-egui/src/tabs.rs:73 |
1 | Button |
Use egui::Role (re-export of accesskit::Role), not a local enum. Preserve the existing
selected/enabled arguments and labels; the border-style items in dialogs.rs use
WidgetInfo::selected so their checked state survives. Add contract tests against the actual
controls: Role::CheckBox with checked state on Dark Mode (the only WidgetType::Checkbox site;
the custom-painted ribbon/tab controls are not checkboxes), Role::Button plus selected state on
Format Painter and a selected Backstage row, Role::Button on All sheets and on each sheet tab,
and Role::Label on the border preview. The custom-painted sheet tabs currently expose no widget
info, so G1 gives each one WidgetInfo::selected(Role::Button, …) with its sheet name before
asserting selection. Compilation alone is not evidence of a role change.
3.3 Deprecated IDs
Id::new and Ui::id still compile in 0.37 but warn, and cargo xtask ci denies warnings. Replace
all of them mechanically; the values are identical:
- 46 production
Id::new(occurrences and 10 test occurrences (excludingFontId::new/LayerId::new), acrossgrid.rs(17),formula_bar.rs(6),panes.rs(5),paper_view.rs(4),widgets.rs(3),ribbon.rs(3), and one each incell_pictures.rs,credits.rs,dialogs.rs,floating_pictures.rs,i18n.rs,printing.rs,tabs.rs,text_box.rs. - 9
ui.id()uses:tabs.rs:98,widgets.rs:149,150,246,327,paper_view.rs:470,ribbon.rs:44,grid.rs:867,920(nine lines;widgets.rscontributes four). - No
UiBuilder::idorAsIduses.
Replace with Id::unique and Ui::scope_id. Do not change the source values or add salts. The
debug ID tracker in 0.37 is stricter about unstable IDs; a ui.scope_id().with(...) under a
reordered row is stable only if the salt is stable. Existing salts already include row/column/
document identity; keep them. Add a focus-retention test: focus the name box, switch language,
open/close the comments pane, and verify focus stays on the name box (not on another field).
3.4 Font data API
epaint::text::FontData.font changed from Cow<'static, [u8]> to
Blob = Arc<dyn AsRef<[u8]> + Send + Sync>. FontData still derives Clone and implements
PartialEq/Debug itself; the Blob type has no PartialEq. Tests that want pointer identity
must therefore compare the blobs with Arc::ptr_eq, not rely on FontData equality.
Concrete changes in crates/ui-egui/src/theme.rs:
try_load(theme.rs:244): buildFontData::from_blob(blob, *index)orFontData { index, ..FontData::from_static(bytes) }.font_file(theme.rs:263): cacheBlobrather than&'static [u8]in the existingMutex<BTreeMap<...>>. ReturnOption<Blob>; clone theArcfor eachFontData. This preserves the existing “one file loaded once” memory intent and makesArc::ptr_eqmeaningful.drop_unparseable_fonts(theme.rs:281):font_parses(fd.bytes(), fd.index).font_definitions_with(theme.rs:457) andload_fallbackcontinue to insertArc::new(fd);font_datais stillBTreeMap<String, Arc<FontData>>.crates/ui-egui/src/fonts.rs:355bind_familyusesFontData::from_owned(bytes); unchanged, except any.fontfield access must use.bytes().
Tests in theme.rs must change, not be deleted:
fallback_faces_load_each_file_once: compare!Arc::ptr_eq(&a.font, &b.font)instead ofa.font != b.font.unparseable_font_files_are_rejected_not_fatal:fd.bytes().to_vec(),fd.bytes().len().interface_fonts_are_read_once_and_shared_with_egui: with theBlobcache, assertArc::ptr_eq(&first.font, &second.font)andfirst.bytes() == epaint_default_fonts::UBUNTU_LIGHT. This is stronger than the oldCow::Borrowedpointer check and proves egui shares the bytes.- Keep
drop_unparseable_fontsas the last line of defence. It is not a vendored patch; it is the app's protection against epaint panicking on a corrupt system font (#226), and 0.37 still parses faces duringset_fonts.
Add egui = { workspace = true, features = ["default_fonts"] } to
crates/ui-egui/Cargo.toml (or enable it on the workspace dependency) and update
epaint_default_fonts to 0.37. Without this, FontDefinitions::default() is empty in
font_definitions_with in a production library graph without another font-enabling dependency.
The consumer's egui_kittest dev-dependency does not propagate kittest's own
default_fonts/monochrome_emoji_fonts dev-features, so the test harness must enable
egui/default_fonts itself or install pinned fonts.
3.5 Screenshot callback — exact lifecycle
Current code (crates/ui-egui/src/control.rs):
Shots { token, queued: Vec<(token, at_ms, frames)>, pending: Vec<(token, path, Sender, deadline)> }(control.rs:70).pollenqueues a screenshot request and a pending reply (control.rs:450).issue_screenshots(control.rs:477) waits 100 ms and three UI frames, then sendsViewportCommand::Screenshot(UserData::new(token)).collect_screenshots(control.rs:493) scansEvent::Screenshotevents in the next input, writes/replies, and expires pending requests after 8 s.selection_glideandgriduseapp.shots.in_flight()to freeze animation while a capture is pending.
0.37 replaces both the input event and UserData with a one-shot callback. GridCraft adopts the
shared CL-2 completion slot; it does not add a local SyncSender channel or a second token map.
The selected topology is CL-2 provisional slot API -> GridCraft and DesignCraft adapters against
that exact revision -> CL-2F API freeze after joint review. The slot owns one result and the
request record owns the slot; the screenshot callback holds only a Weak reference to it:
// Sketch of the shared CL-2 ownership, not an existing API.
let weak = Arc::downgrade(&request.slot); // app owns the strong slot and its waker
let callback = egui::ScreenshotCallback::new(move |image: Arc<egui::ColorImage>| {
// Short lock only; never encode, write, reply, run commands or touch app state here.
let Some(slot) = weak.upgrade() else { return }; // canceled/dropped: release image now
let wake = slot.offer(token, image); // one result, replaces nothing
drop(slot); // release the lock before waking
if let Some(wake) = wake { wake.wake(); } // slot-owned UI waker, after unlock
});
ctx.send_viewport_cmd(egui::ViewportCommand::Screenshot(callback));
Design:
Shotskeeps request identity, path/reply routing, settle counter, 8 s deadline and thein_flight()query. Replace thequeued/pendingvectors with one slot per admitted request fromcraft_ui::capture(CL-2 provisional revision); do not add a second channel. The app owns the strong slot and the waker; the slot stores at most oneArc<ColorImage>, records the issuance-time UI logical geometry and geometry epoch, and is released on drop/cancel/timeout. The callback holds onlyWeak, unlocks before invoking the slot's waker, and holds no strongContext; it does not encode, write, reply or request its own repaint. A late callback after cancel or app shutdown upgrades to nothing and drops the image without a file or reply. Correlation is slot ownership plus the request token; callback order is irrelevant and a late image cannot bind to a new request.- Admission is bounded by count and aggregate bytes before issuance, with checked arithmetic,
not just by the number of slots. Adopt the shared capture defaults: 4 outstanding requests,
128 MiB per image and 256 MiB aggregate reserved for in-flight/completed images; an app may
choose stricter explicit limits. Reserve checked
width * height * 4bytes for the expected capture from the current viewport logical size andpixels_per_point, rounded up and capped by the surface/max_texture_sidebefore issuing; reject with a clear “screenshot budget exceeded” reply when count or bytes would exceed the budget, without evicting earlier requests. The reservation and theCaptureSlotper-image bound are the largest imagevalidate_imageaccepts (one pixel of rounding per axis), so the aggregate bound is strict for retained bytes; a viewport with no size yet reserves the per-image maximum and waits for a real size or the deadline instead of being refused as an overflow. Revalidate the geometry immediately before issuing. Record the issuance-time UI logical content/screen geometry and a geometry epoch and bind them to that image; cancel or retry on any intervening resize/DPI/zoom/surface change before save, even if the value changes back to its original. The callback supplies pixels only, so derive scale from the returned image dimensions and the recorded logical viewport, and verify aspect/rounding. On completion reject an oversized, stale or epoch-mismatched image, release its bytes and reservation, and keep retained slot bytes back at zero after drain/shutdown. The renderer/readback transient allocation is recorded separately in peak RSS; a post-callback size check alone does not prove the peak is bounded. issue_screenshotsruns after every widget but before autosave and the title update, so no GridCraft widget can request a discard later in that pass. Issue once per admitted request (one callback, one command);Context::run_dynappends each pass's output before it checksrequested_discard(), so a command issued in a discarded pass still reaches the integration. Settling counts one real pass per request (cumulative_pass_nr, so repeateduicalls in one pass count once) and skips a pass that has already asked to be discarded; a discard requested after issuance can still cost one counted pass, but the image is geometry-validated at drain.request_repaint_afteralone wakeslogiconly, and a queued request has not yet issued its paint-requesting command. Therefore, while a request is queued or settling with a capturable size,logicsendsViewportCommand::RequestPaintWhileHiddenon each frame untilissue_screenshotsruns; once issued, theScreenshotcommand'swants_paintforces the painted frame, so no further hidden paint request is sent. A minimized request has no capturable size and sends none: its hidden full-frame wait cannot reach issuance, and the restore-triggered pass issues it instead. Also keep the 16 msrequest_repaint_afterfrom bothuiandlogicfor completion and timeout.collect_screenshotsbecomesdrain_screenshots(app, ctx, now)called fromlogic(notui). G1 includes the drain, deadline/timeout handling and the basic exactly-one reply, so its own screenshot smoke can pass; G2 owns the adversarial byte-budget/discard stress. For each completed slot: validate request identity, size, geometry/epoch and deadline, take the image once, then encode/write/reply outside the slot lock. A completed request with no live record drops its image without a reply.logicchecks deadlines and replies with the existing error text (control.rs:521), updated if it still tells users to make the window visible (an issued screenshot command paints a hidden window; a queued one reaches issuance throughRequestPaintWhileHidden). It must not hold a slot lock while callingreply.send.- Request IDs use
wrapping_add(1)and skip zero; never index arrays by token. A reused viewport or document does not matter because the slot dies with its request record. in_flight()remains true while any request record has an unfinished slot, so animation stays frozen through a late/queued capture.- If CL-2's stress tests expose a weakness, fix it in CL-2/CL-2F with the DesignCraft adapter in the same review; a GridCraft-local channel is not a fallback.
Tests (crates/ui-egui/src/control.rs unit tests or tests/screenshot_callback.rs):
- Build a
Context, runissue_screenshotsfor a request and inspect theViewportCommand::Screenshot(callback)fromFullOutput; complete it with a 2×2 red image; runlogic; assert one PNG/path reply withwidth/height, and the file bytes decode. - Two requests with distinct slots: complete out of order; each reply gets its own image/path.
- Timeout/cancel/drop: inject a fake clock, cancel one request and time out another, then complete both callbacks; assert no second reply, no file write and retained bytes return to zero.
- Count and byte budgets separately: with an aggregate budget below four 4K captures and a per-image budget below one oversized capture, admission stops on bytes before the count; extras are rejected once with an error; completed/oversized images release their reservation. Assert peak retained bytes, not only slot count.
- Discard after issuance: a unit-test hook calls
request_discardimmediately afterissue_screenshots; assert exactly one command was issued, the capture completes exactly once, and the reply/file is written once. Do not require a post-pass API. - Resize/DPI/zoom: change viewport size/scale between admission and issuance, and again between
issuance and completion including a change back to the original values; assert the geometry
epoch mismatch retries within the deadline or returns the size/geometry error and releases the
reservation; never combine old pixels with current geometry. The identity intentionally keeps
zoom/visibility/occlusion, so a bounded retry failure is acceptable instead of comparing only the expected physical size. - End-to-end
ui.screenshotthroughControlRequest/handlein a kittest harness with wgpu: file and base64 forms, invalid path error, hidden/occluded-window capture with a current size (queued admission sendsRequestPaintWhileHidden, then the issued command paints the hidden window), a minimized request that waits for restore and otherwise times out, and no reply after drop.
3.6 MouseWheel event source
0.37 adds the required source: MouseWheelSource field to Event::MouseWheel (#8665). Every
literal constructor must add source, preserving the test's intent:
crates/ui-egui/tests/it/wheel_zoom.rs: line events usesource: MouseWheelSource::Unknown(simulated wheel); point/page units and theWheel,TrackpadandMomentumsources are now covered, including a gesture with noTouchPhase::End.crates/ui-egui/tests/it/sheet_tabs.rsandtests/it/filter_list_scroll.rs:Unknown.
Production matches (keytips.rs:64, grid.rs:2945) use .. or a wildcard and need no field
change. Add tests for Wheel, Trackpad and Momentum as well, because the 0.37 wheel smoothing
changes (#8662, #8678) and the winit W13 source reporting affect the notch counting in
grid::show (grid.rs:591-637) and Ctrl+wheel zoom. The expected zoom ladder
(grid::wheel_zoom_step) and scroll distance must not change silently. Event::PasteImage and
Event::MiddleClickPaste are new variants; no exhaustive match exists today, but new handlers must
choose explicitly (section 6.4).
3.7 Style and geometry pinning
0.37 changes default widget styling (#8617, #8445): widget corner_radius rises from 2/3 to
4/6, button padding from 4/1 to 8/2, and interact_size.y from 18 to 20. GridCraft already sets
button_padding = (6,3) and interact_size.y = 22 in theme::apply (theme.rs:804, values at
831–832), but does not
set widget corner radii. To avoid an unintended restyle:
- In
theme::apply, setv.widgets.noninteractive.corner_radius = CornerRadius::same(2),inactive = 2,hovered = 3,active = 2,open = 2(the 0.36 defaults), in both light and dark themes. Keep the existingwindow_corner_radius = 10,menu_corner_radius = 8, popup shadow, and all token colours. - Do not rely on
Visuals::dark()/light()defaults for anything GridCraft paints with egui widgets. GridCraft's custom-painted ribbon/grid already set their own radii. TextEdit::horizontal_align(Align)is deprecated (paper_view.rs:489). Replace withTextEdit::align(Align2), preserving vertical top alignment:[LEFT_TOP, CENTER_TOP, RIGHT_TOP][e.section.min(2)]. Do not useAlign2::LEFT_CENTER, which would move the header/footer text vertically.- 0.37 TextEdit changes (
#8628unclipped growth,#8420min size,#8763focus surrender) affect the formula bar, in-cell editor, header/footer editor, dialogs and tab rename. Reviewtests/in_cell_editor_size.rs,in_cell_editor_wrap.rs,committed_text_fields.rs,formula_bartests,page_layout_view.rs,sheet_tab_rename.rsand add a click-away selection regression (section 8).
4. Root-Ui, multi-pass and logic-only acceptance
| Risk | Observable assertion |
|---|---|
| Hidden logic stops | With the window hidden/minimized and AutoSave on, a dirty saved .xlsx is saved once after the delay; control ui.inspect still answers; a plain hidden tick runs logic only (a forced hidden UI pass is covered separately) |
| Duplicate side effects | A unit-test hook requests request_discard after a command and after screenshot issuance: one journal entry/undo step; one title command; one autosave; screenshot completes once |
| Root Ui borrow | No Context::root_ui call inside SheetApp::ui; the passed &mut Ui is used for every panel; a debug build does not panic with the root-Ui borrow message |
| Panel order | Title bar, ribbon/backstage, notice, formula bar, status bar, sheet tabs, pivot pane, panes, central panel reserve space in that order at 1200×800 and 640×420; grid.rect equals the central panel's inner rect |
| Clip/hit agreement | Geo cell positions, grid.cells_rect, scrollbars and ui.cell.click hit the same cells after resize, zoom (75/100/200%), right-to-left sheets and frozen panes |
| Multi-pass geometry | A pass discarded by a Grid sizing pass leaves final grid.geo equal to a single-pass render |
| Logic input | A control ui.key/ui.click while hidden is applied in forced hidden UI passes; one unapplied gesture step is retained across repeated hidden logic ticks with press/release distinct, logic sends RequestPaintWhileHidden until a full ui pass consumes it, and logic never scans Event::Key/Pointer |
Test anchors: tests/interaction.rs (harness shape), tests/interface_scale.rs, tests/host_chrome.rs,
tests/right_to_left.rs, tests/wheel_zoom.rs, tests/scroll_position.rs,
tests/selection_scroll.rs, tests/freeze_handles.rs.
5. Dependency graph, fork and vendoring decisions
5.1 Intended GridCraft graph
For suite validation, GridCraft must resolve one copy of each egui-family crate and one winit:
gridcraft (eframe + egui + egui-wgpu + egui-winit + epaint)
-> eframe 0.37 (approved storytold rev)
-> egui 0.37 (approved storytold rev)
-> egui-wgpu 0.37 (approved storytold rev, E2+E3)
-> egui-winit 0.37 (approved P8 rev: E4/P2b W15P routing + E7/E19 held-key + E21 + E21b/P7b)
-> epaint 0.37 (approved P8 rev, E5/E8/E11/E12/E13/E14/E15/E16/E20)
-> ecolor/emath 0.37 (approved P8 rev)
-> winit 0.30.13 (approved completed rev, W17-W27 + W15P + W27 lifecycle)
-> craft-ui / craft-print (craft-libs approved rev -> egui 0.37 fork)
G1 explicitly enables native eframe/clipboard; verify egui-winit/clipboard and
arboard in the native feature graph. Web features are unchanged. Cross-application
copy/paste must work in the grid, formula bar and in-cell TextEdit, including text from
an independent process and copy back to it; an in-process clipboard is not acceptance.
Add a root [patch.crates-io] in gridcraft/Cargo.toml for every egui-family package GridCraft
actually resolves: egui, epaint, ecolor, emath, eframe, egui-wgpu, egui-winit,
egui_kittest, epaint_default_fonts, plus winit. Use full revision SHAs, never branches. A
dependency's patch section does not propagate; this root patch is required. egui_extras can be
patched too if it stays in the graph, but it is currently unused.
Publication rules:
- No
path =overrides in the published tree; no machine-local patches; no moving branch. cargo metadata --lockedandcargo tree -i egui/-i winitmust show exactly one source per package for native and wasm feature sets.cargo check --lockedmust work in a clean checkout with network access to the public fork.aionggis a review candidate only. The production graph must use the complete reviewed/publishedstorytoldegui P8 release (P2b async W15P routing, P7 held-key pairing, P7b/E21b, P7c's selected #8763/#8748 fixes and justified performance fixes) and the completed winit series (W15P and W27 lifecycle). No stock 0.37 or intermediate P stage is a production candidate. The final SHAs are not selected yet; do not pin a candidate head, branch, old SHA or invented SHA. S2 and G1 stay blocked until the approved full SHAs are recorded in 1.1 and checked in a clean--lockedbuild.- Keep the old coherent set (current
Cargo.lock+ craft-libsd27646a5) as the rollback point.
5.2 craft-libs PR 199 and issue 198
GridCraft consumes only craft_ui::printing today; G1 adds craft_ui::capture (CL-2) for
screenshots. PR 199's role/ID/docking/ellipsis edits are relevant to craft-libs itself, not to
GridCraft's source. Integration decisions:
- Do not rebase or merge PR 199's tree. Its head
b68de8cand the audit mergeec8840aare stale relative to49190884278f16c9a9f5823d7698a8ffb5deb062: they omitcraft-platformand revert security, settings, docking and printing fixes. Port only the API-port hunks from8614e4a04fae516fba16c22b31ff1008cb6be10bplus the relevant README change from1764bcaonto current4919088, preserving intervening commits; gate the result with a diff check against4919088that shows only those intended hunks. - CL-1 must add the winit patch and a publication-compatible check (registry egui 0.37 build of the packaged crate). GridCraft needs the fork pin separately.
- CL-1 and CL-2F are the shared gate for all eleven
craft-uiconsumers at the P8 stage. GridCraft and DesignCraft are the CL-2 provisional pilot; both adapters are reviewed before CL-2F freezes the API. A consumer already bumped to an incompatiblecraft-uirevision must land that bump in the same atomic dependency stage as the egui P8 landing; no mixedcraft-ui/egui graph is acceptable. - Bump
craft-uiandcraft-printto one approved craft-libs revision. Leavingcraft-printatd27646a5whilecraft-uimoves creates two git checkouts of the same repository and an incoherent graph. - Issue 198 is a language-support proposal, not a merged dependency. Do not import its locale or
formatting roadmap. GridCraft keeps its own
i18ntable and formula locale; only consume the fork APIs the suite actually selects (for exampleContext::set_languageon the fork, E17). - If craft-libs'
printing::StateAPI changes in CL-1, updatecrates/ui-egui/src/printing.rsandapps/gridcraft/src/print.rs; the current wrapper already isolates GridCraft from most of craft-ui.
5.3 Vendored fixes and provenance
GridCraft has no vendor directory. Its current root uses canonical egui/winit Cargo patches; those dependency selections do not replace these app-local workarounds, which must be preserved:
| Fix | Provenance | Decision |
|---|---|---|
graphics.rs backend selection, no Vulkan by default, FXC for DX12, GL fallback |
GridCraft #120, #339, #354 | Keep in apps/gridcraft/src/graphics.rs; E2/E3 fork errors make the existing fallback path effective |
theme.rs font parse guard and drop_unparseable_fonts |
GridCraft #226 | Keep and adapt to Blob; add a corrupt-font regression |
Custom text_box.rs wrapper and selection glide |
GridCraft UI | Keep the wrapper, its state sync and selection glide; its inner field is built-in TextEdit and inherits E21b. Do not replace the wrapper with a bare TextEdit |
cell_pictures 64 MiB/4096-entry texture cache, floating_pictures size cap |
GridCraft image work | Keep; verify texture IDs/uploads under 0.37 |
macOS AppKit exit workaround in apps/gridcraft/src/main.rs on_exit |
GridCraft #117 | Keep; re-test on macOS; the 0.37 AccessKit adapter may change the teardown |
fmv-macos-events, muda pins |
macOS file-open/menu | Keep; winit remains 0.30.13 |
Relevant fork patches by provenance:
- E2+E3 (
9dd799ab): surface fit withinmax_texture_dimension_2dand returnedWgpuError::ConfigureSurface/CreateRenderer; provenance is PdfCraft's vendored egui-wgpu (pdfcraft#609, #617, #1042), now in the shared fork. Required by GridCraft #761 and by the existinggraphics.rsfallback/retry logic. Without it, a too-large window or a GL surface timeout can panic before the app exists and the fallback never runs. - E4 (
66bacf23) is only the regular-clipboard half: it initializes arboard, leavessmithay: Noneand routes text through winit; the candidate keeps arboard for PRIMARY. The selected completion is winit W15P: SCTK 0.19.2PrimarySelectionManagerStateon the existing connection/event queue, with per-seat PRIMARY device/source/offer objects and no second regularwl_data_device. E4'sget_primary_text/set_primary_textroute through W15P in the separate P2b stage. GridCraft's own arboard handle is used for HTML (wayland-data-control); plain-text and PRIMARY routing must follow the single-path design in 6.4, with real compositor evidence. - E5/E8/E12/E13/E14/E15/E16 (epaint/egui): bidi shaping/order, TextEdit clipping, grapheme
caret/delete, fallback alignment, line breaking, visual cursor. GridCraft draws CJK/Thai/Hebrew/
Arabic workbook text and uses an app-managed in-cell editor built on
TextEdit; preserve and test. - E7/E19/E10 (keyboard layout): letter keys from Latin layout/physical code. E19 needs winit
W25 (
KeyEventExtLatinLayout). GridCraft's keytips and shortcuts must keep working on Russian/Arabic/Hebrew layouts and web keyboard events. - E17/E18/E20 (language):
Context::set_language,UiBuilder::language, accessible language, widget text language. GridCraft'sUiState.languageshould drive these; mapPtBrtopt-BRrather than the current shortcode()string, and do not duplicate RTL processing in app text. - E21 (
a70470b0):ImeEvent::DeleteSurroundingBytesand exact-byte deletion. GridCraft's cell editor undo transaction must remain one step; pair with W26/W27. - E21b/P7b (new prerequisite, no candidate commit yet): bounded outgoing IME surrounding text in
IMEOutput, produced by the built-inTextEditand sent by egui-winit through W27set_ime_surrounding_texton Wayland. GridCraft's acceptance is the complete shared E21b/W27 contract, not a payload restatement: the owner/context generation is present even when text isUnavailable; an available-empty document is distinct and sent as an explicitset_surrounding_text("", 0, 0)plus commit; an owner/unavailable transition clears caches and commits the disable/enable boundary before the new owner; the final completed pass, including an authoritativeNone, replaces the oldime.or(self.ime)accumulation; a current-epochdoneserial mismatch applies incoming edits once and defers outgoing state until a matching serial, while an obsolete epoch invalidates the batch; the new <=4,000-byte payload extraction must not allocate a full-document clone (existing TextEdit change-detection/undo clones are baseline, not this requirement). Preedit is excluded and cursor/anchor are valid UTF-8 byte offsets; password input never supplies text. GridCraft's formula bar (formula_bar.rs:321-323) and text-box/in-cell editors (text_box.rs:151) use built-inTextEdit, so they inherit the producer; they still need focus, deletion+commit, deletion-only, preedit, reversed-selection, emoji/combining/RTL, empty-vs-unavailable, final-passNone, both serial cases, owner generation, committed clear and two-surface tests. Custom painters that synthesizeIMEOutputwould need their own model adapter; GridCraft currently has none, and must not set the new field toNoneas a port. - E1 (
ViewportHooks): not used by GridCraft (single viewport); do not add a hook. - E6 (
request_focusduring IME): relevant to the in-cell editor; test. - E9 (
Style::right_to_left): GridCraft's sheet mirroring is app-level; do not turn on global RTL UI style. It would mirror the ribbon/dialogs incorrectly. - E11 (vello multithreading): not enabled by GridCraft; no action unless a dependency turns it on.
6. Behaviour-specific refactors and tests
6.1 Commands, undo and persistence
The engine and command registry do not change. Required checks:
- Every UI action still goes through
SheetApp::run/Session::execute; no direct model mutation from the 0.37 API changes. home.bold,cell.set,edit.copy/cut/paste,edit.undo/redo,file.save,file.saveAsandfile.openproduce the same journal and undo labels.- A UI click and the control-channel command produce the same one undo step.
- Programmatic empty params never open a dialog (
UiRequest::Dialogonly from menu-style invocation). - Save → close → reopen a synthetic workbook: values, formulas, number formats, styles, merges,
tables, frozen panes, right-to-left flag, print settings and
UiStateprefs round-trip. Comparesheet.read,document.inspect, and raw XLSX package parts/hashes where stable. - Autosave in
logicwrites exactly once per change after 2 s; saving while hidden does not touch UI state. engine.journalreplay still works; no password appears in journal/errors.
Anchors: crates/engine/src/cmd/* (unchanged), crates/ui-egui/src/lib.rs::run_command,
tests/autosave_persist.rs, tests/interaction.rs, crates/engine/tests/command_sequence_fuzz.rs.
6.2 Fonts, glyphs and text layout
- Font definitions:
theme::font_definitions_withmust still build UI, UI-bold, cell, cell-bold, cell-italic, cell-bold-italic, serif, mono, plus loadable script fallbacks and workbook families. FontDataAPI refactor as in 3.4; keep theBlobcache anddrop_unparseable_fonts.- 0.37
MissingGlyphPolicy: the productionContextdefault is Tofu;egui_kittestdefaults toMissingGlyphPolicy::Panicandcheck_accessibility = true. GridCraft has 76Harness::builder/build_ui_state/build_statecall sites in 73 test files (plus the snapshot example) and no shared harness module, so the default would make CI depend on installed fonts and on names that do not exist yet.egui_kittest's owndefault_fonts/monochrome_emoji_fontsdev-dependencies do not propagate into GridCraft, so the harness must enableegui/default_fontsitself or install pinned fonts. G1 must make the suite green without globally disabling the checks:crates/ui-egui/tests/common/mod.rsis the one harness constructor now used by thetests/itbinary andkeytips.rs. It installstheme::pinned_font_definitions()(egui's bundled faces plus the pinned craft-fonts input, never the host system scan) throughHarnessBuilder::with_context_setup, marks the testSheetAppwithSheetApp::use_prepared_fonts()so the firstlogicpass does not replace the pinned set, and leavescheck_accessibility = trueandMissingGlyphPolicyatPanic. The snapshot example andheader_autofit_fonts.rsdeliberately keep the production/system-font path where the subject of the test is installed-font behavior.- The CI/acceptance font contract is the existing pinned input: set
CRAFT_FONTS_DIRto the craft-fonts checkout (andCRAFT_FONTS_REQUIRED=1for release/acceptance). A minimal image without it runs the deterministic bundled-face harness; the one test that renders every catalog skipsJa/Ko/Zhin that mode and records the gap, rather than relaxingMissingGlyphPolicyor accessibility globally. - Complete all accessible names/roles that the existing test suite exercises as part of G1 (the form fields, Name Box/formula bar, Font Size box, tab rename, dialog labels and any other unnamed input the strict harness reports). G3 adds the remaining dialog/grid/popup names and semantic assertions; it does not own "make G1 green".
allow_missing_glyphs()orwith_accessibility_check(false)is not a suite-wide default. A specific test may opt out only with a recorded reason, an owner-facing follow-up gate and a comment at that call site; it must not be hidden in the shared constructor.tests/symbol_glyphs.rsis the installed-face coverage test and is strict in both modes (below); do not weaken it.- Do not change production font policy from Tofu.
- Fonts come from pinned, OFL-licensed faces in
storytold/craft-fonts, never from a copy in this repository:BIZ UDPGothicRegular/Bold (Jpan,Latn),NanumGothicRegular (Hang,Kore),Noto Sans SymbolsRegular andNoto Sans Symbols 2Regular (Zsym).crates/ui-egui/src/craft_fonts_policy.rsis the single policy the build script and the tests share: exact family/style plus manifest script tags, the barred-family filter (Adobe*,Source Han*,Source Sans*,Source Serif*,Noto Sans CJK*,Noto Serif CJK*, with no broadNoto Sansprefix that would reject the permitted Symbols faces), the required-face check and the web allowlist.CRAFT_FONTS_DIRis the only input (no fixture path lives in Cargo configuration), and without it the generated table is empty and the build is unchanged.CRAFT_FONTS_REQUIRED=1makes a missing checkout, manifest, row, font file or required face a build error. - Two symbol modes, each asserted exactly:
- no-font/default: bundled faces must supply
↘ ◀ ↑ 📋and the rest of the interface set.✕,✓and⌃have no bundled egui face, so chrome paintsIcon::Close/Icon::Checkand the Control-key tooltip falls back to^; workbook text still refuses to fake a glyph. - extended acceptance (
CRAFT_FONTS_DIR,CRAFT_FONTS_REQUIRED=1): the exact union↘ ✕ ◀ ↑ 📋 ✓ ⌃in bothProportionalandCELL, plus the Korean interface labels, must render; a loaded or required set that lacks a face fails instead of skipping.
- no-font/default: bundled faces must supply
- Native builds embed every non-barred row of
fonts/manifest.txt, not only the five required faces: permittedBIZ UDMincho,Shippori Mincho,Noto Sans Arabic,Noto Sans Georgian,Noto Sans HebrewandNoto Sans Thairows are also present when the pinned checkout carries them. All are OFL/non-barred, so the asset policy holds; the policy and size gate must describe the actual set, not claim five faces. - Simplified Chinese is a native-only opt-in: the OFL
LXGW WenKai Literow from craft-fonts'fonts/simplified-chinese-manifest.txt(the face DeckCraft uses). It is outsideWEB_ALLOWEDand never enters the wasm bundle;CRAFT_FONTS_REQUIRED=1native builds require the manifest, while ordinary builds tolerate an older checkout without it. System Han faces still lead the chain when present. The native font set (including this face) needs its own raw/compressed size measurement; the current measured figure is the five-file web allowlist, 12,970,840 bytes raw. - Web (
wasm32) embeds onlyBIZ UDPGothicRegular/Bold,NanumGothicRegular and the two Symbols faces. Raw font bytes, raw wasm and compressed wasm still need measuring against the consumer's real hosting limit; do not record "within budget" before that measurement. - Font rebuild gate: 0.37 applies
set_fontsimmediately when no text has been laid out this pass (#8602,#8609). GridCraft's one-framefonts_readyskip remains correct but may now skip a frame unnecessarily. Re-test language switch, workbook-font discovery and installed-font rebind; if the extra frame is observable, gate only when text already exists. - 0.37
#8512system-font discovery/provider: GridCraft has its ownfonts::start_scanandthemecandidate probing. Do not switch toegui_system_font_providerwithout measuring startup, preserving workbook font names, and keeping fallback order. This is a blocking comparison, not an automatic adoption. - Text shaping: E5/E12/E13/E16 change RTL rows, caret/selection and grapheme deletion. Add tests for
mixed LTR/Arabic/Hebrew in a cell, visual order, caret at the visual end, Delete/Backspace over a
combining cluster, and formula-bar caret. Use installed fallback faces and skip only when the face
is absent, as
theme::fallback_covers_its_scriptalready does. - E14 fallback alignment and E15 line breaking: the defaults are intended to preserve the old rules, but compare wrapped cells, CJK/Indic fallback rows and vertical alignment against the baseline. A changed fallback baseline must be explained, not accepted as a new golden.
- Screenshot/font fixtures: record the exact font files and hashes used for every visual evidence run. Do not commit font files or screenshots of Microsoft software.
6.3 Accessibility and input
0.37 makes unnamed input widgets a kittest failure (#8589) and improves popup/menu/panel trees
(#8581, #8607, #8608, #8612). GridCraft has many custom-painted controls and several
TextEdits without labels. The names and roles exercised by the existing harnesses land in G1
with the shared strict harness; the broader coverage below lands in G3. Required work:
crates/ui-egui/src/text_box.rs's text-box editor uses the translatedText Boxaccessible name in every catalog; thei18nunit test checks it alongside the Name Box, Cell Editor, Font Size and Sheet Name names.crates/ui-egui/src/dialogs.rs::form_fields: wrap each label + field inui.label_inputs_by(label.id, |ui| …)or call.labelled_by(label.id)on the field. Do this for text, password and number fields.crates/ui-egui/src/formula_bar.rs: give the Name Box and formula bar explicitaccessible_name/labelled_by; keep their existingIds.crates/ui-egui/src/ribbon.rs::size_combo: the TextEdit has no label; add.accessible_name("Font Size")(translated) and keepcontrol_keytip.crates/ui-egui/src/tabs.rs: the tab rename TextEdit gets the sheet name as its accessible name.crates/ui-egui/src/widgets.rs: icon-only controls already useon_hover_text, which 0.37 maps to a description/name; verify withunnamed_widgets()rather than assuming.crates/ui-egui/src/grid.rs: scrollbars, freeze handles, header dividers and fill-handle areas are custom interactions. Give each a role/name only where it is a real control (scrollbarRole::ScrollBar; other clickable overlaysRole::Buttonwith a name), and keep decorative overlays out of the tree. Do not inventRole::Unknownnodes for every painted rect.- Popup/menu roles: 0.37 exposes menu items/submenus; ensure context menus, filter menus, fill menu and cell context menu have correct names and that hidden popups disappear from the tree.
- Focus/keyboard: Tab/Shift+Tab, Enter/Space, Escape and arrow keys must trigger the same commands as pointer clicks; focus must not move when language/theme changes or a pane is hidden.
- IME: preedit, commit, cancel and replacement produce one edit/undo step. Test E21 incoming
delete-surrounding and the complete E21b contract (section 5.3) in the formula bar and
text_box.rs: owner/context generation even when unavailable, available-empty vs unavailable, committed cache clear/disable-enable on owner transition, final-passNoneauthoritative, current-epoch serial mismatch applying incoming edits once while outgoing state is deferred, and obsolete epochs invalidating the batch; plus deletion-only batches, delete+commit, preedit exclusion, reversed selection, emoji/combining/RTL byte indices, focus loss/password clearing and two windows. Native IME behavior (including a Wayland Vietnamese/predictive trace) still needs real Windows/macOS/Linux evidence; E21 alone is not surrounding-text acceptance.
Accessibility acceptance:
Harness::unnamed_widgets()is empty for the main window and each reviewed dialog.- Named inputs expose role, value, selected/checked, disabled and focus states.
- A deliberate mutation (remove one name) makes the check fail; revert it before landing.
- Screen-reader native smoke test on Windows/Linux (Orca/AT-SPI) if available; otherwise record the gap.
6.4 Clipboard, PRIMARY and files
- One routing design for two clipboard owners. GridCraft's
Servicesowns an arboard handle for HTML and plain text (apps/gridcraft/src/main.rs:150-163;wayland-data-controlon Linux/FreeBSD), while egui/egui-winit owns the text clipboard used byTextEditcopy/paste. After E4/W15P both must not compete for the same Wayland selection:- Grid copy/cut keeps using the one arboard data-control handle, because winit's selected
clipboard API has no multi-MIME HTML offer; it publishes HTML+plain text. TextEdit copy/paste
and PRIMARY go through egui/egui-winit (and W15P for PRIMARY). GridCraft must not create a
second arboard handle or a second
wl_data_devicefor the plain-text path. - Paste reads HTML first through arboard, then the current plain text (arboard or the egui clipboard), with one defined owner order and no stale re-read. A grid copy followed by a TextEdit paste, and a TextEdit copy followed by a grid paste, must both work on Wayland.
- Create the arboard handle lazily and keep it owned by
Servicesfor the app's lifetime; record which protocol each path uses, and prove that an HTML paste from a browser and a plain-text paste from another app each work after the other path has taken ownership. - Password fields never publish text or PRIMARY; no unchanged-selection reclaim; canceled/slow transfers must not leave a half-published owner.
- Grid copy/cut keeps using the one arboard data-control handle, because winit's selected
clipboard API has no multi-MIME HTML offer; it publishes HTML+plain text. TextEdit copy/paste
and PRIMARY go through egui/egui-winit (and W15P for PRIMARY). GridCraft must not create a
second arboard handle or a second
- PRIMARY (W15P):
#8428makes Label/TextEdit selections publish PRIMARY and middle-click paste into a TextEdit. The contract is the winit bounded-transfer one: a fresh triggering serial from either a pointer press/release or keyboard input on the focused seat (never a keyboard-only serial rule and never an arbitrary first seat); synchronous ownership claim on that input turn while only later serving/reads are asynchronous; bounded async reads with a 2 s total deadline, 16 MiB cap, at most 4 read workers, 2 writer threads and queue depth 8; a live owner-specific completion waker; canceled or stale results discarded; password fields never publish. Test: select formula text → middle-click in the in-cell editor at a position → text is inserted there, one undo step; pointer and keyboard triggers, focus movement, owner/seat changes, empty/absent offers, self-paste, trickle/oversized/canceled transfers and event-loop responsiveness; non-PRIMARY platforms unaffected. Native gates are wlroots/Hyprland, KWin and Mutter, not one compositor only. Event::PasteImageandEvent::MiddleClickPasteare new. GridCraft deliberately ignores both in the grid: there is no image-clipboard pipeline or cell-picture insert-from-clipboard command, and inserting at an unchosen anchor/size would be a new feature. A clipboard with a text representation still arrives asEvent::Paste; PRIMARY middle-click paste belongs to egui's TextEdit. The match arms are explicit and commented, so a future image-paste feature must choose behavior rather than inherit a silent no-op.- File dialogs (rfd), drag-and-drop and
fmv-macos-eventsare native workflows; kittest cannot prove them. Use the Windows clone and a macOS session with real evidence. The web picker uses the async inbox; test cancel/retry with two dialogs so an inbox cannot cross-deliver.
6.5 Viewports, GPU and screenshots
- One root viewport only. No
show_viewport_deferred, no child-window title/focus/close tests. ViewportCommand::Screenshotis the only viewport command affected by the callback change; the other commands (InnerSize,Focus,Close,CancelClose,Title,StartDrag,Maximized) stay.- GPU: preserve
cell_pictures/floating_picturestexture caches. 0.37's texture manager andmax_texture_sidebehavior must not change image sizes or leak textures. Test texture IDs, upload deltas, eviction at 64 MiB, images larger thanmax_texture_side, and drop on workbook close. - E2/E3: verify the Windows fallback still triggers on a failed renderer/surface. The fork's
surface_fittest is a dependency test; GridCraft adds a visible large-window/resize workflow on the owned Windows clone where the adapter limit permits it. - 0.37 color emoji changes texture sampling. Keep
TextureOptionssemantics for cell pictures and icons (nearest/linear, wrap mode); do not accept a clamp-only optimisation as a replacement for Repeat/MirroredRepeat. - An issued screenshot command requests a real paint of a hidden window: the candidate marks
ActionRequested::Screenshot(andRequestPaintWhileHidden) aswants_paint, and eframe treats that as visible for painting. But issuance is UI-only, so a hidden window must first be made to runui: while a request is queued or settling with a capturable size,logicsendsViewportCommand::RequestPaintWhileHidden; only then doesissue_screenshotsemit the paint-requestingScreenshotcommand, after which no further hidden paint request is needed. A hidden or occluded window that still reports its current content size can complete this way; a minimized window has no capturable size, so it sends none and its request stays queued until restore and otherwise reports the 8 s timeout. Do not reuse an old rect for a minimized window: issuance and drain fail closed on the fresh geometry. Do not assert a hidden/occluded screenshot timeout. Test the real path: issueui.screenshotwhile hidden/occluded and assert one image/reply; then minimize, issue another request, restore, and assert it completes (or, without a restore, assert the documented timeout). Useui.focusonly when focus itself is part of the workflow. A timeout covers "no frame was presented" (event loop stalled/closing) or "no capturable size", not "window hidden". Update the existing error text if it tells users to make the window visible. Synthetic input also runs only in a UI pass and follows the one-unapplied-step rule in section 3.1. - Capture identity stays strict across resize/DPI/zoom/visibility/occlusion. Keeping
zoomand the occlusion flags in the identity is intentional even when the expected physical size is unchanged: a bounded retry (two attempts) followed by an explicit error is preferable to combining pixels from different layout generations. This bounded failure is accepted behaviour, not a relaxation target. - Native qualification for a real hidden/occluded capture and a minimized/restore request remains required. This plan describes the intended behaviour; no native hidden/minimize/restore run has been performed for this source.
6.6 Printing: keep the existing synchronous construction, measure it
SheetApp::submit_print runs file.print synchronously (lib.rs:595-611), and the engine builds
the whole PDF in build_pdf before returning bytes (crates/engine/src/cmd/print.rs:1600-1621).
This is existing baseline behavior, not a 0.37 regression: opening Print settings is lazy (a menu
file.print opens the backstage when a native printer is configured), while the Print button and
ui.printSubmit block the UI while the PDF is generated and handed to the worker. The migration
does not move PDF generation to a background engine unless new behavior requires it or a
predeclared budget says so. Required gate:
- Measure the three phases separately on the same workbook: settings-open latency (no PDF), PDF
generation (
file.print/build_pdf) and submission (printing.begin/worker hand-off), plus the frame that contains them. Use the 100k-row fixture and a representative print area. - Define the candidate acceptance as no regression against the recorded baseline within 9.3's thresholds; a regression is fixed in the print path or explicitly budgeted and reviewed.
- Test a hidden/minimized print submission and feedback polling once through
logic; print discovery/worker completion must not depend on a paintedui. - If a future requirement moves generation off-thread, it needs its own command/undo/worker ownership design, cancellation and a regression test; it is not part of this migration.
7. Native, web and platform workflows
7.1 Native Linux
- Build/test via
task.py run --kind buildon a fresh allocation; record source SHA, lock hash, feature set, rustc, font package versions and evidence path. - Native measurement is staged, not assumed: (1) managed build/test and offscreen/kittest
render, (2) provisioned X11 with a named window manager or Wayland with a named compositor,
(3) interactive workflow with real input. No
DISPLAY/WAYLAND_DISPLAYis inherited on this server, and the host's i915//dev/dri/renderD128access is availability evidence only, not a qualified display or GPU backend. Record the chosen backend and driver per row. - In the provisioned session run
gridcraft --sample sales --control 7979, drive the control channel, take native screenshots, exercise wheel/trackpad, Ctrl+wheel zoom, clipboard/PRIMARY, IME (if installed), file picker cancel and window resize. Record compositor and scale factor. - FreeBSD: run the existing FreeBSD workflow/row; verify the Linux/FreeBSD
arboardwayland-data-controlcfg and winit configuration do not change the build or clipboard behavior. - Software renderer (llvmpipe) and real GPU are separate rows; neither is accepted until measured with identified adapter/driver.
7.2 Windows
- Allocate one uniquely named
craft-windowsclone; use its own SSH/VNC ports. Check allocator capacity first: a full copy needs its reservation, and an overlay is allowed only with an adequate budget. If neither fits, the row stays blocked and no existing VM is stopped or moved. Build the exact task checkout with a separateCARGO_TARGET_DIRand runcargo test --workspace(or the repository's build/test script for the checkout) throughguest-exec.py, submitting each guest command withtask.py run --kind vmunder the clone's task ID. - Use the interactive scheduled-task launcher for the GUI; verify the app is on the active console
with Explorer. Drive
--sample sales --control 7979, edit/save/reopen, open a password-protected fixture, use the printer settings (discovery may have no printer), resize/minimize, and capture screenshots. - Measure renderer construction separately from first frame because of
#8736(FXC DX12 shader compile). GridCraft forces FXC, so this is on its default path. Time a fresh-processegui_wgpu::Renderer::newprobe (or thewgpu_core=tracebracket betweencreate_pipeline_layoutandcreate_render_pipeline) separately from the first presented frame; record both. - The baseline VM is software-rendered (Microsoft Basic Render Driver); report it as such, not as a physical-GPU result. Keep the Windows ARM64 packaging workflow in the matrix; it is not a substitute for a native x64 GUI/IME run.
7.3 macOS
- Build/test on a real macOS host with the macOS CI gate; cross-compilation is not acceptance.
- Cover AppKit native menu,
fmv-macos-eventsdouble-click/Open With, close/quit with unsaved work,on_exitworkaround, Command shortcuts, IME (Korean/Japanese/Chinese), trackpad wheel/momentum, Retina scale 1x/2x and Metal/wgpu. This environment is not available on the server; record it as a rollout blocker until a real run exists.
7.4 Web
- Staged browser evidence: (1) wasm compile in CI, (2) a pinned headless browser run where a pinned Chromium/Firefox is provisioned, (3) interactive WebGPU/WebGL2 with recorded browser and adapter identity. No host browser is inherited at planning time; an unavailable runtime is an explicit blocker, not a compile-only pass.
cd apps/gridcraft-web && trunk build --releaseandtrunk serve --release; test in at least Chromium and Firefox with WebGPU where available and?webglforcing WebGL2.- Workflows:
?sample=sales, edit/save/download/reopen, browser file picker cancel/retry, dropped file, paste HTML/plain text, language switch (including CJK tofu behavior), interface scale, resize, and keyboard layout (E10). cargo xtask wasmonly proves compilation. Browser behavior, IME and clipboard require a real browser run; record versions and screenshots/logs.- Record raw and compressed
.wasmsize; the 0.37 family may change code size.
7.5 CI and commands
Baseline and candidate commands (all new jobs use maintenance/task.py run/status/finish, a unique
--task, and a fresh --job-id). Example for the repository gate:
python3 /home/codex/work/maintenance/task.py run \
--job-id gridcraft-egui037-ci-01 --kind build --task gridcraft-egui037 \
--source /home/codex/work/CraftApps/egui037-migration-20261022/gridcraft \
--evidence /home/codex/work/ssd2/task-evidence/egui037-migration-20261022/gridcraft \
-- bash -c 'cargo xtask ci'
Inside that managed job (never as a direct host command), the candidate checks are:
cargo xtask ci
cargo test --workspace --locked
cargo test -p gridcraft-ui-egui --all-targets
cargo check -p gridcraft-web --target wasm32-unknown-unknown --locked
cargo run -p gridcraft-ui-egui --example snapshot -- --sample sales out.png
cargo tree -i egui
cargo tree -i winit --target all
cargo metadata --format-version 1 --locked
cargo xtask ci already runs fmt, clippy -D warnings, workspace tests, assets, layers, wasm,
parity and keytips. Add a dependency-identity check to CI (a small script or xtask step) that fails
if any egui-family package or winit comes from a second source/revision, or if a branch/path patch
exists. Test the check with a deliberately wrong pin.
8. User-observable regression assertions
8.1 Geometry and hit testing
Use Geo::new, grid.rect, grid.cells_rect, screen_cell_rect and ui.inspect:
- 1200×800 and 640×420, ppp 1.0 and 2.0, light/dark, en/es/ja/zh, RTL on/off.
- Click the centre of A1/B2 and drag C3:F7; selection matches; frozen panes and split panes keep headers fixed; scrollbar thumbs map to the same scroll.
- Ribbon tab/height, formula bar, status bar, tabs and central grid boundaries stay within 0.5 pt of the baseline where the style pin is intended to preserve them.
- Right-to-left sheet mirrors columns and hit tests; outline buttons and ruler drags keep sides.
- Interface scale 75/100/200% keeps
ctx.zoom_factor()and geometry consistent.
8.2 Fixtures
Fixtures are built in code and never committed as large binaries:
Session::new()+file.new {sample: "sales"|"budget"|"grades"}for the three samples.- A 100,000-row × 6-column workbook built through
range.setValuesin 10,000-row chunks, plus formulasG=B*C+D, a chained running sum,SUM/SUMIFS/XLOOKUPand a sort, mirroringcrates/engine/examples/perf.rs. - A two-sheet workbook with a formula being pointed across sheets, a right-to-left sheet, frozen panes, merged cells, a table, a chart, comments/notes, validation and a cell picture.
- Password-protected and corrupt-font fixtures through
gridcraft-xlsx'stest-supportfeature and a temporary file; no password is written to logs or evidence. - 128 small generated PNGs for the picture cache, plus one image larger than
max_texture_side. - Pinned UI text in en/es/ja/zh (and Greek/Cyrillic/Hebrew where a face exists) so snapshots and accessibility names are deterministic.
Record the fixture builder's source hash and the exact parameters in every evidence run.
8.3 Screenshots and pixel probes
- Produce before/after PNGs with
examples/snapshotat 1440×900, scale 1 and 2, light/dark, Home/Insert/View,sales/budget/grades, formula autocomplete (GRIDCRAFT_LOCALE_SCREENSHOT), table headings (GRIDCRAFT_TABLE_HEADINGS_SCREENSHOT) and representative dialogs. - Store them in durable evidence, not as unreviewed repo assets. Record font files/hashes, OS, renderer, DPI and viewport.
- Add a small kittest render test with fixed harness size/ppp/theme that probes known pixels: window background, ribbon fill, selected cell fill, grid line, header fill, selection outline, formula bar background. Use exact RGB for flat tokens and a small tolerance for antialiased edges. The test must fail if a probe is moved by 3 px or a token colour changes; verify with a deliberate mutation, then revert.
- Do not bulk-refresh any golden. Review every changed image and explain it (style pin, font, renderer, or a real regression).
8.4 Accessibility
unnamed_widgets()empty for main window, Backstage, Find/Replace, Format Cells, Sort, Data Validation, print settings and Paste Special.- Query roles/names/values/selected/disabled/focus for: ribbon buttons, tab list, sheet tabs, name box, formula bar, font size box, dark-mode checkbox, dialog fields, OK/Cancel, filter menu.
- Keyboard: Tab order reaches every input; Enter/Space activate the same command; Escape closes the right popup; focus survives language/theme change and pane show/hide.
- A deliberate missing name or wrong role makes the assertion fail.
8.5 Input, IME and text
Event::Text,Key,PointerButton,MouseWheel(withMouseWheelSource::Unknown,Wheel,Trackpad,Momentum), touch pan and pinch zoom.- Wheel zoom ladder (
grid::wheel_zoom_step) at 100/121/99/400%, fine-grained trackpad fractions, Ctrl held vs released, momentum phase; no latch from a missingTouchPhase::End. - In-cell editor: type, Enter/Tab/arrow, Escape, multiline Alt+Enter, click-away commit, formula point mode, F4, autocomplete; one undo step per commit.
- Regression for
#8763: select text in the formula bar, then right-click → Copy (or click a custom toolbar action that reads the text selection); the action must see the selection that existed when the click started (or the app must restore it). The final P8 fork's P7c fixes (selected #8763/#8748) must satisfy this case. Only add a local workaround for a proven app-specific difference, with a test and removal condition; PR #8764 itself is open/unmerged. - Bidi/grapheme: Arabic/Hebrew mixed cells, combining marks, emoji clusters, selection and caret.
- IME: preedit/commit/cancel/replacement in the formula bar and in-cell editor produce one undo
step; E21 delete-surrounding and the complete E21b contract are checked together; a deletion-only
batch, delete+commit, current-epoch serial mismatch vs obsolete epoch, owner-generation change,
empty-vs-unavailable, committed clear, final-pass
None, password clearing and two windows are separate cases. - Clipboard/PRIMARY as in 6.4.
8.6 Persistence and lifecycle
- Edit → save → close → reopen; compare values/formulas/styles/merges/tables/freeze/RTL/print and
UiState/Session.prefs. - Undo/redo after reopen? (Undo history is not persisted; assert it is empty as today.)
- Hidden/minimized autosave; close with unsaved work asks;
app.quitdoes not ask; printing busy cancels close. - Control
ui.screenshotandui.inspectacross resize/minimize/focus; a hidden/occluded queued screenshot reaches issuance throughRequestPaintWhileHidden, then completes from the issued command's own paint request when a current size is reported (no artificial timeout); a minimized request stays queued until restore and otherwise reports the 8 s timeout. - Window title updates on document rename/dirty change, including while hidden.
9. Performance measurement and gates
9.1 Baseline before production change
Record the baseline at b8771d51fb4da60bd48c13cf313d6d6014dc5e66 with the current lock and the reviewed craft-libs base
3832c47; the earlier ac8da7d4/d27646a5 pair stays as historical evidence only. No candidate
numbers may be reported without a paired current baseline. Capture exact SHA, lockfile hash, features, fonts, fixture,
OS, renderer/driver, DPI, viewport, profile, rustc, hardware and load. Use paired runs, alternate
order, 10 warmups and at least 30 timed iterations for steady operations, 20 fresh processes for
startup, and at least 1,000 frames for frame distributions. Report raw samples and the paired
median with a confidence interval for 30-iteration operations; a 30-sample p95 is the 29th
observation and is not a tail estimate, so do not use it as an acceptance threshold. Use p95 from
an expanded tail run (at least 1,000 samples) and p99 only from at least 1,000 frames; report the
sample count with every percentile. The first baseline report and every investigated regression
include an equivalent stock-0.37 control, measured in the same harness alongside 0.36 and the
fork. Stock 0.37 is a measurement control, never a production stage; where an operation cannot
compile unchanged on stock, use a pinned minimal adapter and record the equivalence limit. Also
record peak RSS, texture uploads/allocation counts where practical, and idle CPU/repaint count.
Never report compile duration as UI performance.
9.2 Scenes
| Scene | Operation | Measurement |
|---|---|---|
| Startup | fresh process → first painted frame / control channel ready | wall time, split font setup, wgpu renderer creation, first frame |
| Grid scroll | sales/100k-row synthetic workbook, 1,000 frames of vertical/horizontal scroll |
frame distribution, grid_ms, tessellation time, vertices/indices, uploads |
| Cell edit | type/commit 1,000 cells via control channel | command latency, frame CPU, undo memory |
| Recalc | crates/engine/examples/perf on the same workbook |
engine-only latency, recalc cell count (not a UI number) |
| Dialog/ribbon | open/close Format Cells, switch tabs, type filter | CPU frame time, paint counts |
| Text tessellation | fixed 120-ASCII-label scene + mixed color-glyph row | per-tessellate time/instructions; reproduce #8731 |
| Fonts | 20 cold starts with the same font set | discovery/build/set_fonts time, peak RSS |
| Memory | 100k-row workbook + 128 pictures | peak RSS, texture bytes, cache evictions |
| Idle | visible and hidden, 10 s | logic-tick and painted-frame counts, CPU, no busy loop |
settings open, file.print PDF generation, submission |
three separate latencies plus the containing frame; 100k-row fixture | |
| Windows startup | renderer construction vs first frame | #8736, FXC DX12, software WARP separately |
| Web | release wasm load + first frame + 1,000 scroll frames | raw/compressed wasm, browser frame timing |
A benchmark harness should be added before the migration, not after:
crates/ui-egui/examples/frame_bench.rs: build a deterministicSheetAppwith a synthetic workbook, calllogic/uiinctx.run_ui, then timectx.tessellateseparately; emit JSON samples and vertex/index counts. No GPU required.apps/gridcraftopt-inGRIDCRAFT_FRAME_LOG=<path>: a small logger called from bothSheetApp::logicandSheetApp::uiappends one JSON line per invocation with an explicitphase("logic"or"ui"), timestamp,perf.frame_ms/grid_mswhere available, viewport size, scale andvisible()as data (never as a phase inference); the app addseframe::Frame::info().cpu_usagewhen it has aFrame. Hidden logic-only ticks therefore have samples, and a separate 10 s idle probe records process CPU and tick counts. No behavior change when unset.egui_kittestwgpu render loop for offscreen GPU frame time and texture upload deltas; record adapter info from a smallwgpu::Instanceprobe.
9.3 Thresholds and noise policy
Freeze these before candidate runs (stricter repo budgets win):
- Investigate a repeatable >5% median regression (paired CI above the noise floor excludes noise) in any scene, or >10% p95 only where p95 comes from the expanded tail run; >5% peak-RSS increase; any new idle busy-loop or printing underrun; any loss of an existing absolute budget. A 30-sample p95 is reported but never used as a gate.
- For sub-millisecond measurements, require the relative increase to also exceed 0.05 ms or corroborate with allocation/instruction counts.
- Text tessellation: compare 0.36.2, stock 0.37.0 and the fork on the same scene. If #8731 reproduces (expected from the upstream report), the row-level color-glyph fast path must land in the fork before GridCraft accepts the migration; do not accept a 60% text-tessellation regression as “expected 0.37 cost”.
- Windows startup: measure renderer construction separately. If #8736's reported cost reproduces as a repeatable >5% total startup regression, evaluate the fork shader fix with equivalent wrap arithmetic; clamp-only is not valid. The upstream ~40 ms figure is a report, not our measurement.
- Reject noisy comparisons as inconclusive and rerun; never auto-widen a threshold or tolerance after seeing a failure. Document any pre-measurement exception and its review.
10. Publication stages, dependency DAG and rollback
10.1 Stages
| Stage | Content | Depends on | Rollback |
|---|---|---|---|
| P0 | This plan; evidence notes | none | delete plan branch |
| S1 | Shared craft-libs CL-1 for the eleven craft-ui consumers: port only 8614e4a API hunks + 1764bca README onto 4919088, final approved egui/winit pins (blocked until those refs exist), registry compatibility, release SHA |
P0 review, final refs selected | revert CL-1; keep old d27646a5 |
| S2 | Complete reviewed/published storytold egui P8 (E1–E21 + E21b, P2b async W15P routing, P7 held-key pairing, P7b/E21b, P7c selected #8763/#8748 fixes, justified perf fixes) and completed winit (W17–W27 + W15P + W27 lifecycle), immutable approved SHAs. No stock 0.37 or intermediate stage |
P0 review, fork review, final refs selected and published | revert fork pins; keep 0.36.2/0.36 craft set |
| CL-2 | craft-libs provisional capture-slot API in crates/ui/src/capture.rs; cancellation/out-of-order/shutdown stress; lazy-print tests |
P0 review | revert helper with its adapters |
| CL-2F | Shared capture API freeze for the eleven craft-ui consumers after reviewing the DesignCraft and GridCraft CL-2 pilot adapters |
CL-2, G1 (Grid adapter), DesignCraft adapter | keep CL-2 revision; no app change |
| G0 | Baseline harnesses on ac8da7d4: frame bench, phase-tagged frame log, 0.36-compatible geometry/screenshot tests, benchmark evidence (0.37-only assertions arrive in G1/G3) |
P0 | remove harnesses; no production behavior change |
| G1 | Atomic dependency/compile migration: root patches, manifests (native eframe clipboard), lock, craft-ui/craft-print bump, CL-2 slot adapter, E21b/W15P consumption, Role/ID/FontData/screenshot-callback source changes including drain_screenshots, timeout and the basic reply, strict shared harness with required names/fonts, style pin |
S1+S2+CL-2, G0 baseline | revert G1; restore baseline lock/manifests |
| G2 | Lifecycle: autosave/title/printing in logic, adversarial screenshot byte-budget/discard/timeout/epoch tests, hidden synthetic-input paint | G1 | revert G2; G1 still builds |
| G3 | Remaining accessibility names/roles, pixel probes, focus/ID tests, broader IME/RTL coverage | G1 (G2 if same files) | revert G3 |
| G4 | Platform/performance acceptance (staged): Linux X11/Wayland, Windows VM, browser, macOS; print and CL-2 evidence; fix findings | G2+G3+CL-2F | revert G4 fixes; retain evidence |
| G5 | Docs: architecture, ui-parity, control protocol notes, release notes | G4 | revert docs |
Implementation does not start until P0 has a fresh independent adversarial review, the complete
S1/S2 revisions are fixed and published, and CL-2's provisional API exists. The final production
storytold SHAs for egui P8/winit are unknown until that approved implementation; their absence
blocks S2/G1 rather than being filled with a candidate SHA or a stock/intermediate stage. Each app
PR is reviewable on its own; do not land an intermediate dependency bump that leaves main
unbuildable. A consumer already carrying an incompatible craft-ui bump must put that bump in the
same atomic dependency stage as the egui P8 landing. G1 is the only atomic “must compile and pass
CI” PR: it includes the shared strict harness, every accessible name and pinned font needed by the
existing suite (no suite-wide allow_missing_glyphs/with_accessibility_check(false)), and the
CL-2 adapter. G2/G3 can be separate commits/PRs once G1
is green. CL-2F freezes the shared capture API only after both initial adapters are reviewed. G4 is
validation plus targeted fixes, not a feature grab; rerun the affected performance and visual checks
after any fix.
10.2 Compatibility pin and lockfile checks
Before each publication:
cargo metadata --lockedandcargo treeshow one egui-family source/revision and one winit.- Root
[patch.crates-io]entries use full SHAs; no branch/path. craft-uiandcraft-printresolve to one approved craft-libs SHA that contains CL-1, CL-2's provisional-or-frozen capture slot and any CL-3 contract fixes GridCraft consumes. CL-1 and CL-2F are published for the eleven sharing consumers; GridCraft/DesignCraft use the CL-2 provisional pilot and their adapters precede CL-2F. Do not add a GridCraft-local capture channel/token map while the shared slot is the selected design, and do not leave a mixedcraft-ui/egui stage.- The lockfile has no second
egui/eframeversion and no stale accesskit_consumer duplicates except ones proven necessary by an unrelated dependency. cargo check --lockedin a clean checkout works; CI's dependency check fails on a missing patch.docs/version statements that mention 0.36 are updated only after validation; do not rewrite historical citations.
10.3 Rollback
Rollback restores the entire coherent set: Cargo.toml, all manifests, Cargo.lock, root patches,
source adaptations, craft-ui/craft-print revision and any test harness changes. Do not “roll
back” by changing only the egui version string. Keep the old lockfile, the 0.36.2 registry crates and
craft-libs d27646a5 reachable. No persistence format changes are planned; ui.json, prefs.json, XLSX and
control JSON remain compatible. If a persistence change becomes necessary, specify backward
compatibility and preserve unreadable data.
11. Original planning risks and remaining acceptance criteria
Current disposition (2026-10-24): exact canonical revisions and the coherent capture API in items 1–2 are selected as recorded at the top of this plan. The representative strict harness checks in item 9 passed in final full CI07. Earlier candidate/PR states below are planning history, not current pin blockers. Native/platform/IME, screen-reader and performance acceptance remain separate; the final qualification update records what actually passed.
- Final production fork revisions.
aiongg/egui/aiongg/winitare review candidates, not production sources. The exact immutablestorytoldegui P8 (including P2b async W15P routing, P7 held-key pairing, P7b/E21b, P7c's selected #8763/#8748 fixes and justified perf fixes) and completed winit (W15P + W27 lifecycle) SHAs are not selected at planning time. Pinning is blocked until the approved implementation publishes them; record the full SHAs in 1.1 and verify--lockedbefore S2/G1. No stock 0.37 or intermediate stage is accepted. Without E2/E3, #761's crash/fallback behavior is not qualified; without E4/W15P and E19/E21/E21b, Wayland clipboard, PRIMARY, non-Latin and IME behavior regress. - craft-libs CL-1 and CL-2 exact revisions. PR 199's tree is stale; CL-1 is the hunk port onto
4919088, not a rebase. GridCraft cannot bumpcraft-ui/craft-printuntil CL-1 and CL-2's provisional capture slot exist with clean tests and a coherent lock. CL-1 and CL-2F are the shared gate for the elevencraft-uiconsumers; GridCraft/DesignCraft are the CL-2 pilot and their adapters are reviewed before CL-2F freezes the API. Any incompatible existingcraft-uibump must land in the same atomic dependency stage. - PRIMARY and HTML coexistence. W15P must land in the approved winit revision, and the 6.4
routing must be proven on wlroots/KWin/Mutter: grid HTML+plain-text copy through the one
arboard data-control handle, TextEdit copy/paste and PRIMARY through egui-winit/W15P, no second
arboard handle or
wl_data_device, password exclusion and owner handoff intact. Native evidence is required. - E21b outgoing IME text. The final canonical fork includes the shared producer and Wayland
adapter; the original candidate lacked them. GridCraft-specific acceptance still requires the
complete shared contract: owner generation present even when unavailable, available-empty
distinct and explicitly sent as an empty payload, cache clear plus committed disable/enable on
owner/unavailable transition, final completed pass including
Noneauthoritative, a current-epoch serial mismatch applying incoming edits once and deferring outgoing state until a match while obsolete epochs invalidate, and no full-document allocation for the new bounded payload. Formula-bar/in-cell surrounding text, W27 delivery, deletion+commit, preedit exclusion, focus/password clearing and two-window isolation remain gates; E21 alone is not enough. - #8763/#8748 TextEdit selection and hidden-child regressions. 0.37 collapses a focused TextEdit's selection before the clicked widget runs. The final egui P8's P7c owns the selected #8763/#8748 fixes and regression coverage; GridCraft verifies its formula-bar/context-action paths under section 8.5. A local workaround requires a demonstrated app-specific need and removal condition; PR #8764 itself is open/unmerged.
- #8731 text tessellation. Stock 0.37 is reported ~60% slower for plain text; stock is the measurement control. If reproduced, the fork needs the justified row-level fast path in P8 before GridCraft accepts. GridCraft renders a large text grid, so this is a release blocker, not a note.
- #8736 DX12 shader compile. FXC is GridCraft's default. Measure renderer construction and first frame; decide on the fork shader fix without weakening wrap modes. The upstream figure is a report, not our measurement.
- #8512 system fonts. Do not adopt the new provider without a measured startup/coverage comparison; GridCraft's per-workbook font binding and script fallback must remain.
- Strict harness names/fonts. G1 must pass
egui_kittest's Panic/accessibility defaults using pinned fonts and complete accessible names, not a global opt-out. If a specific fixture cannot be pinned, record the gap and the per-test exception; do not make CI depend on host fonts. - Staged platform evidence. Native Linux needs a provisioned X11/Wayland display and
compositor; Windows needs an owned clone that fits the allocator budget and remains
software-rendered until a qualified GPU run; browser needs a pinned runtime; macOS and physical
Windows GPU/audio need real hosts. Host
/dev/dri/renderD128access is not display/GPU acceptance. These stay explicit rollout blockers until recorded. - AccessKit tree changes. Role/tree/name changes may alter native screen-reader output. kittest assertions are necessary but not sufficient; native AT-SPI/UIA/VoiceOver smoke tests are required where available.
- Print scope. PDF construction remains synchronous on the UI path as it is today. A measured candidate regression is fixed or explicitly budgeted; moving generation to a background engine is a separate, designed change.
- No generic compatibility layer. craft-ui owns the capture slot because multiple apps need the same completion lifecycle. GridCraft keeps its timeout, paths, tokens, JSON and animation policy and does not add a local channel alternative.
Acceptance for G1 is: cargo xtask ci passes at the exact approved P8/winit revisions with actual
test counts and the strict shared harness (no global glyph/accessibility opt-out); cargo check --locked passes for native and wasm; one egui/winit source; E21b/W15P/CL-2 APIs are consumed;
drain_screenshots, timeout and the basic screenshot reply work, including a hidden/occluded
queued capture admitted through RequestPaintWhileHidden (a minimized request waits for restore or
times out); the historical compile errors are gone; and the
G1 smoke tests (open/edit/undo/save/reopen, screenshot, language switch, resize) pass. Acceptance for the migration as a whole adds G2–G4 evidence, the
visual/geometry/accessibility checks, paired performance results against the recorded baseline,
print and capture measurements, and no unresolved blocking unknown above.
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/gridcraft main for this migration is b8771d51fb4da60bd48c13cf313d6d6014dc5e66 (the checkout in
/home/codex/work/CraftApps/egui037-migration-20261022/gridcraft-implementation). The earlier
bdf8b8cc… and ac8da7d4… SHAs and line anchors above are retained as historical review
evidence; use b8771d51fb4da60bd48c13cf313d6d6014dc5e66 and its exact lockfile for current-main integration and paired
measurements. Shared craft-libs integration now pins canonical 25da1314; 3832c47 is its
historical integration base. The original refs remain reachable. These unpublished immutable
pins still require an approved publication and clean remote-fetch verification.
Add Ctrl+8 outline symbols/headings combinations to geometry and hit tests; consume reveal_from once; include Auto Fill Options, accounting fill and Ctrl+click hyperlink regressions. Pin fonts when using the now host-font-dependent snapshot example.
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.
Explicit accessibility qualification update — 2026-10-24
Full CI07 is successful evidence for the final formatted runtime source, including the seven
representative integration tests that explicitly call check_accessibility() on rendered
ribbon/locale/pane/menu/dialog/editor/backstage states. The real SheetApp capture Harness also
passed checks before issuance and after manual callback drain/repaint, preserving callback count,
request token, one-reply and released-reservation assertions. The CI06 unnamed controls were
repaired with visible localized label relationships or precise semantic names; roles and behavior
assertions were preserved. The four source-file differences from the fresh accessible-name review
are formatter-only. Full CI07 passed all ten stages with 2,249 passed, 0 failed and the existing
release timing experiment ignored. CI05 remains historical pre-repair evidence; CI06 remains a
failed historical run.
This is callback and representative UI-tree coverage, not GPU/native hidden-window, native screen-reader or every-state accessibility acceptance. The canonical egui fork includes outgoing IME surrounding-text support and its Wayland adapter; application-specific formula-bar, in-cell and text-box owner transitions, stale deletion/commit handling and native composition remain separate qualification gates. Native open/edit/undo/save/reopen, language/resize, clipboard/PRIMARY, hidden/minimize/restore/readback and browser/macOS/Windows workflows remain open, as do paired performance, renderer/print and font/WASM size acceptance. Required CI success permits local commit review; it does not authorize a push, tag or PR publication. Every publication still needs fresh explicit human approval, followed by clean remote-fetch verification of the exact pins.
Revision: 2026-10-24 — bound CI07, review union and final canonical source; no production changes.