Reconcile main 991904888a and preserve the reviewed lifecycle, IME, rendering and lockfile changes.
Formatting, strict lint, layers, assets and WASM passed. Full CI retained 3,961 passes, 14 baseline-matching GPU failures and 31 ignored executions. The local review commit has explicit approval to retain these documented failures; native/browser acceptance and paired app performance remain open.
124 KiB
EffectCraft — egui 0.37 migration plan
Current landing status, 2026-10-25: the working tree includes main 991904888a20b7da72b2a4880b21c7d912154736 and pins egui 6d518fc799681571963842f95b55a6b68431b15a, winit a517b3846ee8457d84ee4975110bfda6797c44dc and shared UI 25da1314d443171b4adeaad2cd9454b273f35c6f. The final IME adapter uses ordered native reset request identifiers and ImeResetCompleted; the earlier tombstone-only description below is historical.
Full current-source CI egui037-effectcraft-current-main-ci-20261025-0231 recorded 3,961 passing tests, 14 failures and 31 ignored across 88 test summaries. All 14 failures are GPU tests: their names and diagnostic blocks match the frozen pre-migration eb51087 baseline exactly after removing thread process numbers. Formatting, strict lint, layering, assets and the 32-crate WASM check completed. The 596-test UI regression suite passes. This is not a green full-CI result or hardware-GPU qualification. Native IME, visible final-source workflows and paired performance remain open.
The repository's green-only commit gate remains unmet by the GPU results; the reviewed worktree and complete evidence are retained for human review. No push, publication or merge is authorized by this status.
Historical implementation and research record follows. Its older pins, test counts and unacknowledged-reset description do not describe the current candidate.
Status: implementation in progress, 2026-10-24. The dependency/API port, IME context adapter,
screenshots, accessibility, font coverage and text-edit repairs are present in the dirty migration
worktree. The parent-managed local run
egui037-effectcraft-candidate-20261024-upstream-tests-09 recorded 820 passed, 14 ignored:
effectcraft-ui-egui --lib 297 passed / 3 ignored, --test it 521 passed / 11 ignored and
--test ui_accessibility 2 passed; cargo clippy -p effectcraft-ui-egui --lib --tests -- -D warnings
exited 0. That run covers effectcraft-ui-egui only; the engine suite, full cargo xtask ci, wasm,
native GUI and real-platform IME were not rerun for this revision. Evidence:
/home/codex/work/ssd2/task-evidence/egui037-effectcraft-candidate-20261023/task-jobs/egui037-effectcraft-candidate-20261024-upstream-tests-09/output.log.
The custom viewer text editor now has an explicit per-viewport IME composition lifecycle instead
of a frame-number suppression window: an active composition is owned by the edit session's
layer/session identity, and a target switch or editor end turns it into a tombstone. An empty
pre-edit is the observed end of the visible composition, but its terminal commit/delete can arrive
in a later event, so the owner stays associated with the cleared composition; if the target changes
before that terminal event, the tombstone rejects it instead of applying it to the replacement. A
lone non-empty old pre-edit keeps the tombstone across arbitrary idle frames. A rejected terminal
commit consumes the tombstone immediately, so a new pre-edit later in the same raw input batch
starts a composition for the current target. A rejected empty pre-edit or delete-surrounding keeps
the tombstone through the rest of that batch so its paired old commit is rejected too, and is
consumed at batch end when no commit follows. A composition started after that terminal boundary is
accepted, so the tombstone never becomes a permanent lock. egui::ImeEvent
payloads still carry no context generation, and the inspected egui-winit 0.37 source ignores
Ime::Enabled/Ime::Disabled and implements should_interrupt_composition by toggling
set_ime_allowed without an observable reset acknowledgement. The fallback therefore drops the
first ambiguous composition after an unobserved platform reset rather than claiming provenance it
cannot prove; a producer-side event epoch would be needed to close that residual, and this is not
claimed as tested on a real platform IME here. Two real-app headless regressions in
crates/ui-egui/tests/it/ui_text_edit.rs cover the observed-empty-preedit pending commit and a new
pre-edit after a stale terminal commit in the same raw batch; this revision adds them but does not
claim they were rerun here.
The later bounded same-owner interruption repair (2026-10-24) tracks the expected caret and
selection separately from that immutable layer/session owner. A live or terminal-pending
composition is invalidated before application pointer/selection changes can accept an old IME
mutation, including keys before a later IME event in the same raw batch and control-client
selection changes before a pass. Accepted IME mutations update the expected selection so the
IME deletion's own caret shift does not reject its paired commit. The existing terminal rules
still permit a fresh preedit after a rejected terminal commit, and a direct commit with no active
composition remains valid even after a pointer change in the same batch. Five new real-app
regressions cover15 scenarios with both deletion forms, pointer and keyboard interruption,
terminal-pending/external selection, undo preservation, fresh composition and direct commits.
These new source bytes and tests await parent-managed formatting, focused/full gates and fresh
independent review; no new test, native-IME or final dependency-graph acceptance is claimed here.
Every future push or new pull request still needs a fresh, explicit human approval of the exact commit range and its validation; this document is not publication evidence.
Scope: storytold/effectcraft only (this checkout). Shared work in storytold/craft-libs
(PR 199 and the CL-2 capture helper), the suite egui fork and the suite winit fork are dependencies
with their own plans; this document records what EffectCraft needs from them, not what they should
do. Sibling apps, their plans and their dependency bumps are out of scope.
This plan deliberately separates:
- facts verified by reading the pinned source in this checkout, the egui checkout and the upstream 0.36.2/0.37.0 tags;
- historical clues (the 2026-10-22 audit compiler output), which are not current acceptance;
- proposals (refactors and acceptance criteria) that the implementation PRs must prove.
1. Baseline and provenance
1.1 Exact source under plan
| Item | Value |
|---|---|
| EffectCraft implementation checkout | /home/codex/work/CraftApps/egui037-migration-20261022/effectcraft-implementation |
| Current EffectCraft worktree | HEAD 049070d65c66a73f322b496b22521816e0018f14; dirty migration worktree. The lock and manifests still use the local integration patch, not final immutable pins. |
| EffectCraft historical planning baseline | 9581b53c3f8945683252af0eb5fac79e0e43aed4 (Release 0.16.0, 2026-10-22), retained for baseline comparisons only |
| Branch | codex/egui-037-plan-20261022 |
| Workspace | single root workspace Cargo.toml (resolver = "3", members crates/*, apps/*, xtask) |
| Lockfile | Cargo.lock, lock format 4 |
| MSRV / edition | Rust 1.95, edition 2024 (Cargo.toml:7-12) |
| Target upstream release | egui tag 0.37.0, peeled commit 841a328ba2dd8f6996beb7189ebb7fc00e668a2a (tagged 2026-10-15) |
| Candidate suite egui (comparison input only) | aiongg/craft/0.37 = a4f9a17f64a6bae61350fb85804c9df0d9865916 (2026-10-21, E1–E21). Read-only source for this plan; not a publication pin. |
| Candidate suite winit (comparison input only) | aiongg/craft/0.30.13-lang = 8b35388bd719e8cc86a95971ba0bc026c9a8f058 (winit 0.30.13 + W1–W15, W17–W27). Read-only source; not a publication pin. |
| Production egui pin | pending approved fork implementation: the final reviewed and published storytold/egui P8 SHA containing E1–E21, P2b async W15P routing, P7 held-key pairing, P7b/E21b, P7c's selected #8763/#8748 fixes and the justified #8731/#8736 performance fixes if measurements require them. All egui-family crates must come from that one revision. |
| Production winit pin | pending approved fork implementation: the completed, final reviewed storytold/winit SHA containing W1–W27, W15P and the W27 lifecycle fixes. |
| Final dependency unit | one immutable set recorded together: the complete storytold egui P8 SHA + storytold winit SHA + craft-ui SHA with CL-1 and CL-2F, proved by a clean --locked fetch. This is the consumer's first and only compatibility landing: no stock-0.37 or intermediate fork production stage. |
| Suite shared UI dependency | craft-ui from storytold/craft-libs, reconciled merged-PR base 3832c47ef68a0e6214f2c1e85f3526b6adcb686e (current shared-library implementation base). Historical planning rev 5c874d51eeb3de70e735c8ce87a59a7dedd47675 is superseded; the final publication pin is the reviewed immutable SHA containing CL-1 and CL-2F. |
| Shared PR history | storytold/craft-libs#199 is merged at 3832c47ef68a0e6214f2c1e85f3526b6adcb686e. Historical port hunks 8614e4a04fae516fba16c22b31ff1008cb6be10b and README 1764bca, applied to old main 49190884278f16c9a9f5823d7698a8ffb5deb062, are superseded. Never merge b68de8c/ec8840a as trees. |
| Separate language PR | storytold/craft-libs#198 (fork E5/E9/E12–E21, winit W17–W27) |
All pins must be full commit SHAs. Branch names are not publication pins. The aiongg SHAs above
are source-inspection inputs for this review; the lock file in a publication branch must name only
the final storytold revisions (plus the existing unrelated craft-libs PDF pin).
1.2 Baseline dependency set (historical 9581b53c; from Cargo.lock and manifests)
The implementation worktree now resolves the egui family at 0.37.0 and craft-ui at
3832c47ef68a0e6214f2c1e85f3526b6adcb686e through local-integration-config.toml; the current
Cargo.lock entries therefore have no registry source for those patched crates. That local
configuration is review evidence, not an immutable publication pin.
| Crate | Version / source | Where pinned |
|---|---|---|
egui |
0.36.2 crates.io, checksum dc938cc2… |
Cargo.toml:108 |
eframe |
0.36.2 crates.io, checksum 55ad41fa… |
Cargo.toml:109 (default features off) |
egui_kittest |
0.36.2 |
Cargo.toml:110, dev-only |
egui-wgpu / egui-winit |
0.36.2 |
transitive, one each |
kittest |
0.4.0 |
transitive of egui_kittest |
accesskit / accesskit_winit |
0.24.1 / 0.32.2 |
transitive |
winit |
0.30.13 crates.io, checksum a6755fa5… |
apps/effectcraft/Cargo.toml:23 ("0.30", default features off) and transitive |
wgpu |
30.0.1 |
Cargo.toml:101 |
craft-ui |
baseline 0.1.0 git storytold/craft-libs rev 5c874d51…; implementation resolves local craft-libs-implementation at 3832c47… |
Cargo.toml:107 plus local-integration-config.toml |
craft-pdf |
git storytold/craft-libs rev ca50d63b… |
Cargo.toml:15 |
craft-docpath |
git storytold/craft-libs rev 82a24b00… |
Cargo.toml:16 |
hayro-syntax patch |
git storytold/craft-libs rev ca50d63b…, =0.7.2 |
Cargo.toml:213-214 |
wasm-bindgen |
0.2.129 (exact crate and CLI), apps/effectcraft-web/Cargo.toml:34 and xtask/src/main.rs:254 |
web only |
| filmcraft crates | git rev 2c5ba7a7… |
Cargo.toml:76-95 |
There is exactly one egui family in the baseline. The target must preserve that invariant (see §6).
Manifest audit: 38 Cargo.toml files (root, 32 crates/*, 3 apps/*, xtask, and the standalone
plugin). Only crates/ui-egui, apps/effectcraft, apps/effectcraft-web and (through
effectcraft-ui-egui) apps/effectcraft-cli use the egui family. examples/plugins/posterize-bands
has its own workspace/lock and no egui. There are no vendored crates and no [patch] on the egui
family in the baseline; the only patch is hayro-syntax.
1.3 Package, binary, feature and entry-point targets
| Target | Kind | egui-family surface |
|---|---|---|
effectcraft (apps/effectcraft) |
native binary | eframe with default_fonts, wgpu, accesskit, links, wayland, x11 (apps/effectcraft/Cargo.toml:18); direct winit; native Desktop app (apps/effectcraft/src/main.rs:426-456) |
effectcraft-web (apps/effectcraft-web) |
cdylib + rlib, wasm32 |
eframe web runner (app.rs:88-122, start() at app.rs:185); eframe features default_fonts, wgpu |
effectcraft-ui-egui (crates/ui-egui) |
library (swappable frontend) | the real UI, control channel, docking, viewer, timeline, kittest dev-dep eframe + wgpu (crates/ui-egui/Cargo.toml:24-41) |
effectcraft-cli |
binary + MCP server | depends on effectcraft-ui-egui for bench --ops (apps/effectcraft-cli/Cargo.toml:19) |
effectcraft-ui-egui --example snapshot |
example | headless kittest+wgpu screenshots (crates/ui-egui/examples/snapshot.rs) |
xtask |
tool | ci, wasm, web, assets, layers (xtask/src/main.rs:409-428) |
examples/plugins/posterize-bands |
separate nested workspace | no egui dependency; its own Cargo.lock; untouched |
Platform cfgs that the migration must respect:
- wasm32:
frames.rsremote/local frame pump,audio.rsWeb Audio,theme.rsno system fonts,prefs_live.rsno native drops, viewer deferred readbacks (lib.rs:825-855),sys-localeonly off-wasm (crates/ui-egui/Cargo.toml:36-37), web storage/workers. - native: pointer wrap (
pointer_wrap.rs:133), system font fallbacks (theme.rs:388-430),rfd/cpal, native menu (macOS),zbusappearance (Linux), appimage (Linux). eframe/winitplatform features: Wayland and X11 on Linux,muda/objc2on macOS,winresourceon Windows.
1.4 Current test and CI commands (source-declared)
cargo xtask ci=cargo fmt --check;cargo clippy --workspace --all-targets --release -- -D warnings;cargo test --workspace --release; PDF dependency check;layers;assets;wasm(xtask/src/main.rs:409-418).cargo xtask wasmchecks every L0–L4 crate pluseffectcraft-ui-eguiandeffectcraft-webforwasm32-unknown-unknown(xtask/src/main.rs:235-252).- UI tests: 81 files in
crates/ui-egui/tests, driven byegui_kittest::Harness(build_eframe,run_steps, syntheticEvents, automation-registry rects, AccessKit queries).ui_gpu.rsruns the viewer on egui-wgpu's device;ui_agent_audit.rsaudits commands and automation coverage;viewer_geometry.rshas an opt-in visual matrix (CRAFT_UI_VISUAL_DIR). - Web:
cargo xtask web --serve 8765, thennode apps/effectcraft-web/tests/smoke.mjs,workers.mjs,page.mjs,storage.mjs(docs/web.md:412-445). - Performance:
effectcraft-cli bench --ops --json(apps/effectcraft-cli/src/main.rs:885-925),cargo xtask bench(xtask/src/bench.rs), and the headlessui_egui::bench::Driver(crates/ui-egui/src/bench.rs:13-108).
2. What actually changes in egui 0.37 that affects EffectCraft
This section is limited to changes visible in the pinned sources. It corrects two assumptions that must not enter the plan:
App::logic/App::uiandContext::run_uiare not new in 0.37. They already exist in 0.36.2 (egui-0.36.2/crates/eframe/src/epi.rs:167,182;egui-0.36.2/crates/egui/src/context.rs:794), and EffectCraft already uses them (crates/ui-egui/src/lib.rs:1852-1935,apps/effectcraft/src/main.rs:426-456,apps/effectcraft-web/src/app.rs:88-122). The migration is not a wholesale update-to-ui rewrite.Context::run_logicis not a substitute for a pass. It does not interpret time or events;run_logiconly refreshes viewport state (egui-0.37.0/crates/egui/src/context.rs:1190-1218). Calling it cannot initialise fonts or consume input.
2.1 Compile-visible API changes (the historical audit is a clue, not acceptance)
The 2026-10-22 audit (/home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/effectcraft.errors)
compiled only effectcraft-ui-egui (lib and lib test), against a historical fork revision and
machine-local path patches. It reported the same 15 errors in fork and stock mode, with 602
(stock) / 603 (fork) warnings; the difference is warning counts only. Those errors are consistent with the upstream 0.37 sources read here:
| # | Anchor in this checkout | Error | 0.37 API and required change |
|---|---|---|---|
| 1 | crates/ui-egui/src/color_picker.rs:19 |
Role::ColorButton absent |
WidgetType is a deprecated alias of accesskit::Role; use Role::ColorWell (egui-0.37.0/crates/egui/src/lib.rs:644-659) |
| 2 | color_picker.rs:336 |
Role::Other absent |
Role::Unknown |
| 3 | control.rs:371 |
Event::MouseWheel missing source |
add source: MouseWheelSource::Wheel (egui-0.37.0/crates/egui/src/data/input/event.rs:175-200) |
| 4 | lib.rs:1539 |
no egui::UserData |
screenshots are callback based (ScreenshotCallback) |
| 5 | lib.rs:1569 |
no Event::Screenshot |
receive the image from the callback, not from input events |
| 6 | menu_keys.rs:84 |
UiStack.id absent |
UiStack.unique_id / UiStack.scope_id (ui_stack.rs:133-150); use scope_id for a stable panel identity |
| 7 | panels/effect_controls.rs:1274 |
Role::SelectableLabel |
Role::Button with WidgetInfo::selected |
| 8 | panels/project.rs:613 |
Role::Other |
Role::Unknown (or Role::TreeItem directly) |
| 9 | panels/project.rs:781 |
Role::Other |
Role::Unknown |
| 10 | panels/text_panels.rs:700 |
Role::SelectableLabel |
Role::Button |
| 11 | theme.rs:732 |
&dyn AsRef<[u8]> comparison |
FontData.font is now a Blob = Arc<dyn AsRef<[u8]> + Send + Sync>; compare FontData::bytes() |
| 12 | theme.rs:752 |
same | same |
| 13 | widgets.rs:24 |
UiStack.id |
scope_id |
| 14 | widgets.rs:493 |
expected IdSalt, found Id |
Atom::custom(IdSalt, …) (atomics/atom.rs:98) |
| 15 | widgets.rs:498 |
expected IdSalt |
WidgetAtomResponse::rect(IdSalt) (atomics/widget_atom.rs:224) |
Deprecations that -D warnings turns into failures (all 602 audit warnings are in this list):
| Deprecated item | Count in this checkout | Replacement |
|---|---|---|
egui::Id::new |
616 lines (591 src, 25 tests); 626 occurrences (601 src, 25 tests) across 59 src and 12 test files |
Id::unique (same hash; id.rs:101-104 delegates to it) |
egui::WidgetType |
19 | egui::Role (variant renames in §2.2) |
Ui::id() |
2 (color_picker.rs:335,337) |
Ui::scope_id() (ui.rs:363-371) |
egui::AtomLayoutResponse |
1 (widgets.rs:497) |
WidgetAtomResponse |
TextEdit::vertical_align |
1 (widgets.rs:329) |
TextEdit::align(Align2::LEFT_CENTER) (builder.rs:408-427) |
Integration tests were not compiled by the audit and must be assumed to contain more breakage. Two known classes are already visible by inspection:
- 11
Event::MouseWheel { … }constructions lacksource(control.rs:371,ui_timeline_zoom.rs:33,81,ui_scrolling.rs:34,157,viewer_geometry.rs:98,235,ui_panels.rs:386,ui_tail.rs:195,ui_layer_panel_zoom.rs:109,ui_camera_tracker.rs:173). The 12th match is a pattern inlib.rs:581. ui_agent_audit.rs:73readsWidgetInfo.typ; it becomes.role.
There is no PointerSource in the target fork: Event::PointerButton keeps the 0.36 shape
(aiongg/craft/0.37:crates/egui/src/data/input/event.rs:106-118). The audit error at
control.rs:371 was the wheel event, not the pointer button.
2.2 Role mapping used by this migration
WidgetType → accesskit::Role (egui's own mapping table, egui/src/lib.rs:644-659):
| Old | New |
|---|---|
Button |
Role::Button |
ColorButton |
Role::ColorWell |
Slider |
Role::Slider |
SelectableLabel |
Role::Button + selected |
Checkbox |
Role::CheckBox |
TextEdit |
Role::TextInput / MultilineTextInput |
DragValue |
Role::SpinButton |
CollapsingHeader |
Role::DisclosureTriangle |
Panel |
Role::Pane |
ResizeHandle |
Role::Splitter |
Other |
Role::Unknown |
2.3 Behaviour changes that compilation will not catch
These are the app-specific risks, each with a source anchor and a planned assertion.
default_fontsis no longer a default feature ofegui/epaint. eframe still enables it by default, but EffectCraft disables eframe defaults and re-enables onlydefault_fonts,wgpu,accesskit,links,wayland,x11(crates/ui-egui/Cargo.toml:25,apps/effectcraft/Cargo.toml:18). The app relies onFontDefinitions::default()attheme.rs:334; the feature must stay explicitly enabled on theeguidependency, not only through eframe, socargo check -p effectcraft-ui-eguiand test builds are deterministic. (#8583;egui-0.37.0/crates/egui/Cargo.toml:40-46.)- The bundled emoji fonts are no longer in
FontDefinitions::default(). In 0.36.2 the default definitions includeNotoEmoji-Regularandemoji-icon-fontunconditionally (egui-0.36.2/crates/epaint/src/text/fonts.rs:490-580). In 0.37 they are behindmonochrome_emoji_fonts, and the default only adds the smallegui-iconsfont (egui-0.37.0/crates/epaint/src/text/font_definitions.rs:105-180). EffectCraft's own chrome is affected. A read-only cmap audit of the fonts actually bundled by this app and by egui found:✔U+2714 (menu check gutter,menus.rs:2176) is in only NotoEmoji and emoji-icon-font;▣U+25A3 (Media Browser folder,media_panels.rs:198; Essential Graphics media row,essential.rs:247) is in Hack and emoji-icon-font, but Hack is not in the app's Proportional family (theme.rs:339-347only prepends Inter and appends JetBrains Mono), so only the emoji font covers it;✓U+2713 (checked label text,widgets.rs:485) is in Inter/JetBrains Mono and unaffected. Withoutmonochrome_emoji_fontsthese two become tofu, and kittest's defaultMissingGlyphPolicy::Panicturns that into test failures.
- Clipboard support became an eframe feature. 0.36.2's eframe always enabled
egui-winit/clipboard(egui-0.36.2/crates/eframe/Cargo.toml:152). 0.37 gates it behindclipboard(egui-0.37.0/crates/eframe/Cargo.toml:142-144), which is in eframe's default set but not enabled by EffectCraft'sdefault-features = falsemanifests. Without it, cut/copy/paste to the OS clipboard silently degrade to an in-app simulated clipboard (egui-winit-0.37.0/Cargo.toml:44-46), breaking Edit ▸ Copy/Paste, text-field paste andmenus.rs:1867-1906which maps clipboard events back to command keys. - Screenshots are callback based (
#8489;data/screenshot_callback.rs,ViewportCommand::Screenshot(ScreenshotCallback),Context::request_screenshot). The current correlation byUserDatatoken throughEvent::Screenshotis impossible (lib.rs:1530-1580,control.rs:439-444). See §5.7. WidgetTypeand ID APIs are deprecated, and the CI clippy gate is-D warnings(xtask/src/main.rs:412). This is 626 mechanicalId::newoccurrences (616 lines) plus the 19 source role edits and one more inui_agent_audit.rs(see §5.2/§5.3).- AccessKit roles/names are enforced by kittest 0.37.
HarnessBuilderdefaultscheck_accessibility: trueandHarness::runpanics on visible input widgets without a name (egui_kittest-0.37.0/src/builder.rs:50-51,src/accessibility.rs:9-45).is_inputcoversButton,CheckBox,ColorWell,ComboBox,Slider,SpinButton,Splitter,Tab,TextInput,TreeItem, … (egui-0.37.0/crates/egui/src/accessibility.rs:9-60). Many of the app's stockTextEdit,DragValueandSliderfields are currently unnamed (39TextEdit::singleline/multilineconstructors out of 44TextEdit::references, and 51DragValue/Sliderconstructors), and customui.interactwidgets have noWidgetInfoat all. - Missing glyphs panic in kittest 0.37 (
#8601;MissingGlyphPolicy::Panicis the harness default,builder.rs:50). Tests that deliberately render uninstalled scripts must opt out withHarnessBuilder::allow_missing_glyphs(); two are already known:tests/ui_effect_names.rs:35(the builder insideharness()at line 20, used by thezh-hanstests at 68, 81, 107 and 127, including高斯模糊) andtests/ui_presets_folder.rs:40(ja). Fontconfig on this planning host lists no faces at all, so those tests would panic rather than draw tofu until a CJK face is installed or the opt-out is used. Ui::id→Ui::scope_id,UiBuilder::id→scope_id,Panelconstructors takeimpl AsIdSalt. EffectCraft does not useSidePanel/TopBottomPanel, and usesCentralPanel::default()only (dock_ui.rs:475,prefs_live.rs:207), so no panel-order rewrite is needed. The rootUiis already the app surface (lib.rs:1905-1935,lib.rs:1614-1723). Never callContext::root_uifrom a panel or a test closure: it panics while the root Ui is borrowed (context.rs:1005-1055).- Style defaults changed (roomier buttons, larger corner radii;
#8617,#8445). EffectCraft pinsitem_spacing,button_padding,interact_size.y,menu_margin, scroll style and all widget corner radii from tokens (theme.rs:510-530,theme.rs:474-495), so the visible impact should be small, but atom-basedButton/ComboBoxlayout can still move geometry. This is a geometry/pixel test target, not an assumption. - Menus/popups and text editing changed (
#7802,#7624,#8572,#8548,#8763,#8628). EffectCraft builds its own menu bar and keyboard navigation (menus.rs:2025-2050,menu_keys.rs), withMenuConfigstyle overrides. Regression tests must cover hover-switch, arrows, submenu opening, outside-click close, and a button acting on a focusedTextEdit's selection (the#8763case). Container tooltips now also show when their children are hovered (#7909); check tooltip presence/absence on the app's large custominteractrects (viewer, timeline, dock) instead of assuming the 0.36 behaviour. - Kinetic scrolling/rubber-banding (
#8662,#8663) and the newMouseWheelSourcechange wheel semantics. Synthetic wheel events must sayWheel(notUnknown) so tests and the control channel keep deterministic scroll behaviour. Frame::default_fonts+ AccessKit bump: accesskit 0.24.1 → 0.25.1, accesskit_winit 0.32.2 → 0.34.1, kittest 0.4.0 → 0.5.0. The kittest trees are not identical:lib.rsre-exportsaccesskit_consumer::NodeRefasAccessKitNodewhere 0.4 re-exportedNode, andquery.rs/filter.rsalso differ. The 26 query call sites must be compiled against 0.5, not assumed unchanged.- New 0.37 lifecycle tools that this plan uses:
ViewportCommand::RequestPaintWhileHiddenandViewportInfo::visible()(viewport.rs:1202-1240,viewport_info.rs:90-101); visibility remains derived from minimized/occluded even during a forced hidden UI paint;Context::request_screenshot/ScreenshotCallback(context.rs:4595-4606). The call sends aViewportCommand, which requests a repaint of that viewport; a pending screenshot also makes a hidden window take a full UI+paint pass (wgpu_integration.rs:686-690,egui-winit/src/lib.rs:2116-2128). It does not schedule a repaint when the callback completes; the completion slot owns that wake-up (§5.7);Context::set_repaint_observerandRepaintCause(context.rs:2290-2296,283-300).
- Known upstream regressions at 0.37.0 (reported upstream; not measured by this task):
#8731: plain-text tessellation ≈61% more instructions (tessellate_textper-vertex colour check). The candidate comparison revision is unpatched:aiongg/craft/0.37:crates/epaint/src/tessellator.rs:2295-2341still has the conditional inside the vertex loop.#8736: egui-wgpu pipeline creation is ~40 ms slower on Windows/DX12 (FXC) because ofwrap_texel_coord. Both are measured acceptance gates in §9 and, if over threshold, must be fixed in the final P8 egui revision before P1 is frozen. P5 reports/retests; it does not change the pin.
3. Fork, winit and craft-libs integration decisions
3.1 What EffectCraft actually needs from the suite fork
EffectCraft has no vendor/ directory and no local egui patch. Its only [patch.crates-io] entry is
hayro-syntax, inherited from the craft-libs PDF stack (Cargo.toml:213-214); it is unrelated to
egui and must stay.
The production dependency is the complete reviewed and published storytold egui P8 revision,
not the personal aiongg branches used as comparison input here. P8 carries E1–E21, P2b async
W15P routing, P7 held-key pairing, P7b/E21b outgoing IME surrounding text (§5.5), P7c's selected
#8763/#8748 fixes and the justified #8731/#8736 performance fixes. The completed final
winit SHA carries W1–W27, W15P and the W27 lifecycle fixes. The two forks and craft-ui CL-1 +
CL-2F are approved as one immutable unit at P1; P4 adds extended native/language coverage without
a second compatibility graph (§10). There is no stock or intermediate-fork production stage;
stock 0.37.0 is a measurement control only.
| Fork patch | EffectCraft use | Decision |
|---|---|---|
E1 viewport_hooks |
not used (craft-tablet only) | keep in fork; no app change |
| E2+E3 surface fit / wgpu error scopes | protects windows whose surface exceeds the adapter limit and reports configure/renderer failures; relevant to the app's "never crash" rule and its device-loss handling | keep, benefit; verify with a required-adapter ui_gpu.rs run and a device-loss test |
| E4 Wayland clipboard/PRIMARY | the app edits text in project rows, timeline fields, expression fields, Script UI and settings; Hyprland serves one data device per client | keep; requires winit W15 plus the new W15P PRIMARY path (§5.5/§8.7) |
| E5/E12/E13/E16 bidi and grapheme editing | UI labels and stock TextEdits only; text layers use effectcraft-text, not epaint |
keep; do not claim it fixes text layers |
| E6 IME focus | stock TextEdits |
keep |
| E7/E19 letter keys on non-Latin layouts | shortcuts (menus::parse_shortcut, handle_shortcuts); requires winit W25 |
keep; manual platform test |
E8 TextEdit glyph clipping |
stock fields (Thai/Vietnamese/Arabic marks) | keep |
E9 Style::right_to_left |
app never sets it; its dock/layout is custom LTR | keep in fork; do not enable in EffectCraft (would mirror stock widgets inside an unmirrored custom layout) |
| E10 web named keys / Latin codes | web keyboard behaviour | keep; no app change |
| E11 vello_cpu multithreading | not used | keep in fork |
E14 FontTweak::fallback_align |
the app installs system fallback faces (theme.rs:388-430) |
keep; optional follow-up to set it for CJK/Indic fallbacks, with screenshot proof |
E15 LineBreaker |
text layers use effectcraft-text; not adopted |
keep in fork |
E17/E18 Context::set_language / Frame::set_ui_language |
screen-reader language and UI shaping language (the app has an interface language setting, i18n::language) |
adopt in the language stage (fork-only; see §5.9) |
| E20 widget text language | goes with E17 | adopt with E17 |
E21 ImeEvent::DeleteSurroundingBytes |
the app has a custom viewer text editor that consumes IME events itself (viewer_text.rs:397-508) |
adopt in the IME stage; stock 0.37 only has char-based DeleteSurrounding |
| P2b (new prerequisite) | async W15P PRIMARY routing in egui-winit: bounded read workers, owner-specific completion waker, no blocking UI path | in P8; gate in §5.5/§8.7 |
| P7 (new prerequisite) | held-key pairing: the emitted egui key identity is stored by physical key and reused on release; clipboard shortcuts alone do not prove this | in P8; gate in §3.2/§5.5 |
| P7b/E21b (new prerequisite) | candidate egui-winit never calls winit W27's set_ime_surrounding_text; IMEOutput carries no logical text. The custom viewer editor is a required producer. |
in P8 before final acceptance; see §5.5, §8.4 and ime-surrounding-gap.md |
| P7c (new prerequisite) | selected upstream fixes #8763 (a button/menu command acting on a focused TextEdit selection) and #8748 (hidden parent not updating deferred child viewports) |
in P8; tested in §5.6/§5.8. Local workarounds are not the final unit. |
Removal conditions are the fork's own (PATCHES.md): each patch goes when an egui/winit release in
use contains it. EffectCraft must not add a second copy of any of these fixes locally.
3.2 Winit patches that matter here
| Patch | Why EffectCraft needs it | Gate |
|---|---|---|
| W1 mixed-DPI rect | window placement and monitor hit tests (apps/effectcraft/src/window.rs) |
Windows mixed-DPI manual test |
| W2/W5/W20/W21 X11 IME | the app's editable fields and expression editor | X11 IME under Xvfb+WM (qualified parts only; §8.7) |
| W3/W18/W19/W22/W24/W26/W27 IME | the custom text editor and stock fields; W26/W27 are required by E21 and by E21b/P7b | macOS press-and-hold/dictation, Wayland text-input-v3, Windows IME |
| W15P (new) | winit-owned Wayland PRIMARY on the existing connection/event queue, per-seat manager/device/source/offer from SCTK 0.19.2; no second regular data device | wlroots/Hyprland, KWin and Mutter capability matrix (§8.7) |
| W13 Wayland wheel phase | zoom/scrub shortcuts must not latch | Wayland manual test |
| W15 Wayland clipboard/files | needed by E4 | Hyprland clipboard check |
| W23 macOS Command keys | menu shortcuts | macOS layout test |
| W25 Latin layout | E19 shortcuts; preserve the emitted key identity by physical key across release | Russian/Arabic/Hebrew layout test plus a held Ctrl+Z/Cmd+K release after a layout/modifier change |
W27 also needs the lifecycle fixes recorded in winit-review-triage.md: clear the pending
preedit/commit/deletion batch on leave with an explicit epoch, test late Done, disable/reset, seat
removal and two windows, and preserve deletion-FIFO/commit pairing. Focus epochs and protocol
serials are distinct: a current-epoch Done with a mismatched serial still applies its incoming
edits exactly once and only defers outgoing surrounding state until a matching serial; a stale
(different owner/focus generation) batch is invalidated and discarded. W25/W23/E19 must
preserve the emitted egui key identity by physical key across release (including layout/modifier
changes), clearing it on focus loss, IME interception and destruction, and test a non-clipboard
held key (Ctrl+Z/Cmd+K), repeat semantics and two physical keys mapping to one logical key.
W15 needs bounded, cancelable transfers with a 2 s total monotonic deadline and a 16 MiB total
byte budget; the egui adapter must not block the UI and must cancel stale pastes after
focus/target/generation changes.
3.3 craft-libs PR 199, CL-2 and language PR 198
- PR 199 is merged at
3832c47ef68a0e6214f2c1e85f3526b6adcb686e, the current reconciled shared-library implementation base. Do not merge its historical headb68de8c5(a stale tree relative to currentmain, missingcraft-platformand reverting security/settings/docking/ printer fixes) or audit mergeec8840a(stale lock, egui 0.36.2) as trees. The historical API port8614e4aand README1764bcaon oldmain4919088are superseded by the merged base. - EffectCraft pins
craft-uito the reviewed CL-1 merge/rebase SHA on craft-libsmainafter the port, not to the PR head. That revision must keep its egui dependency on registry0.37so EffectCraft's[patch.crates-io](not a git URL patch) can redirect it. - CL-2 capture completion helper. The shared plan's selected design is a bounded, cancelable per-request completion slot with a weak callback reference, aggregate image-byte and request limits, and an explicit viewport wake-up after the slot lock is released. EffectCraft is one of the eleven existing craft-ui consumers, so it pins CL-1 + CL-2F in the same atomic dependency stage as P1; only the DesignCraft/GridCraft pilots use the reviewed provisional CL-2 revision, and their adapters are reviewed before CL-2F freezes the API. Any consumer with an incompatible existing craft-ui bump must reconcile it in that same stage. EffectCraft does not pin the provisional revision. No app-local result channel and no generic UI compatibility layer. EffectCraft keeps its own request ids, paths, crop, deadlines and reply JSON.
craft-ui's production API used here (docking::{Action, Zone, Placement, resolve_drop, zone_rect, drop_zone},layout::{split_rect, SplitAxis, SplitSize, ResizeHandle, ResizeHandleStyle}) is unchanged by the port; the additiveprinting::tickis not used by EffectCraft.- PR 198 / the fork language work is adopted only where §3.1 says so. The app keeps its own
catalogs (
i18n.rs,i18n/ui.rs,i18n/effects.rs) and its own text engine; it does not adopt craft-text line breaking or a shared translation layer. - Blocking: CL-1 and CL-2F must land before the EffectCraft dependency-pin PR can be reviewed honestly. Until then, implementation may use a local pinned SHA only for development, never in a publication branch, and never a machine-local path patch.
4. Root-Ui, lifecycle and multi-pass semantics
4.1 Current split (already on 0.36.2, must be preserved and completed)
Today EffectcraftApp::logic does GPU-failure application, language/theme/font install, FPS, control
drain, screenshot scheduling/collection (lib.rs:1870-1903), while ui calls frame() which does
polls, playback, events, autosave, prefs, window title, close guard and the whole paint
(lib.rs:1905-1935, lib.rs:1614-1723).
Verified integration facts (candidate source, same call sites as upstream 0.37):
eframerunslogicanduiinside oneContext::run_uiclosure for every full pass (eframe-0.37.0/src/native/epi_integration.rs:349-364; webapp_runner.rs:289-292). A full pass can run more than once per frame whenContext::request_discardis used.- While the root window is minimized/occluded, native eframe runs
Context::run_logicwithApp::logiconly (epi_integration.rs:384-425), unless aScreenshotorRequestPaintWhileHiddenaction is pending, in which case it runs a full UI pass with current input and paint (wgpu_integration.rs:680-740). prepare_raw_inputcallsraw_input_hookbefore both paths (epi_integration.rs:414-428).update_logic_onlythen stores the hook-mutated raw input inpending_raw_inputfor the next full pass (epi_integration.rs:400). Therefore synthetic events fed during a logic-only call are retained, not lost, and are applied exactly once by the next full pass.- Native eframe throttles repaints of an invisible/minimized window to at least 100 ms
(
eframe-0.37.0/src/native/run.rs:27,376-383), so a hidden gesture or capture takes a few throttled full passes; the app's deadlines must allow for that and must not infer failure from the first 100 ms. ViewportInfo::visible()derives fromminimized/occluded, not from a paint request. It staysSome(false)during a forced hidden UI pass, so visibility cannot distinguish "logic only" from "forced hidden paint".run_logicrefreshes onlyraw.viewportsandraw.focused(egui-0.37.0/crates/egui/src/context.rs:1190-1218); it leavesInputState::time, events and pointer state at the previous pass's values. eframe setsRawInput::timeinprepare_raw_input, butrun_logicdoes not copy it intoInputState.Context::request_screenshotsends a viewport command, which requests a repaint, and a pending screenshot is a "wants paint" action even when hidden. The callback itself does not request a repaint when the pixels arrive.
Consequences, all pre-existing in 0.36.2:
- With the window minimized/occluded, job polling, playback advance, audio pumping, auto-save ticks, engine-event draining, the unsaved-close guard and the window-title update do not run on ordinary logic-only ticks.
- A discarded pass re-runs the
run_uiclosure, sologicruns again for the same frame.Gridrequests a discard on first show (egui-0.37.0/crates/egui/src/grid.rs:468); EffectCraft has 18Grid::newsites across dialogs/settings/forms. - The control channel's
AfterInputreplies are completed inlogicas soon assyntheticis empty, which is before the pass applies the last synthetic event (lib.rs:1893-1901,lib.rs:1921-1930). While hidden, an event can also have been fed into retained raw input with no pass yet applied.
4.2 Target boundaries
Introduce one explicit app tick and one draw pass; do not add a generic Context bridge:
// EffectcraftApp
now: f64, // monotonic app clock value for this tick, used for deadlines
focused: bool, // last known focus, refreshed from viewport info
last_input_at: f64,
last_draw_geom: Option<(egui::Rect, f32)>, // last non-discarded root draw: screen rect + ppp
geometry_epoch: u64, // bumped whenever that screen rect or ppp changes
want_paint: bool,
clock: AppClock, // real or injected (§4.3)
/// The only non-paint entry. Effects are idempotent; time may advance slightly per call.
pub fn tick(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame);
/// Paint only. Receives the root Ui from eframe.
pub fn draw(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame);
// eframe::App
fn raw_input_hook(&mut self, ctx, raw) { /* feed synthetic, pointer wrap; no clock, no replies */ }
fn logic(&mut self, ctx, frame) { self.tick(ctx, frame) }
fn ui(&mut self, ui, frame) { self.draw(ui, frame) }
tick must contain, in this order:
init_gpu(frame)(moved fromui,lib.rs:761-806), so the compositor exists before the first paint and while hidden;apply_gpu_failure+close_after_device_loss(moved fromui,lib.rs:1905-1910).i18n::set_current(language), theme/font install andapply_prefs(moved fromframe).- Engine/background polling:
poll_render,poll_track,poll_mask_track,poll_jobs,poll_offload,poll_warp,poll_roto,poll_camera(lib.rs:1617-1655), plusframes.set_context(ctx)so worker completions can request repaints while hidden. These are queue-draining and a second call in a discarded pass is a no-op (e.g.render_queue.rs:579-583takes the update queue). run_script_tasks,handle_events(engine queue drained once),command_inbox; toast/status timestamps usenow, notctx.input(|i| i.time).run_dialog_answersand the native-menu answers.tick_autosave(already wall-clock based,lib.rs:1790-1816) with the app clock, watch folder,tick_footage_reloadusingself.focused/self.now/self.last_input_atinstead ofctx.input.- Control:
drain_controlwithnow,finish_job_waiterswith a bounded repaint. - Screenshots:
collect_screenshots(drain completion slots and validate each image against the geometry + epoch recorded at issuance, §5.7), thenissue_screenshots(now), then timeouts and cancellations. - Playback:
advance_playback(ctx, scale, now)using the last drawn viewer scale; audio pump;cache_when_idle. - Unsaved close guard (
panels/unsaved.rs:50-62) and window-title update. prefs_live::memory_watch/home_on_open(split out ofprefs_live::frame,prefs_live.rs:15-19);video_previewstays in draw.- If
want_paint, or synthetic input/input_waitersremain, or a control step needs a UI pass, sendViewportCommand::RequestPaintWhileHiddenand clearwant_paint. This also covers playback with an open Video Preview or floating panel window (§5.8).
tick never reads input events, pointer state or modifiers for decisions. draw keeps only paint
and direct manipulation: dock layout, panels, menus, dialogs, areas, ui.inspect registry, pointer
hover, shortcuts that read events, finish_native, video_preview, dropped-file overlay/import,
Home thumbnail capture, toast, workspace-state save on pointer-up, and frames.pump on wasm (or
move pump to tick; either is acceptable if tested, but it must not be the only progress path).
draw updates last_input_at from the pass's input activity and records
last_draw_geom = Some((ui.ctx().screen_rect(), ui.ctx().pixels_per_point())) at the end of every
pass that is not discarded (!ui.ctx().will_discard()), bumping geometry_epoch when the rect or
ppp differs from the previous value. Screenshot issuance copies this geometry + epoch; completion
rejects/retries if the epoch changed before the save (§5.7). The callback itself supplies only
pixels, so the issued record — not callback timing — is the geometry anchor.
No Context::root_ui anywhere. Areas/Windows/Modals keep taking ctx (they create their own
layers); panels receive the existing &mut Ui. show_viewport_immediate stays as is.
4.3 App clock, focus and input gating
- The clock is app-owned and injected. Neither
ctx.input(|i| i.raw.time)norctx.input(|i| i.time)is valid in a logic-only tick:run_logiccopies only viewports/focused (context.rs:1190-1218), soi.raw.timeis the previous full pass's raw value andi.timeis the previous pass'sInputState::time; kittest leavesRawInput::timeNone.pub struct AppClock { source: ClockSource, // Real(web_time::Instant) | Manual(f64) } impl AppClock { fn now(&self) -> f64 { /* monotonic seconds */ } } let now = self.clock.now();tickstoresself.now = nowbefore using it. Tests injectAppClock::manual()and advance it; a regression must show that a stalectx.input(|i| i.time)cannot satisfy a deadline. Native and web use the sameweb_time::Instantsource;EffectcraftApp::now()exposes the last value soWebApp::logiccan pass it topersist::snapshot(§8.6). - Focus comes from the fresh viewport copy with a visible-pass fallback:
Do not read
let focused = ctx.input(|i| i.viewport().focused) .or_else(|| ctx.input(|i| i.focused)); self.focused = focused.unwrap_or(self.focused);i.events/pointer intick; input handling belongs todraw(the known UI phase). - Synthetic input is not gated on visibility, but is gated on an unapplied event.
raw_input_hookruns once per eframe prepare (frame or logic call), not per egui UI pass, and callsself.feed_synthetic(raw_input)thenpointer_wrap::prepare_input(...). In a logic-only call eframe retains the mutated raw input (pending_raw_input) and calls the hook again on the next call; in a forced hidden full pass it is applied immediately. Gating onvisible() == Some(false)would skip feeding in both cases and leave the gesture (and its reply) pending forever.feed_syntheticdrains one pointer event per call (lib.rs:578-591), so repeated logic-only updates before the forced full pass (eframe's overdue-RedrawRequestedpath and its 100 ms invisible-window throttle) would append press, move and release to the same retained input and collapse a gesture into one pass. The hook must therefore set a guard when it moves a synthetic event into raw input and skip feeding while that guard is set; the guard is cleared when a known UI pass has applied that step once, even if layout is later discarded; record that application separately from screenshot settling/accepted-frame counters. RawInput events are consumed on the first pass and are not replayed in later passes. Never clear the guard intick; never wait for the event to reappear in an accepted pass. The next step is fed only on a subsequent host input preparation. No event is discarded. §8.3 tests repeated logic-only ticks with a drag and a click. raw_input_hookperforms no reply, no clock read and no side effect other than pointer-wrap state, the synthetic-pending guard and merge-key closing.pointer_wrap::prepare_inputreturning "gesture ended" clearssession.history.merge_key; doing that during a logic-only call is safe because the events are retained and the same reset is idempotent.- Replies for
AfterInputare completed only at the end of a non-discardeddrawpass (after the pass, check!ui.ctx().will_discard()) that ran after the last synthetic event was fed, and never intick. A hidden request therefore replies only after the forced pass applied it, and a pass that egui throws away cannot produce an early success reply. - If a control request needs a UI pass (synthetic input, a panel-window change, or a caller that
explicitly wants fresh
ui.inspect/ui.elementsdata),ticksendsRequestPaintWhileHidden. control::handleis called fromtickwithnowpassed in (a signature change, not a wrapper): replace itsctx.input(|i| i.time)atcontrol.rs:428withnow, andinspect'swindowFocusedatcontrol.rs:562withctx.input(|i| i.viewport().focused).ui.inspectandui.elementskeep last-painted semantics.
4.4 Multi-pass and idempotence
A discarded pass re-runs tick and draw with the same raw input (events are taken by the first
pass). Rules:
tickeffects are idempotent for a replayed pass: polls drain queues; auto-save checks a timestamp;advance_playbackcomputes the target frame from the plan and the monotonicnowrather than incrementing, so a replay with a slightly laternowdoes not double-countPlayback::frame_shown(lib.rs:230-246); screenshot scheduling advances only whennowpasses a settle deadline, never once per pass (the old frame counter must not be replayed); job-waiter completion is keyed by the pending job, not by pass count.- One-shot side effects in
drawmust rely on egui's per-pass input (Response::clicked,drag_stopped) and command merge keys, not on layout-pass counts. High-risk paths are viewer drags (viewer.rs:1674-1874), timeline drags (timeline.rs:1500-1700), painting strokes (layer_panel.rs:350-360) and form toggles insideGrids. - Add a forced-discard regression test (§8.3) that proves one undo step and one command effect across a real two-pass frame.
5. Concrete refactors with anchors
5.1 Manifests, features, pins and lock checks
Root Cargo.toml (final publication form; placeholders are filled only after the reviewed forks
exist). P1 waits for the complete reviewed/published storytold egui P8 (E1–E21, P2b, P7, P7b/E21b,
P7c and any justified perf fix), the completed winit (W1–W27, W15P, W27 lifecycle) and craft-ui
CL-1 + CL-2F. It includes the custom-editor IME payload adapter atomically. Stock 0.37.0 and
aiongg candidates remain comparison controls, never intermediate production stages.
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "<reviewed main SHA including CL-1 and CL-2F>", features = ["serde"] }
egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] }
eframe = { version = "0.37", default-features = false }
egui_kittest = { version = "0.37" }
[patch.crates-io]
winit = { git = "https://github.com/storytold/winit", rev = "<final reviewed winit SHA incl W15P>" }
egui = { git = "https://github.com/storytold/egui", rev = "<final reviewed egui P8 SHA incl E21b + P7c>" }
eframe = { git = "https://github.com/storytold/egui", rev = "<same egui SHA>" }
egui-winit = { git = "https://github.com/storytold/egui", rev = "<same egui SHA>" }
egui-wgpu = { git = "https://github.com/storytold/egui", rev = "<same egui SHA>" }
egui_kittest = { git = "https://github.com/storytold/egui", rev = "<same egui SHA>" }
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "ca50d63b773c99f96e75c77ce5a1d21e7c9cda0d", version = "=0.7.2" } # keep
epaint, epaint_default_fonts, ecolor and emath are path dependencies of the git egui crates
and must resolve from that same git revision. aiongg/* SHAs are comparison inputs only and must
not appear in a publication lock.
Notes:
craft-uiis a git dependency fromcraft-libs; redirecting it with[patch."https://github.com/storytold/craft-libs"]is only acceptable for development, never in a publication branch. The publication form is the realrev.- Do not patch
egui_plot,egui_dock,egui_extras(unused) oregui_system_font_provider(unused whilesystem_fontsis off). - The fork's own
[patch.crates-io] winit = { branch = … }belongs to the fork workspace and does not propagate; the consumer patch above is mandatory. - Feature additions in the app manifests:
crates/ui-egui/Cargo.toml:25:eframefeatures +=clipboard.apps/effectcraft/Cargo.toml:18:eframefeatures +=clipboard.apps/effectcraft-web/Cargo.toml:30: keepdefault_fonts,wgpu(web has no OS clipboard feature; browser clipboard events are separate).egui_kittestdev features:eframe,wgpu, plussnapshotonly if §8.4's pixel goldens are adopted.egui_kittestdepends oneguiwithdefault-features = false; only kittest's own[dev-dependencies]enabledefault_fonts/monochrome_emoji_fonts, and a dependency's dev-dependencies do not propagate. The workspaceeguifeatures above are therefore what EffectCraft's library and integration-test builds receive; do not rely on kittest to enable fonts. If a test root resolveseguiindependently, it must enable the font features itself or install controlled fonts, and the actual graph is checked per production/test root.monochrome_emoji_fontsis enabled at the workspaceeguidependency so✔/▣survive (§2.3.2). It propagates intoeffectcraft-web; record the raw and compressed WASM size change and either accept it with a budget or introduce a native-onlyui-eguiemoji-fonts feature after measuring (§9).system_fontsstays off initially: the app already installs language-driven system fallbacks (theme.rs:388-430) and scans fonts for its menus; enabling eframe's fontique provider would add startup cost (#8512) and make rendering machine-dependent. Revisit as a separate product change for colour emoji.
winitdirect dependency inapps/effectcraft/Cargo.toml:23must match the patched fork (it does:0.30).- Lock/duplicate checks after every pin change:
Acceptance: exactly one egui-family source (the final storytold revision) covering
cargo tree -d cargo tree -i egui@0.37.0 cargo tree -i winit@0.30.13 cargo tree -e features --target x86_64-unknown-linux-gnu cargo tree -i wasm-bindgen --target wasm32-unknown-unknown cargo check --locked cargo test --locked cargo metadata --locked --format-version 1 --no-depsegui,eframe,egui-winit,egui-wgpu,egui_kittest,epaint,epaint_default_fonts,ecolorandemath; exactly one winit (the final storytold revision); no registryegui/epaintbeside it; nopath = "…"patch and no branch dependency; a clean checkout fetches every approved revision and passes the checks with--locked. wgpustays30.0.1; egui 0.37 accepts30.0and already resolves 30.0.1 here. The pinnedwasm-bindgen = 0.2.129must stay consistent withwasm-bindgen-futures; check the wasm-target inverse tree above.
5.2 IDs and scopes
- Mechanical
Id::new(→Id::unique(: 626 occurrences / 616 lines (601/591in source,25in tests; 59 source and 12 test files). Count by occurrence and file, not only by line.GlyphId::newincrates/text/crates/pdf(7 lines) is unrelated and must stay. Values are identical becauseId::newdelegates toId::unique(egui-0.37.0/crates/egui/src/id.rs:101-104); persisted workspace state (workspace_state.rs, version 1) and project JSON store no egui IDs, so no migration is needed. color_picker.rs:335,337:ui.id()→ui.scope_id().widgets.rs:24:s.id→s.scope_id(the panel's stable scope; do not useunique_id, which changes with layout).widgets.rs:492-497: makelabel_swatch_id() -> egui::IdSalt=IdSalt::new("label-swatch");Atom::custom(salt, …);WidgetAtomResponse::rect(salt).menu_keys.rs:84:find_menu_root(ui).scope_id.- No new ID scheme; add a test that the same source yields the same
Idacross frames and that two different payloads differ. Do not derive IDs from translated text (already true).
5.3 Roles, names and accessibility
Migrate the 19 source WidgetInfo sites with the table in §2.2, plus ui_agent_audit.rs:73 in the
test (.role). Specific choices:
color_picker.rs:19:Role::ColorWell, label = hex (existing screen-reader contract).color_picker.rs:336:Role::Unknown(a painted swatch, not an input).color_picker.rs:338:Role::Button("Original color").color_picker.rs:271,300:Role::Slider(labels stay).effect_controls.rs:1274,text_panels.rs:700:WidgetInfo::selected(Role::Button, enabled, selected, label).panels/project.rs:613: useWidgetInfo::labeled(Role::TreeItem, …)directly, but keep theaccesskit_node_builderatproject.rs:614-620forselected,levelandexpanded(WidgetInfocannot express level/expanded); remove only itsset_role(TreeItem)line.project.rs:781:Role::Unknown(comment).dock_ui.rs:367:Role::Splitter("Resize floating panel") instead ofButton.widgets.rs:497:WidgetAtomResponse.
Naming work for kittest's accessibility check:
- Strict checks stay on from the first green PR. There is no shared test harness to flip:
Harness::builder()appears at 180 call sites in 78 test files, andHarnessBuilder::default()setscheck_accessibility: true(egui_kittest/src/builder.rs:51). A new helper would not reach existing direct builders, and a per-builderwith_accessibility_check(false)would only hide failures. P1 therefore adds the actual names until every test passes with strict checks; a repository grep gate forbidswith_accessibility_check(false)(§8.2). No global or blanket opt-out exists at any stage. - Stock fields: give each
TextEditahint_textoraccessible_name, eachDragValue/Slideranaccessible_name. Known call sites:TextEdit(39) inprecomp.rs:117,rq_templates.rs:137,scriptui_view.rs:145,timeline.rs:1507,2066,2458,3516,project.rs:704,shortcut_editor.rs:276,440,532,markers_ui.rs:119,text_panels.rs:607,settings.rs:174,607,effect_controls.rs:1214,forms.rs:786,794,806,dialogs.rs:471, plus the rest fromgrep -rn TextEdit:: crates/ui-egui/src;DragValue/Slider(51) inlayer_styles_dialog.rs,gradient_editor.rs,settings.rs,key_dialogs.rs,forms.rs,dialogs.rs,dialogs_3d.rs,viewer_tools.rs,scriptui_view.rs. - For rows whose label is a separate widget, use
Ui::label_inputs_by(label_id, |ui| editor)— e.g. the Grid rows atforms.rs:771,settings.rs:596,key_dialogs.rs:142,207,251,markers_ui.rs:133,rq_templates.rs:56,100,layer_styles_dialog.rs:214. - For rows whose label is painted (Timeline property rows, Effect Controls rows), use
Ui::name_inputs(label, |ui| editor)orResponse::accessible_name(label)where the label is already known. This coverstimeline.rs:2458,3516andeffect_controls.rs:1214. - Custom widgets have no
WidgetInfotoday; add a small local helper, not a compatibility layer:Use it first for the highest-value controls: dock tabs (fn named(resp: egui::Response, role: egui::Role, name: impl Into<String>) -> egui::Response { resp.widget_info(|| egui::WidgetInfo::labeled(role, resp.enabled(), name.into())); resp }Role::Tab, selected state,dock.rs:1149-1280), active/close buttons, the dock split handle, numeric scrub fields (widgets.rs:53-160), the color field/slider (color_picker.rs), the error bar prev/next buttons (expr_bar.rs:59-70), and the floating-panel resize grip (dock_ui.rs:367). Do not blanket-wrapui.interact: a node without a real name/role is worse than no node. The helper stays private toeffectcraft-ui-egui; there is one consumer (this app), so it is not proposed forcraft-uiand is not a generic compatibility layer.
Accessibility assertions (§8.4) must include roles, selected, enabled state, names, and the
AccessKit language once §5.9 lands. The app's ui_agent_audit.rs (automation coverage) stays
separate: an automation id is not an accessible name. docs/gaps.md:227 records that the project
has done no accessibility work; this stage is a first, tested step (visible stock controls and the
highest-value custom controls), not a claim of screen-reader parity.
5.4 Fonts, glyphs and text
- Enable
egui/default_fontsandegui/monochrome_emoji_fontsin the workspace dependency (see §5.1) soFontDefinitions::default()attheme.rs:334keeps the 0.36.2 font set. Do not rely on eframe feature unification for correctness. - Keep the app's two-pass font warm-up (
styled/fonts_ready,lib.rs:1876-1890,lib.rs:1905-1911): 0.37 may applyset_fontsimmediately when no text has been laid out (#8602,#8609), but relying on that would change when the first frame draws and would make headless tests depend on pass timing. Do not userun_logicto install fonts in a UI test. theme.rs:732:d.font.as_ref() == bytes→d.bytes() == bytes(and keepd.index).widgets.rs:329:TextEdit::vertical_align(Align::Center)→TextEdit::align(Align2::LEFT_CENTER)(the old call only changed the vertical component; the horizontal default was LEFT).- Update the comment at
theme.rs:354-360("egui … has no bidirectional layout"): the suite fork shapes RTL runs, but text layers still useeffectcraft-textand are unaffected. Add a UI-label RTL test (Arabic/HebrewLabel/menu text has visual-right-to-left run order) and keep the text-layer RTL test incrates/text/src/layout.rs:1419as the text-engine contract. - Keep
system_fontsoff. If colour emoji is wanted later, it is a separate change with a startup measurement and cross-platform screenshots, not part of this migration. - Fallback registration stays as is (
theme.rs:388-430), includingFontData::from_ownedanddata.index = face.info.index;FontDatastill accepts that construction in 0.37. Optional follow-up: E14FontTweak::fallback_alignfor the installed system faces, with CJK/Indic screenshots before and after. - Extend the existing glyph-coverage test (
theme.rs:700-716) to the full chrome symbol set used by the app, including▣,✔,✓,⚠,▶,◀,▸,●,○,…,×,›,•,·,−,←/→/↑/↓,⌘,⇧,⌥,⌃,⏎,↵,′,″, in Proportional, Monospace,mediumandsemibold. This is the regression test for finding 2.3.2. - Tests that deliberately render scripts with no installed face opt out explicitly at that test
builder:
ui_effect_names.rs:35(the builder insideharness()at line 20, used by thezh-hanstests at 68, 81, 107 and 127) andui_presets_folder.rs:40(ja; the builder is inside the test) useHarness::builder().allow_missing_glyphs()with a comment naming the script. No global opt-out. Test fonts come from the workspaceegui/default_fonts+monochrome_emoji_fontsfeatures (§5.1), not fromegui_kittest's non-propagating dev-dependencies. - Headless text rendering is not proof of native font fallback/IME; native platform checks remain (§8.7).
5.5 Input, IME, clipboard and PRIMARY
control.rs:371:Event::MouseWheel { …, source: egui::MouseWheelSource::Wheel }; the same in the 11 test constructions (§2.1). UseWheel, notUnknown, so synthetic scrolls are not treated like momentum/rubber-band input.viewer_text.rs:397-508(custom text editor): handle stockegui::ImeEvent::DeleteSurrounding { before_chars, after_chars }by converting the caret position to a char range and issuingtext.delete { range, merge: <same key> }before the followingImeEvent::Commit, so replacement is one undo step. Native winit emits the fork'sDeleteSurroundingBytes { before_bytes, after_bytes }(E21/W26/W27); add that arm in the language/IME stage, converting UTF-8 byte offsets at char boundaries viaTextDoc::text,TextDoc::char_of_byteandTextDoc::byte_of(crates/keyframe/src/text_doc.rs:527-545). Regression: preedit + delete-surrounding + commit replaces exactly the intended characters and adds one undo step; press-and-hold/dictation on macOS is verified manually (W26).- Incoming composition lifecycle (viewer_text). Each viewport stores
ImeComposition::Active(owner),TerminalPending(owner)orStale, where the owner is(layer, edit session)and excludes caret, selection, text, revision and geometry. A target switch ortext.endEditinvalidates both live states; theStaletombstone rejects every pre-edit/delete/commit from the replaced owner. An empty pre-edit clears the visible composition but recordsTerminalPending(owner): the visible pre-edit ended, while its terminal commit or delete-surrounding is still pending. A switch before that terminal event therefore still rejects it rather than applying it to the replacement. A non-empty old pre-edit is treated as a continuing update of that old composition and keeps the tombstone across arbitrary idle frames. A rejected terminal commit is the composition boundary and consumes the tombstone immediately, so a new pre-edit later in the same raw input batch is accepted for the current target. A rejected empty pre-edit or delete-surrounding is only tentatively terminal: it keeps the tombstone through the rest of the batch so a paired old commit is rejected too, and is consumed at batch end when no commit follows. Tests must separate the late pre-edit from its commit/delete by at least three idle frames, exercise an observed empty pre-edit followed by a target switch and a delayed old commit, verify a new pre-edit after a rejected terminal commit in the same batch commits to the new target, verify a genuinely new composition after the consumed terminal batch, and verify that no permanent lock remains. Do not replace this with an elapsed-frame guard: time is not causal ownership. - E21b/P7b outgoing surrounding text (new prerequisite, not existing behaviour).
Source inspection at the candidate revision found no call to winit's
WindowExtWayland::set_ime_surrounding_textanywhere underegui-winit, andIMEOutput(egui/src/data/output.rs:100-115) has only purpose/rect/cursor rect/interrupt. Deletion and commit handling alone therefore cannot pass the suite's surrounding-text contract.- The fork adds a bounded transient
IMESurroundingTextpayload toIMEOutput(logical text, cursor/anchor as UTF-8 byte offsets, owner/context generation identity). It is built from committed text only, excludes preedit, is skipped from debug/persistence output, and is per IME-owning widget/viewport. The owner generation is present even when support is unavailable; available-empty is distinct from unavailable and is sent as an explicit empty payload, never as an absent one. Changing owner/support (normal, password, unavailable) clears the backend cache and uses the protocol's committed disable/enable boundary; an empty text payload is not a general clearing API. - The final completed pass owns the outgoing value, including
None: the adapter must not useime.or(self.ime)-style carry-over that can retain an earlier pass's text.egui-winitcalls W27 on text/selection/focus changes; per-window caching cannot bleed between widgets or windows. The excerpt is at most 4,000 UTF-8 bytes cut at valid char boundaries. A selection that cannot fit returns an explicit unavailable context (or an explicit empty one when the document really is empty), never a truncated "complete" selection. - A current-epoch
Donewhose protocol serial does not match still applies its incoming edits exactly once and only defers outgoing surrounding state until a matching serial. A stale (different owner/focus generation) batch is invalidated and discarded. Track focus epochs and protocol serials separately. - No new document-sized allocation. Building the new bounded payload must not clone the full
document; the baseline
TextEditalready clones whole buffers for change detection/undo, and that pre-existing cost is measured separately. - EffectCraft custom adapter (viewer_text). The producer at
viewer_text.rs:568must supply the logical model text from the edited layer'sTextDoc(committed text), not display-bidi text, with preedit excluded (viewer-text-preeditstays a display-only temp value), plus the editor's owner generation unconditionally. ConvertTextEdit.anchor/caret(character indices,crates/engine/src/commands/text_edit.rs:18-40) to UTF-8 bytes withTextDoc::byte_of, clamp at char boundaries, excerpt to 4,000 bytes, and remap cursor/anchor to the excerpt. Emit an explicit empty payload for an available empty document; emit unavailable (and trigger the committed disable boundary) when there is no edit session, when focus leaves the viewer, or aftertext.endEdit/Escape/click-away, so stale text cannot survive. The editor has no password mode; the stock-field password/unavailable clears are tested there. The payload is scoped to the viewport that owns the focused editor (floating panel windows and the main window cannot share state). - Atomic compatibility.
IMEOutputlosesCopywhen the payload is owned. The P8 fork SHA, all egui-family crates, thecraft-uirevision and EffectCraft's singleIMEOutputliteral (viewer_text.rs:568; there are no destructures in this repo) must change in one dependency unit. Do not land a mixed stock/fork lock, and do not claim a stock-0.37 app build after this stage: the literal would not compile against stockIMEOutput(§11.13). - Tests: outgoing text immediately after delete+commit and deletion-only batches; scalar/
grapheme vs byte indices; supplementary emoji, combining marks and RTL logical order; preedit
exclusion and caret movement; long selection/excerpt limit and allocation bound; same-window
field switch; two windows; focus loss and late serial; current-epoch mismatched serial applies
incoming edits once and defers outgoing; stale-epoch discard; available-empty vs unavailable
and normal/password/unavailable transitions; final-pass
Noneauthoritative; multi-pass final output; native Wayland Vietnamese/predictive IME protocol trace. macOSNSRangeconversion remains a separate W26 path; this does not claim new macOS surrounding-text APIs.
- The fork adds a bounded transient
- Clipboard:
eframe/clipboardfeature (§5.1).ctx.copy_text/output.commandspaths atlib.rs:1600-1612,lib.rs:1946-1953,viewer_text.rs:413-430,menus.rs:1867-1906and the macOSHooks::clipboard_text(lib.rs:159-160,apps/effectcraft/src/main.rs:326) stay unchanged. - PRIMARY (E4 + W15P) is a native gate, not one manual row. Upstream 0.37 fills PRIMARY from
Label/TextEditselections and pastes on middle click (#8428); the reviewed fork replaces the arboard fallback with a winit-owned PRIMARY path on the existing Wayland connection/event queue, using SCTK 0.19.2'sPrimarySelectionManagerStateand per-seat device/source/offer objects. Never create a second regularwl_data_device.set_primary/get_primaryroute through it; password selections are never offered; an unchanged selection does not reclaim ownership. Serial handling accepts keyboard and pointer press serials for the focused seat with origin, freshness and epoch (do not invent a keyboard-only rule — the protocol describes pointer-driven selection). Claim the selection synchronously while the triggering serial is fresh; only serving the selection is asynchronous. Reads are bounded: a 2 s total monotonic deadline, 16 MiB total bytes, at most 4 read workers and 2 writers with a bounded queue of 8. A live owner-specific completion waker wakes the owning viewport after the slot lock is released and never blocks the UI; canceled or stale results are discarded, and a stale paste is canceled after focus/target/generation changes. The native capability matrix in §8.7 covers wlroots/Hyprland, KWin and Mutter; ordinary paste is not PRIMARY acceptance. An isolated PRIMARY-only fallback requires measured necessity and review. - Non-Latin layout shortcuts (E7/E19/W25): headless tests cannot exercise winit's mapping. Add a
manual row (Russian, Arabic, Hebrew) that checks Menu shortcuts and the timeline/Project Ctrl+C/V/X
paths, and keep
parse_shortcuttests (menus.rs:2229-2265) as the app-level contract. Use a non-clipboard held-key case (e.g. Ctrl+Z) as well, because clipboard shortcuts return before emitting Key-down.
5.6 Menus, popups and docking
menus.rs:2025-2050: keepMenuBar::new().config(MenuConfig::new().style(menu_style)).ui(...); 0.37 keepsMenuConfigand addssubmenu_gap. Regression tests: hover-switch between top-level menus, arrow navigation (menu_keys::Nav), opening a submenu, Escape/outside-click closing, and a menu command that acts on the focusedTextEdit's selection (#8763). That last case is P7c's selected fix in the P8 revision, not an app workaround.menu_keys.rs:84: scope id (see §5.2).menu_keysalso usesui.is_sizing_pass()for popup sizing (already); keep and test that a sizing pass does not invoke a command twice.- Dock: the layout and drawing are app-owned (
dock.rs,dock_ui.rs); onlycraft_ui::dockingactions are shared. Preserve panel order/nesting, clip rects, split sizes and IDs. 0.37's "Panel/Scene clip rect respects parent" (#8562) needs a clip test for a maximized panel and an embedded (web) floating panel. - Root Ui order (
lib.rs:1614-1723): menu bar strip, header, dock body, native drop overlay, mini flowchart, dialogs, ScriptUI windows, coach, import card, toast. There are no sidePanel::left/top/...calls; the dock is an explicit child Ui with computed rects. Keep that order and nesting (it decides hover/layer order and clip rects).CentralPanelis used only inside floating-panel and video-preview viewports (dock_ui.rs:475,prefs_live.rs:207). - Do not enable
Style::right_to_left(E9); the dock and viewer are custom LTR geometry.
5.7 Screenshots (control channel, kittest, hidden windows)
Replace token-via-input-event correlation with the shared CL-2 completion slot (§3.3). Do not add an unbounded or app-local result channel: the slot is owned by the app request record, and the callback holds only a weak reference to it.
struct PendingShot {
token: u64,
path: Option<String>,
crop: Option<[f32; 4]>, // logical points, as the control protocol already takes
reply: Sender<Value>,
deadline: f64, // app clock
admitted: (egui::Rect, f32, u64), // screen rect + ppp + geometry epoch at acceptance
issued: Option<(egui::Rect, f32, u64)>, // target-UI rect + ppp + geometry epoch at issuance
slot: Arc<capture::CompletionSlot>, // CL-2F helper type (API frozen)
reserved_bytes: usize,
}
- Admission. Use the shared default policy: at most 4 outstanding requests, 128 MiB per
image and 256 MiB aggregate reserved in-flight/completed bytes. Stricter app caps are
allowed, but never looser. Estimate the target surface with checked
width * height * 4from the known UI geometry, reject over-budget requests before issuing a capture with an actionable reply, and revalidate the reservation at issuance. A post-callback size check alone does not bound peak capture memory; the reservation does. - Issue.
ctx.request_screenshot(callback)sends a viewport command that itself forces a repaint, and a pending screenshot makes a hidden window take a full UI+paint pass. A screenshot that is merely queued and not yet issued, and settling deadlines, must havetick/logic sendRequestPaintWhileHiddenso the UI reaches issuance; do not add a second wants-paint request once the screenshot command has been issued. At issuance record the target-UI logical content/screen rect, ppp and a geometry epoch. That issued geometry + epoch, not callback metadata, is bound to the image: the callback carries only pixels. - Completion. The app owns the strong slot and the owner-specific waker; the callback holds
only a weak reference, stores the image unless canceled/completed, takes the wake handle, drops
the slot lock, then requests the owning viewport's repaint. The callback does not wake itself and
must not strongly capture a
Contextor the app; that would create a cycle with the viewport's queued command.collect_screenshotsdrains completed slots intickand encodes/writes/replies outside the callback. - Geometry/scale binding. With an unchanged geometry epoch, derive the pixel scale from the returned image dimensions against the issued rect and verify aspect/rounding; the returned image may be smaller when E2 surface fitting applies. Any intervening resize/DPI/zoom/surface epoch change between admission, issuance and save cancels and retries the request — even if the geometry later changes back before the save. Never crop with the admission-time, current or any other ppp that disagrees with the issued epoch. Store admitted/issued/captured values and record mismatches; an explicit error is correct when a retry cannot complete inside the deadline.
- Cancel/timeout/drop. Removing the request record (timeout, close, app drop, caller drop) marks its slot canceled under the lock; a late callback finds no live slot or a canceled one and drops the image without a second reply or file write. Release the reserved bytes on every exit path (completion, timeout, cancellation) and assert retained bytes return to zero after drain.
save_screenshottakes the actual scale/geometry instead of&egui::Context(control.rs:570-606), so it can run intick.issue_screenshots(lib.rs:1530-1558) uses the tick'snowand a settle deadline, not a per-pass frame counter;collect_screenshots(lib.rs:1560-1580) becomes the slot drain.- Remove the "occluded/minimized" refusal at
lib.rs:1490-1498for the native wgpu path only after the platform run shows the screenshot request really produces a frame. The refusal exists because 0.36 never painted a hidden window; 0.37 makes a pending screenshot a wants-paint action (wgpu_integration.rs:680-690,egui-winit/src/lib.rs:2116-2128). Keep the timeout error and addui.focusas the fallback. Record Windows/macOS/Linux results separately. Do not claim hidden-window screenshots on a platform that was not tested. On web, eframe does not paint a hidden tab (logic-only passes have no clipped primitives), soui.screenshoton web requires a visible tab; record that limitation. - Correlation tests: two concurrent screenshots with different crops; callback-once behavior; a timed-out request followed by a late callback (no second reply, no file/PNG); shutdown/close with a pending capture; aggregate byte-budget rejection; a resize/DPI change between admission, issuance and completion (retry or explicit error, never a stale crop); a hidden-window capture on native wgpu is a positive row (not a "missing RequestPaint" negative); retained bytes return to zero; the app/Context can be dropped without the callback keeping it alive.
docs/control-protocol.md:125-129and the module docs atcontrol.rs:1-27need the new wording (no user-visible protocol change:ui.screenshotkeeps its method and reply shape).
5.8 Viewports and GPU
- Floating panels (
dock_ui.rs:466-512) useshow_viewport_immediate; the video preview uses the same (prefs_live.rs:166-236). Keep them insidedraw. Persisted window rects (PanelWindow::sanitized,ui.panel_windows) are unchanged. - Hidden playback and child windows.
show_viewport_immediatechildren only update when the parent runs a pass. If playback is playing while the root window is minimized/occluded, or a floating panel/video preview is open,tickmust keepwant_paintset (and request a repaint at the needed cadence) so the parent runs the forced hidden pass. Do not rely on the one-shot open/close command mitigation alone. Test a minimized-window playback with the Video Preview open and with a floated panel, asserting the child content advances and the parent does not thrash. - 0.37's
#8748(a hidden parent does not update deferred children) is carried as P7c's selected fix in the final P8 revision; verify it in the hidden-playback test. Independently, a command that opens/closes/floats a panel or video preview also setswant_paint, and playback keeps the pass cadence, so the app does not depend on one mechanism alone. - GPU:
egui_wgpu::Renderer'sregister_native_texture_with_sampler_options,update_egui_texture_from_wgpu_texture_with_sampler_optionsandfree_textureare unchanged in 0.37 (renderer.rs:790-930), soviewers.rs:150-210needs no API change.RenderState/Frame::wgpu_render_stateare unchanged. Keep E2+E3. The existingui_gpu.rsGPU-vs-CPU tolerance (at most 0.3% of pixels differ by more than 1/255, worst channel difference ≤4) stays. - A skipped GPU test is not evidence.
ui_gpu.rsreturns early wheneffectcraft_gpu::Gpu::headless()finds no adapter (tests/ui_gpu.rs:44-47) or when egui-wgpu's GL device has WebGL2 limits (tests/ui_gpu.rs:62-65). For the GPU acceptance row run with a required adapter, assertgpu_adapter().is_some(), and record adapter/backend/driver and the exact test invocation. The planning host exposes an i915 render node (/dev/dri/renderD128) with no display session; that can qualify a headless wgpu adapter run, not presentation. - Device loss must be handled from
tick(see §4.2); theui()early return currently means a device lost while hidden is not acted on (lib.rs:1905-1910). Add a logic-only test that a reported failure closes the window and keeps the auto-save. - 0.37
Frame::set_wgpu_surface_configis fixed (#8442); EffectCraft does not use it. Do not add it as part of this migration.
5.9 Language and screen readers (fork-only stage)
When craft-libs#198's fork APIs are accepted:
- In the app tick, after
i18n::set_current(language), callctx.set_language(Some(language.to_string()))(fork E17) and, where aneframe::Frameis available,frame.set_ui_language(Some(language))(fork E18). This sets the AccessKit root language and widget text layout language. It does not change the app's catalogs. - Assert
ctx.language()follows the interface language, the AccessKit root node carries the language, and aTextEdit's text is shaped with the UI language (CJK glyph choice where fonts are installed). Tests that need the fork API are marked fork-required in their module docs. - Keep the app LTR (
Style::right_to_leftunset). Text-layer RTL remainseffectcraft-text's contract; UI-label bidi is epaint's. - The E17/E18 policy sub-stage is optional and separately revertable. The production app already requires the final fork and E21b from P1; a stock-0.37 control uses a separate minimal adapter and is not obtained merely by reverting P4 (§11.13).
6. Dependency topology, publication reachability and rollback
Intended graph (final reviewed storytold revisions; names shown for structure):
effectcraft / effectcraft-web / effectcraft-cli
-> effectcraft-ui-egui -> craft-ui (craft-libs rev with CL-1 + CL-2F)
-> egui 0.37.0 (git <final storytold egui P8 SHA, incl E21b/P7c>)
-> epaint, epaint_default_fonts, ecolor, emath (same git rev)
-> eframe, egui-wgpu, egui-winit (same git rev)
-> accesskit 0.25.1, kittest 0.5.0
-> winit 0.30.13 (git <final storytold winit SHA, incl W15P>)
-> winit (direct, same git rev)
- Every egui-family crate EffectCraft resolves must come from the same git revision.
cargo tree -dmust show no second egui family, andcargo tree -i winit@0.30.13exactly one source. - Publication: EffectCraft is an application, so a git egui/winit dependency is acceptable in its
lock, but no branch names, no
pathpatches and no machine-local checkout may appear in a publication branch. Theaionggcandidate SHAs are comparison inputs and must not appear in a publication lock.craft-libsconsumers keep their own patches; the app's[patch.crates-io]is the supported place for the suite fork. - A clean checkout must fetch every final approved revision and pass
cargo check --lockedandcargo test --lockedwithout local overrides; record that proof with the lock. - Rollback: keep the exact baseline lock (egui family 0.36.2, craft-ui
5c874d51, winit registry 0.30.13) in the task evidence. The project file format, command ids, undo model, workspace-state version 1 and the GPU compositor are unchanged by this migration, so rollback is a revert of the pin/refactor commits, with no data migration. Do not persist any 0.37-only preference until the final stage, so an older build ignores nothing it cannot read. examples/plugins/posterize-bandsis a separate workspace with its own lock and no egui; do not edit it.craft-pdf/hayro-syntaxand the filmcraft pins are unrelated and stay.- There are no vendored crates in this repository (
vendor/absent); the only patch is the craft-libshayro-syntaxentry. Do not delete it, and do not "clean up" the commented filmcraft path patch by enabling it.
7. Command, undo, save/reopen contracts
Nothing in the egui bump may change these:
- Every gesture still routes through
effectcraft_engine::Session::executeormenus::invoke(lib.rs:1-6,control.rs:1-19); UI-local state must not become the source of truth. - One gesture = one undo step: existing tests such as
ui_viewer.rs:2263(fit/zoom),ui_viewer.rs:2306(rotate),ui_project_delete.rs,ui_timeline_trim.rsandui_keyframes.rsstay green.Session::history.merge_keyis reset when a numeric scrub ends (lib.rs:1866-1867,pointer_wrap.rs:31-36). - Save/reopen:
.ecprojJSON contains no egui types; save, reopen and pixel-compare a comp after a UI edit. Workspace state (workspaces.json, version 1,workspace_state.rs:37-40,85-88) containsPanelKind, rects and floats, not egui ids; save, restart and restore the dock/floating/ window layout. If a new field is ever required, specify an explicit version bump, a migration and a backward-compatible reader test; do not silently reuse version 1. - Multi-pass must not duplicate a command, an undo step, an autosave write or a close prompt (§4.4, §8.3).
- Auto-save while hidden has two owners today. Project auto-save/recovery already runs from
tick; the workspace-layout autosave is still indraw(lib.rs:1715-1722, gated on!ptr.any_down()andworkspace_store.due(now)). P2 moves its due check and write totickusing the app clock and the last pointer-down state observed indraw. Only then does the hidden claim cover both kinds; the close guard and crash recovery remain tick-owned. §8.3's test asserts both writes while hidden.
8. Tests and acceptance
8.1 Repository gates
Every stage runs the repository gates (through the server job workflow, never directly from the
planning task): cargo fmt --check, cargo clippy --workspace --all-targets --release -- -D warnings,
cargo test --workspace --release, cargo xtask layers, cargo xtask assets, cargo xtask wasm,
plus node apps/effectcraft-web/tests/{workers,page,storage}.mjs (no build needed).
On every dependency stage also run cargo check --locked, cargo test --locked,
cargo tree -d, the family/winit inverse trees and the wasm-target wasm-bindgen tree on a clean
checkout. cargo xtask ci is the final gate.
8.2 Compile and API contract
- The 15 errors and 602 warnings above are gone, with no
#[allow(deprecated)]added to production code. - Grep gates must use precise patterns:
The old
git grep -nP '\bId::new\(' -- '*.rs' # only comments/none git grep -n 'WidgetType\|AtomLayoutResponse\|\.vertical_align(' -- crates apps git grep -n 'with_accessibility_check(false)' -- crates apps # must be empty at every stagegrep -rn "Id::new"also matchesGlyphId::newincrates/text/crates/pdf(7 lines) and can never return "none";\bId::new\(does not match insideGlyphId. ui_agent_audit.rs:73usesWidgetInfo.role(typis gone); all tests constructMouseWheelwithsource.- Tests for
Id::uniquestability and for the role mapping table.
8.3 Lifecycle, hidden windows and multi-pass
New tests (headless, Context::run_logic + run_ui, no GPU). The driver must mirror the real
eframe order: raw_input_hook-equivalent (feed_synthetic) → run_logic → retain the mutated raw
input → next run_ui, and it must not replay input events from tick. Use AppClock::manual():
logic_only_drains_control_and_jobs: a control request that only needstick(e.g.engine.executeof a non-UI command) gets its reply with zerorun_uicalls;poll_rendercompletes a render job and the Toast/event is drained. Advance the manual clock between ticks.logic_only_feeds_synthetic_and_forces_one_paint: with the root viewport markedminimized: Some(true)(sovisible() == Some(false)), a queuedui.clickis fed by the hook during a logic-only tick; the tick emitsRequestPaintWhileHiddenand sends no reply. A second logic-only tick does not feed another event while the first is still unapplied. The following forcedrun_uisequence (still minimized) consumes one pointer event per pass, applies the buffered gesture exactly once and replies exactly once after the final pass. The same test asserts that the forced passes are not gated onvisible()and that no event was dropped.after_input_replies_only_after_a_paint: same, and assert the command's effect exists before the reply is sent; force a discard on that pass and show the reply still waits for the final non-discarded pass.forced_hidden_pass_applies_buffered_gesture: feed during a logic-only tick, run forced full passes untilsyntheticis drained, and assert exactly one command and one undo step (no duplicate from the logic-only call).playback_advances_without_a_paint: with a viewer scale recorded and the manual clock, hidden ticks advancesession.timewithin the preview step and do not duplicatePlayback.frame_shown; audio is started once (stubaudio_device); a stalectx.input(|i| i.time)cannot satisfy a deadline.autosave_and_close_guard_run_while_hidden: project auto-save and the moved workspace-layout autosave both write in logic-only ticks; a hidden close request opens the unsaved dialog and sendsCancelClose.discarded_pass_is_idempotent: forcectx.request_discard(or show a first-timeGrid) while performing one command, one drag and one auto-save; assert one command execution, one undo step, one file write.device_loss_while_hidden_closes_and_saves: bridge a failure during a logic-only tick; assert the close command and the auto-save, as inlib.rs:2040-2075.hidden_playback_keeps_child_viewports: with playback playing and a Video Preview or floating panel open, hidden ticks keep requesting a pass and the child content advances (§5.8).- Screenshot-slot tests from §5.7 (concurrency, cancellation, late callback, resize/DPI including a change back, byte budget, shutdown).
repeated_logic_only_ticks_do_not_collapse_a_gesture: queue aui.clickand aui.drag, run several logic-only ticks (mirroring eframe's overdueRedrawRequestedand 100 ms invisible throttle) while the root viewport stays minimized, and assert the hook fed at most the one unapplied event; then run forced full passes and assert press/move/release each reached a separate pass, exactly one click/drag command and one undo step resulted, and no event was dropped. Repeat for a click followed immediately by a drag.
8.4 Screen geometry, pixels and accessibility
- Fixtures: the built-in demo project (
file.openDemoProject) for visual states, the generatedengine::perf::large_projectfor scale/perf, and the bundled Inter/JetBrains Mono fonts. No external media is needed for UI geometry/accessibility/pixel tests; media-dependent tests keep their existing generated fixtures. - Geometry/hit tests (existing helpers, automation rects): viewer comp rect and fit at 1.0/1.25/1.5/
2.0 ppp; dock split resize; tab drag and overflow; floating panel window (native only; embedded
web path separate); menu item click at a label; timeline row heights (
ui_parameter_spacing.rskeeps 20/19 px); panel order/nesting after maximize/restore. - Double-click paths: 24
double_clicked()call sites (27 textualdouble_clickoccurrences including the Timeline helper, e.g. project renameproject.rs:783,832,1036, Timelinetimeline.rs:1502,2116,2325,2516,2876,3529, Flowchartflowchart.rs:360): 0.37 changed double/triple-click classification (#7915). Assert one double-click performs exactly one action and does not also fire a triple-click path. - Visual review harness — correct the claim and extend it. Today
crates/ui-egui/tests/ui_visual_review.rs(UI_REVIEW_DIR,#[ignore]) captures the workspace in light and dark, but everyPanelKind::ALLpanel and the About/New Comp/Settings/Shortcuts/ Command Palette dialogs only in dark, at the harness default ppp 1.0, at wide 1600×1000 and compact 1024×768 (ui_visual_review.rs:13-48). Extend it to capture every panel and dialog in both themes at ppp 1.0 and 2.0 (four combinations per state, both sizes), then use it on the baseline and every stage. This is the primary before/after visual record; the golden set below is the automated subset. Record that the ignored test actually ran; an ignored/absent capture is not evidence. - Pixel comparisons: choose one of
egui_kittestsnapshotgoldens (enable thesnapshotfeature, addkittest.tomlwiththresholdper OS, commit a small fixed set of PNGs undercrates/ui-egui/tests/snapshots/with.attributionsidecars andATTRIBUTION.mdrows, because the repository's asset rule applies even to screenshots); or- an app-owned tolerant compare against RGBA hashes stored in a Rust constant, with the same tolerance policy and no image assets. Exact selected set: five states — Standard workspace (demo, time 2 s), Timeline maximized with rows open, Project panel with folders open, a menu open, and the Settings dialog — each in light and dark at ppp 1.0 and 2.0 (20 captures), not a single dark/default-scale set. Threshold: max channel delta 2/255, ≤0.1% failed pixels on Linux/Windows, 3/255 on macOS (if any platform-specific rasterization is observed); no tolerance for geometry rects. Never accept a new golden automatically; a changed golden needs a reasoned explanation in the PR.
- Mutation check (required, recorded as evidence, not committed as a test): nudge
Tokens::focusor a panel width by a visible amount behind an env flag and show that the pixel/geometry test fails with the expected diff; restore. This proves the assertions detect change. - Accessibility: strict checks throughout;
Harness::unnamed_widgets()empty for the workspace, for each panel maximized, for every dialog inDialog, and for the Home/Media Browser/Effect Controls/Properties/Timeline panels. Role contract tests for color well, tab + selected, splitter, tree item, text input, spin button, slider and a checked menu item.Role::Button+selectedfor selectable labels. Read-only fields are exempt by kittest. The repo-wide grep gate in §8.2 proves no global/per-builder accessibility opt-out remains. - Glyph coverage: the extended chrome symbol test (§5.4);
ui_effect_names.rs:35andui_presets_folder.rs:40useallow_missing_glyphs()with a comment. No global glyph opt-out. ui_gpu.rsandui_visual_review.rsare skip/ignore-capable: the acceptance record must name the exact invocation and show they actually ran. A skipped GPU test is not GPU evidence (§5.8).- Screenshot tests (§5.7) and IME/adapter tests (§5.5).
8.5 Save/reopen and command/undo workflows
- UI edit → save
.ecproj→ reopen in a freshEffectcraftApp→ project JSON equal and viewer pixels equal; undo/redo across the save still works. - Dock/floating/panel-window layout → save workspace → restart → restored; maximize/restore and split sizes unchanged.
- One command from the menu, one from the control channel and one from a synthetic click each produce one undo step; a discarded pass does not add another.
8.6 Web workflow
cargo xtask wasm(all L0–L4 + ui-egui + web) andcargo xtask web --serve 8765.node apps/effectcraft-web/tests/smoke.mjs --url http://127.0.0.1:8765/ --out <evidence>: WebGPU/GPU-compositor path, viewer,render.frame, Render Queue downloads, background worker event-loop gaps (<400 ms), AudioContext, save/reopen, persistence across reload, offline service worker. Alsoworkers.mjs,page.mjs,storage.mjs.- Browser screenshots come from CDP (
smoke.mjs:113), independent of the egui screenshot API; add a page-side check only for the in-appwindow.effectcraft.request("ui.screenshot", …)reply shape when the tab is visible. A hidden web tab cannot paint (logic-only passes carry no clipped primitives), soui.screenshoton web is a visible-tab feature; record it. - Web is a separate feature set: no OS clipboard/PRIMARY, no system fonts, embedded viewports.
- Hidden-tab behaviour, realistically. Upstream 0.37's web runner already falls back from
requestAnimationFrametosetTimeout(10 ms)whendocument.hidden()(eframe/src/web/web_runner.rs:238-279), soAppRunner::logickeeps running with logic-only passes and the browser clamps background timers to roughly one second (long-backgrounded Chrome may throttle further). Assert that the session snapshot's timestamp advances across the hidden period at the browser's actual cadence, that frame workers keep completing within that allowance, and that the viewport is fully refreshed when the tab returns. Record the browser, visibility transition and measured cadence; do not require exactly 1 s and do not add a new worker/timer scheduler or page-independent persistence feature in this migration. FixWebApp::logicto pass the app clock (self.app.now()) topersist::snapshot, notctx.input(|i| i.time)(which is stale in a logic-only pass).
8.7 Native platform matrix
| Row | Required evidence |
|---|---|
| Linux headless wgpu (this server) | ui_gpu.rs with a required adapter on the i915 render node (/dev/dri/renderD128, no display session): assert gpu_adapter().is_some(), record adapter/backend/driver and the exact command. This qualifies a headless GPU-compositor run only, not native presentation. |
| Linux X11 | Native window under Xvfb + a real window manager (e.g. openbox) + an X11 input method (e.g. ibus/fcitx): screenshot, save/reopen, floating panel window, focus/menu/close/auto-save, clipboard and PRIMARY between windows, X11 IME (Vietnamese/Japanese) with editor/undo checks. Record Xvfb/WM/IME versions and that the session is virtual and not composited. This can qualify the X11 parts; it cannot qualify a physical GPU or Wayland. |
| Linux Wayland | Requires a real compositor; unresolved unless a managed environment is provided. Run the E4/W15P PRIMARY native capability matrix on wlroots/Hyprland, KWin and Mutter: protocol availability, keyboard focus, keyboard and pointer-selection serial freshness, owner/seat change, empty/absent offers, self-paste, slow/canceled transfer, password exclusion, unchanged-selection reclaim, and no second regular data device (protocol trace). Also trace text-input-v3 current-epoch mismatched Done (incoming applied once, outgoing deferred), stale-epoch discard, and disable/enable on owner/support changes. Record unsupported capabilities honestly; do not substitute X11/arboard results. |
Windows (craft-windows clone, separate task) |
real native build/test, GUI launch, screenshots, menu/close/auto-save, clipboard, IME, DX12 pipeline startup timing, mixed-DPI window placement, and a held Ctrl+Z release after a layout switch; Basic Render Driver is software and must be reported as such. |
| macOS | unresolved gate unless real hardware evidence is provided: press-and-hold/dictation and surrounding-text NSRange (W26), Command-key shortcuts, traffic-light/titlebar placement, screenshots while occluded. |
| FreeBSD / Nix / Windows ARM64 | CI workflows are reusable craft-common-actions; run after the pin lands and report only what the runners provide (FreeBSD nightly, ARM64 MSI on schedule). |
Headless kittest is not evidence for native focus, IME, file pickers, clipboard, PRIMARY or GPU presentation. Physical-GPU and Wayland rows remain explicit unresolved gates until measured.
8.8 Known upstream regressions as acceptance gates
#8731(text tessellation, reported +61% instructions for plain text): measure with the paired text-heavy operations (workspace, timeline, project) and the rawtessellateinstruction count where possible. This measurement belongs to the pre-P1 gate. If the median regression is above threshold, the row-wise fix (provenance, test, removal condition) must be in the reviewed P8 revision before P1 is frozen, or the migration is blocked. P5 may not add it afterwards.#8736(DX12 pipeline creation, reported ~40 ms): measure Windows startup and first-frame cost separately for the DX12 backend. If it pushes startup over threshold, the fix must be in P8 before P1 or accepted with an explicit owner decision and the measurement; P5 only re-reports it.#8512(system font startup cost): keepsystem_fontsoff and record font-install/discovery time in the startup row so a later color-emoji decision has a baseline.#8489/#8583/#8601/#8589are handled by this plan's refactors.
9. Performance plan
9.1 Baseline before production changes
Add instrumentation first (stage B0, no behaviour change): raw samples per operation, p50/p95/p99,
the number of final paint passes and discarded passes (Context::will_discard() at the end of
draw), texture-upload bytes per frame from FullOutput.textures_delta, logic-only tick time,
startup-to-first-frame and font-install/load time, and an optional counting global allocator in
effectcraft-cli for bench --ops (byte counts per operation). Record raw and compressed
effectcraft-web .wasm sizes. Measure gains samples, median, p95, p99, cpu_ms;
existing names and JSON fields stay. Then run the B0-instrumented baseline: commit 9581b53c
- the instrumentation patch, with its patch SHA and the 0.36.2 lock recorded. The baseline identity
is B0-instrumented
9581b53c, not the bare commit. The same instrumentation is applied to the candidate so both sides are measured identically.
9.2 Matched operations
| Operation | Command / driver | Sample rule |
|---|---|---|
| Startup to first frame | bench --ops --json startupToFirstFrame; native log "first frame drawn" |
≥20 fresh processes per source/lock; same large project, fonts, window 1680×1020 |
| Steady workspace frame | workspaceFrame |
≥10 warmups, ≥30 timed iterations per session, 3 sessions |
| Timeline scroll / open rows | timelineScrollFrame, timelineOpenFrame |
5,000 layers, every 10th open; ≥30 iterations |
| Project panel scroll | projectPanelFrame |
all folders open; ≥30 iterations |
| Menu open / dock resize / typing | new menuOpenFrame, dockResizeFrame, textTypingFrame |
fixed input scripts; ≥30 iterations |
| Frame distributions | scripted workspace/timeline/typing loop or native playback | ≥1,000 actual final paint passes per row; record discarded passes and dropped frames; p50/p95/p99 only with ≥1,000 |
| Logic-only tick | new logicOnlyTick |
1,000 run_logic ticks, idle queues; asserts no paint work |
| Idle repaint rate | new idleRepaints60s using Context::set_repaint_observer |
virtual 60 s; count causes |
| Playback | native preview long enough for ≥1,000 presented frames (e.g. 20 s at 60 fps, or repeated 10 s segments with a fresh start) | actual pass count, dropped frames, audio clock; ui.inspect.fps is a smoothed readout, not a distribution |
| Texture uploads | bytes/frame from textures_delta |
viewer frame and thumbnails |
| Allocations | counting allocator totals per op | optional but recorded |
Font startup (#8512) |
cold start with system-font provider off; record font discovery/install/load | ≥20 fresh processes; part of startup evidence |
| WASM size | cargo xtask wasm artifact |
raw and gzip/brotli sizes, baseline vs candidate; accept a budget or make emoji fonts native-only |
| Engine control | cargo xtask bench --quick --repeat 5 |
guards accidental engine/render drift |
| GPU vs software | same UI ops with llvmpipe/software GL and a required hardware adapter where present; Windows Basic Render Driver separately | separate tables, never merged |
9.3 Environment and protocol
Record: CPU/GPU model, RAM, OS, driver, display scale, window size, renderer backend, release
profile, exact commit/SHA/lock/craft-ui/egui/winit revisions, font set, fixture project SHA, and
load average. Every row uses ≥10 warmups and ≥30 timed iterations for steady operations,
≥20 fresh processes for startup, and ≥1,000 actual final paint passes for a frame
distribution. Report the exact frame counts, discarded passes, dropped frames, audio underruns, raw
samples, median/p50, p95 and p99; do not report a p99 from 30/90 samples. Measure peak RSS
(/usr/bin/time -v or -l) and idle CPU (60 s); use perf stat -e instructions:u,cycles:u where
available. Use cpu_time/instructions and wall time; on a shared host, wall time is secondary.
Software renderers and physical GPUs are separate tables.
Predefined thresholds (owner sign-off required to widen):
- steady/scroll/project/typing median ≤ +5%, p95 ≤ +10%;
- startup median ≤ +5%, p95 ≤ +10%, using the shared small-operation noise floor;
- logic-only tick median ≤ 0.5 ms and p99 ≤ 2 ms on the large project (no paint work);
- idle repaint count ≤ baseline per minute; no new recurring repaint while idle;
- texture upload bytes per steady frame ≤ +10%;
- peak RSS ≤ +5%; record absolute MiB as well; no automatic absolute-size exemption;
- engine render benchmark (
cargo xtask bench) within its own noise band; no regression; - text tessellation (
#8731) and DX12 startup (#8736) are reported as their own rows and must meet the same median/p95 or be fixed/accepted explicitly.
Noise policy: run the unchanged 0.36.2 build twice as a control. The first report must also run stock egui 0.37.0 as the required upstream-equivalence measurement control (not a production pin), and report regressions only against these controls. If control spread prevents resolving the shared thresholds, report inconclusive and rerun under quieter conditions; never widen a threshold automatically. If a regression exceeds the gate, re-run paired 3× and profile; never update a golden or baseline to hide it. A changed pixel golden is a reported visual change with a reason, not a performance result. No performance number is claimed by this document; the thresholds are proposals to be validated by paired runs on recorded hardware.
10. Staged implementation PRs
Dependency DAG: B0 → P1 → P2 → P3 → P4 → P5. P1 waits for the complete reviewed/published
storytold egui P8 (E1–E21, P2b async W15P routing, P7 held-key pairing, P7b/E21b, P7c's
selected #8763/#8748 fixes and any justified #8731/#8736 perf fix), the completed winit
(W1–W27, W15P, W27 lifecycle), and craft-libs CL-1 and CL-2F after its DesignCraft/GridCraft
pilots. P1 includes the compatible IMEOutput producer and all required fork API adaptations.
There is no stock or intermediate-fork production stage. If a consumer already has an
incompatible craft-ui/egui bump, it must be reconciled in this same atomic stage. P4 adds native
language/platform evidence and optional language UI policy; reverting it never removes the P1
compatibility adapter. P5 is measurement/docs only and requires final platform and performance
acceptance.
| Stage | Scope | Validation | Rollback |
|---|---|---|---|
| B0 | Bench instrumentation only: samples/percentiles, final/discarded pass counters, logic-only tick, repaint observer, texture-byte counter, font-startup and WASM sizes, optional allocator; baseline run on B0-instrumented 9581b53c (record patch SHA and lock) |
bench --ops --json raw evidence; no behaviour change; gates |
revert commit; baseline evidence retained |
| P1 | Final reviewed storytold dependency pins (complete egui P8: E1–E21 + P2b + P7 + P7b/E21b + P7c #8763/#8748 + justified perf fixes; completed winit W1–W27 + W15P and W27 lifecycle fixes; craft-ui with CL-1 + CL-2F), including the viewer_text surrounding-text adapter and compatible IMEOutput literal, feature clipboard, default_fonts + monochrome_emoji_fonts; mechanical API migration (IDs, roles, Ui::scope_id, Atom/IdSalt, font bytes, TextEdit::align, MouseWheelSource, screenshot callback via the CL-2F slot, test Events); actual strict kittest accessibility names and glyph fixes until all tests pass with check_accessibility: true and missing-glyph Panic, with allow_missing_glyphs only on the two named script tests. No with_accessibility_check(false) anywhere. |
cargo xtask ci; cargo check/test --locked; cargo tree -d; one-family/one-winit and wasm-target checks; all tests compile and pass |
revert P1: restores the 0.36.2 lock; no data changes |
| P2 | Lifecycle: tick/draw split, injected app clock, focus, polls/playback/audio/project and workspace-layout autosave/events in tick, hidden RequestPaintWhileHidden (including queued screenshots, settling and playback/child viewports), synthetic-input generation guard, input-waiter ordering after draw, close guard, screenshot scheduling ownership |
§8.3 tests; native hidden/minimized manual check; benchmark logic-only/idle | revert P2: returns work to ui; behaviour-only, no data |
| P3 | Accessibility completion (custom roles/names), glyph coverage, extended light/dark × 1.0/2.0 visual harness, geometry/pixel tests, style/visual adjustments if the tests show unintended movement | §8.4 tests, cargo xtask assets (only if PNG goldens), a required-adapter ui_gpu.rs run |
revert P3: tests and names only (except any visual fix, which is isolated) |
| P4 | Extended language/platform qualification: set_language/set_ui_language policy where applicable, RTL UI-label tests, PRIMARY native capability matrix, clipboard/non-Latin shortcut and native IME rows. E21b/IMEOutput compatibility is already part of P1. |
fork-required tests and native matrix using the same coherent dependency unit | revert P4 policy/test additions; retain P1 final pins and required adapters |
| P5 | Performance acceptance and re-measurement of the unchanged P1 dependency unit (no new fork commit), docs (docs/architecture.md:359 still says 0.36.2; docs/control-protocol.md screenshots; docs/testing.md UI perf), final evidence. Any #8731/#8736 fix must already be in the reviewed P8 pin from P1. |
paired benchmark report with raw samples, frame counts, thresholds, stock-0.37 control, platform rows, unresolved gates | revert P5: docs/report only; no dependency or state change |
Each stage is a separate reviewable PR with its own exact source/lock identity, validation log and screenshots. No stage pushes, opens a PR or publishes without fresh human approval of the exact content and validation.
Compatibility checks after P1 and each later stage:
cargo tree -d
cargo tree -i egui@0.37.0
cargo tree -i winit@0.30.13
cargo tree -i wasm-bindgen --target wasm32-unknown-unknown
cargo check --locked
cargo test --locked
git grep -n 'path *= *"/' -- Cargo.toml crates apps # must be empty
git grep -n 'branch *=' -- Cargo.toml # must be empty in publication
git grep -n 'aiongg' -- Cargo.toml Cargo.lock # must be empty in publication
git grep -n 'with_accessibility_check(false)' -- crates apps # must be empty
11. Blocking unknowns and decisions
- Final storytold egui/winit SHAs. The candidate
aionggrevisions are comparison inputs only. Production waits for the approved, published implementation: a storytold egui P8 SHA with E1–E21 + P2b + P7 + P7b/E21b + P7c's selected#8763/#8748fixes + any justified#8731/#8736fix, and a completed storytold winit SHA with W1–W27 + W15P and the W27 lifecycle fixes. Both must be reachable, immutable and fetchable with--locked. - craft-libs PR 199 base. The implementation starts from merged PR 199 at
3832c47ef68a0e6214f2c1e85f3526b6adcb686e; historical8614e4a/1764bcaon oldmain4919088and the staleb68de8c/ec8840atrees must not be merged. The final immutable craft-ui revision must keep registryegui 0.37and include CL-1 plus CL-2F. - CL-2F. EffectCraft requires the approved craft-ui slot helper at the CL-2F freeze in the same atomic stage as P1; only the Design/Grid pilots use the provisional CL-2 revision. If CL-2F slips, P1 cannot claim screenshot completion done; no app-local channel or unbounded queue is an acceptable substitute.
- E21b/P7b coordination. The egui
IMEOutputpayload and the egui-winit W27 adapter must land atomically with the custom-editor adapters; the stock/forkIMEOutputshape cannot be mixed. #8731fix availability. If text tessellation regresses over threshold, the fix belongs in the reviewed P8 revision before P1 (with provenance/test) or the migration is blocked; P5 cannot change the pin.#8736Windows startup cost. Measure before P1; include the fix in P8 or record an explicit owner acceptance. P5 re-reports it.monochrome_emoji_fonts/system_fonts/ WASM size. The plan requiresmonochrome_emoji_fontsto preserve✔/▣and 0.36 parity. Measure the web raw/compressed size; accept a budget or add a native-only emoji-fonts feature. Enabling colour emoji viasystem_fontsis a separate decision with startup (#8512) and determinism costs.- Pixel golden storage. Committed PNGs require
.attributionsidecars andATTRIBUTION.mdrows under the repository asset rule; if maintainers reject committed screenshots, use the hash-based tolerant comparison instead. Both need the same tolerance policy and the exact light/dark × 1.0/2.0 set. - macOS evidence. No macOS hardware is available to this task; the macOS gate (IME/NSRange, Command keys, title bar, hidden screenshots) remains unresolved until real evidence exists.
- Windows evidence. Requires a
craft-windowsclone and the task workflow; the Basic Render Driver is software and cannot stand in for a real GPU result. - Linux Wayland/PRIMARY and physical GPU. The planning host has no display session (only an i915 render node), so a headless wgpu adapter run is possible but Wayland/PRIMARY/real presentation need a managed environment. Xvfb + WM + IME qualifies only the X11 parts.
- Accessibility workload. The exact number of unnamed inputs is unknown until the suite runs;
the known stock set is 39
TextEditand 51DragValue/Slidersites plus custom widgets that gain roles. P1 fixes what strict checks report; there is no global or per-builder opt-out, and P3 completes the custom-control coverage. - Atomic compatibility boundary. P1 uses the complete reviewed dependency unit and its required IME/screenshot adapters. No intermediate base pin is needed. Independent stock-0.37 controls use a bounded test adapter and are never a production handoff. Extended language policy/qualification in P4 cannot undo required P1 API compatibility.
12. Limitations of this planning task
- No build, test, clippy, GUI, browser or benchmark was run. Compiler inventories are read from the
2026-10-22 audit (a historical fork revision, machine-local path patches,
ui-eguilib/lib-test only) and from source inspection; integration tests and the app/web crates will reveal additional errors. - The environment record reports no
DISPLAY, noWAYLAND_DISPLAYandXDG_SESSION_TYPE=tty, but an i915 render node is present (/dev/dri/renderD128readable/writable). This supports a headless wgpu adapter run only; it does not qualify native presentation, Wayland, PRIMARY or physical-GPU performance. - The cmap/glyph findings are a read-only inspection of the bundled font files, not a rendered proof; the implementation must run the coverage test.
- Upstream issue claims (
#8731,#8736,#8763,#8748,#8631,#8512,#8428,#8657,#8577) are reported by upstream authors, not measured here; the linked evidence file (/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/upstream-discussions.json) records the snapshots.#8764is open/unmerged (upstream-8764-fresh.json), a proposal only. - No performance number is claimed. The thresholds above are proposals to be validated by paired runs on recorded hardware.
- Windows and macOS behaviour, IME, native focus, file pickers and real-GPU presentation are not qualified by headless kittest and remain explicit gates.
- Reviewer dispositions: the review's findings on workspace version 1, the visual-harness
coverage, the absent shared
with_accessibility_checkswitch, the incomplete PRIMARY matrix, child-viewport freezing while hidden, the performance sample counts, theppmcapture point, dependency/lock gates, skippingui_gpu.rs, and theId::newgrep/occurrence counts were adopted and are fixed above. Two review suggestions were corrected rather than adopted:RawInput::timecannot supply a logic-only clock (run_logicdiscards it; an app-owned injected clock is required), and freezing the admission-time DPI is wrong when the frame is captured later (the issued geometry + geometry epoch are bound to the image, and the returned dimensions supply the pixel scale). The v2 review's hidden-input accumulation, P7c/CL-2F dependency, baseline-identity, E21b state/serial, W27/W25 gate, accessibility-builder and anchor findings are fixed in place above. The review's blanket claim that hidden browser logic cannot run is also corrected: eframe 0.37 already falls back tosetTimeoutfor hidden tabs; the realistic gate is the browser's clamped cadence, not a new worker feature. The review's Linux "no display means no X11 evidence" is narrowed to the parts that genuinely need a real compositor/physical GPU; Xvfb + WM + IME can qualify X11 focus, clipboard/PRIMARY and X11 IME behaviour. - The pinned fork, winit, craft-libs and PR 199 can move before implementation; every plan stage records the exact SHAs it was reviewed against.
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/effectcraft main now resolves to f6ef1a790625657259f449f3f4f0773d7df449bb. The owned checkout was fast-forwarded to this revision. Earlier baseline SHAs and line anchors above are retained as historical review evidence; use this new revision and its exact lockfile for current-main integration and paired measurements. The original refs remain reachable.
Preserve queue4096/click1–512/drag<=2048 validation and refusal-without-mutation test; these limits differ from FilmCraft. Preserve chrono feature split.
The complete static delta review and exact old/new object identities are retained in the task evidence: canonical-diffs-refresh-20261023.json and overnight-consumer-review-20261023.md. This amendment claims no new build, native GUI or performance acceptance.
Local implementation stage 1 — 2026-10-23
Implemented in the owned worktree only: no commit, push, PR, release or publication; no build,
test, fmt, clippy or cargo metadata was run (the parent runs managed task.py validation and
generates the lockfile). Local dependency redirects are the parent's (unpublished) worktrees.
Compatibility surface
egui/eframe/egui_kittestmoved to0.37with expliciteguifeaturesdefault_fonts+monochrome_emoji_fontsandeframe/clipboardon the UI crate and desktop app. Thecraft-uirevision is unchanged (the 0.36-era pin) with a TODO: the reviewed CL-1 + CL-2F revision is the parent's to supply; no remote revision was invented.Id::new→Id::uniqueeverywhere (603 occurrences);Ui::id→Ui::scope_id;UiStack.id→UiStack.scope_id;WidgetType→egui::Role(ColorButton→ColorWell, SelectableLabel→Button withselected, Other→Unknown/TreeItem);AtomLayoutResponse→WidgetAtomResponsewith anIdSaltswatch id;FontDatacomparison throughbytes();TextEdit::vertical_align→align(Align2::LEFT_CENTER);Event::MouseWheelgainedsource: MouseWheelSource::Wheelin the control channel and every test.- Screenshots replaced
UserData/Event::Screenshottoken scanning withcraft_ui::capture::CaptureSlot: 4 outstanding requests, 128 MiB per image and 256 MiB aggregate reservations from checked geometry, issuance only after 3 settled non-discarded passes plus 0.35 s, geometry-epoch binding (window position excluded), stale-epoch retry and rejection, deadlines, cancellation and late-callback release. The callback holds only the weak slot; files and replies are written in the tick. - The covered/minimized refusal in
ui.screenshotis intentionally still present: removing it needs a native platform run showing a pending screenshot forces a hidden paint.
Lifecycle
EffectcraftApp::tick(all eframe logic callbacks, including hidden logic-only ones): engine and background queues, script tasks, engine events, native command inbox, close guard, window title, project auto-save, workspace-layout auto-save, memory watch/home-on-open, playback + audio, control channel, screenshot completion/issuance, andRequestPaintWhileHiddenwhen a UI pass is needed.EffectcraftApp::draw(the rootUi): language/theme/font setup (the existing two-pass font warm-up), input-driven shortcuts, layout, panels, dialogs, dropped files, Video Preview, workspace-state observation, plus the pass geometry/epoch and theAfterInputacknowledgement on the final non-discarded pass.AppClock(web_time::Instant, injectable manual clock) replaces every wall-clock orctx.input(|i| i.time)use in the tick (auto-save, watch folder, footage reload, playback, control deadlines, screenshot deadlines, toast timestamps).panels/viewernow records its drawn scale and playback advances in the tick.- One pending synthetic batch:
feed_syntheticrefuses a second batch until a UI pass applied the first (mark_synthetic_applied), a font-gated pass requeues the batch instead of dropping it, andraw_input_hookkeeps the control provenance across hidden iterations. - Web: dropped files are read in
raw_input_hook; the persist snapshot uses the app clock.RequestPaintWhileHiddenis sent on native only — the shared web fork does not carry it, so hidden web input/screenshots remain unqualified. - IME: the custom viewer editor handles
ImeEvent::DeleteSurrounding(characters) andDeleteSurroundingBytes(UTF-8 bytes, converted at char boundaries throughTextDoc), grouped with the followingCommitunder one typing/undo key. Outgoing surrounding text (E21b/W27) is recorded as the next step at the singleIMEOutputproducer; no empty placeholder was invented.
Tests added (not run here)
lib.rs::lifecycle_tests: logic-only tick records no fonts/geometry; a control command completes without a pass; one pending synthetic batch across repeated hidden hooks with a font-gated requeue and exactly one reply; geometry epoch ignores window movement and tracks resize/back; hidden playback advances and stops at the plan's end; workspace-layout auto-save writes on a hidden tick; a hidden close request opens the guard and sendsCancelClose; screenshot issuance/settlement, resize retry, admission bounds + timeout release, and late-callback no-write/no-second-reply.panels/viewer_text.rstests: byte-count IME deletion around an emoji, character-count deletion over a selection with clamping, and no-op cases.theme.rsglyph test extended to the full drawn chrome set;ui_effect_names/ui_presets_folderopt out of the missing-glyph panic only for their CJK/Japanese fixtures.
Known remaining gates
- Parent must generate
Cargo.lock, supply the local patch config and the reviewed craft-ui revision, then run the repository gates. Strict kittest accessibility is still on: stockTextEdit/DragValue/Sliderfields without a label or hint will be reported (no global or per-builder opt-out was added). ui_gpu/native GUI, hidden-window screenshots (the refusal above), Wayland/X11 IME and PRIMARY, Windows/macOS rows, and every performance row remain unmeasured here.