mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-01 22:26:49 -06:00
1427 lines
107 KiB
Markdown
1427 lines
107 KiB
Markdown
# egui 0.37 migration plan for DesignCraft
|
||
|
||
Status update (2026-10-25): implementation is ready locally for review and includes main `7ca39c6c10f123d0cd0176eda1511e59667c89d0`. Managed run `egui037-design-main7c-ci-20261025-0817` passed all nine Linux CI stages: 3,344 test executions passed, 0 failed and 6 ignored across 81 summaries. Counts include repeated executions. The current-main integration received independent review. Dependency pins and lockfile are unchanged. Current-source Windows/packages, native IME and paired application performance remain open. Publication requires separate explicit approval; human review and human merge are required.
|
||
|
||
The original planning record below is historical.
|
||
|
||
Status: planning only. No production change, build, test, GUI run, browser run, benchmark,
|
||
push, pull request or publication was performed while writing this document. Every future test
|
||
or build runs through `maintenance/task.py run/status/finish` with its own task, source and
|
||
evidence; never run a build directly. Every future push or new pull request needs fresh explicit
|
||
human approval after the exact contents and validation are shown. The contributor credit
|
||
preference remains username-only (`@skylartaylor`); do not add display or real names or any
|
||
agent/model/tool attribution.
|
||
|
||
This plan covers only `storytold/designcraft`. It records the work needed to move the
|
||
DesignCraft desktop, web and UI crates from the current coherent 0.36.2 dependency set to
|
||
egui 0.37.0, the final reviewed suite fork and the matching final reviewed winit revision.
|
||
Shared-library work remains in `storytold/craft-libs`; this plan consumes it but does not
|
||
redefine it. Candidate `aiongg` revisions below are comparison inputs only. A production
|
||
manifest must name the final reviewed `storytold` revisions, which do not exist until the
|
||
fork/winit/craft-libs publication is reviewed and approved.
|
||
|
||
Capture acceptance follows the suite contract: a queued-not-issued hidden request and
|
||
each settling frame must request `RequestPaintWhileHidden` from logic; only an issued
|
||
screenshot command forces paint itself. Callback arrival has no implicit wake, so the
|
||
live request owns the viewport waker. Record known issuance UI bounds and a geometry
|
||
epoch, reject/retry any intervening resize/DPI/zoom/surface change even if it changes
|
||
back, and derive pixel scale from returned dimensions only under an unchanged epoch.
|
||
Default limits are 4 requests, 128 MiB/image and 256 MiB aggregate. Count reserved
|
||
in-flight and completed images together.
|
||
|
||
|
||
## 1. Baseline and provenance
|
||
|
||
### 1.1 Exact source baseline
|
||
|
||
| Item | Value |
|
||
| --- | --- |
|
||
| DesignCraft checkout | `/home/codex/work/CraftApps/egui037-migration-20261022/designcraft` |
|
||
| Baseline commit | `e74208dd7e0c801d15dfdf926d64cfe5e3b346d3` (`Merge pull request #1642 from aiongg/ci-speed`) |
|
||
| Workspace version / edition / MSRV | `0.12.0` / edition 2024 / Rust 1.95 |
|
||
| Lockfile | `Cargo.lock`, format version 4 |
|
||
| Current egui family | `egui`, `epaint`, `ecolor`, `emath`, `eframe`, `egui-wgpu`, `egui-winit`, `egui_kittest`, `epaint_default_fonts` all `0.36.2` from crates.io |
|
||
| Current winit | `git+https://github.com/storytold/winit.git?rev=3d51077fd1a966f84df9ef287839dc84fd5b4ca7` (0.30.13, W1–W14) |
|
||
| Current `craft-ui` | `git+https://github.com/storytold/craft-libs?rev=5c874d51eeb3de70e735c8ce87a59a7dedd47675#5c874d51...` |
|
||
| Upstream egui 0.37.0 | `841a328ba2dd8f6996beb7189ebb7fc00e668a2a` (release commit, not the annotated-tag object) |
|
||
| Candidate egui fork (comparison only) | `aiongg/egui` `craft/0.37` = `a4f9a17f64a6bae61350fb85804c9df0d9865916` (E1–E21; E21b/P7b still to be added) |
|
||
| Candidate winit (comparison only) | `aiongg/winit` `craft/0.30.13-lang` = `8b35388bd719e8cc86a95971ba0bc026c9a8f058` (W1–W27; W15P still to be added) |
|
||
| Final storytold egui pin | **Unknown until approved publication**: a reviewed immutable `storytold/egui` revision containing the complete P8 handoff: E1–E21, E21b/P7b, E4/P2b, P7 key pairing and P7c fixes. Never ship the candidate hash. |
|
||
| Final storytold winit pin | **Unknown until approved publication**: a reviewed immutable `storytold/winit` revision containing W1–W27 plus native W15P PRIMARY. Never ship the candidate hash. |
|
||
| Final `craft-ui` pin | **Unknown until approved publication**: `<FINAL_CRAFT_UI_REV>`, the reviewed CL-2F revision (including CL-1, and CL-3 if it has landed) derived from current craft-libs, not a PR head. D1 uses the reviewed CL-1 + CL-2 provisional revision as a pilot; CL-2F follows the DesignCraft/GridCraft adapter reviews. |
|
||
| Published predecessor | `storytold/egui` `craft/0.36.2` = `767b1cad294120689a514bb4bd68c077e46679b9`; published `storytold/winit` = `ffae5198770d7730e718043f035a772b7dbc3b1b` |
|
||
| craft-libs PR 199 | head `b68de8c5f4f1c060904d211a7b1320aa0f7bce78`; audit tree `ec8840af8c5fda695cf195cdd05e5ae078415c07`. Both are **stale trees, not bases**: they omit `crates/platform` and revert current security/settings/docking/printer fixes. Apply only the API-port hunks from `8614e4a04fae516fba16c22b31ff1008cb6be10b` (and relevant README changes from `1764bca`) onto current craft-libs `49190884278f16c9a9f5823d7698a8ffb5deb062`. |
|
||
| Related language work | `storytold/craft-libs#198`, delivered through fork E5/E9/E12–E17/E19–E21, E21b and winit W17–W27 plus W15P |
|
||
|
||
Authoritative lifecycle evidence for this plan is
|
||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/lifecycle-source-audit.md`.
|
||
It was checked against upstream `841a328b` and candidate `a4f9a17f`: both native
|
||
`EpiIntegration::update` and web `AppRunner::logic` call `app.logic` then `app.ui` inside
|
||
`Context::run_ui`, so `App::logic` can run once per layout pass in both hosts. After a normally
|
||
completed `run_ui`, `current_pass_index()` is zero even after multiple passes. `run_logic` does
|
||
not restore an old index. `ViewportInfo::visible()` is a method derived from `minimized`/
|
||
`occluded`; it is not a callback-phase signal, and `None` means unknown.
|
||
|
||
The IME gap is recorded in
|
||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/ime-surrounding-gap.md`. The
|
||
candidate never calls W27 `set_ime_surrounding_text`, and `IMEOutput` has no logical text. E21b/P7b
|
||
is a required new change, not existing capability.
|
||
|
||
The historical compiler diagnostics at
|
||
`/home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/designcraft.errors`
|
||
are clues only. They were produced against an earlier prepare state, their counts are not
|
||
acceptance, and the current source may differ. They do show the first API breaks: missing
|
||
`Event::source`, removed `egui::UserData`/`Event::Screenshot`, renamed roles in
|
||
`new_document.rs`, `font_menu.rs`, `panels/*`, and `FontData` blob typing in `theme.rs`.
|
||
|
||
### 1.2 Packages, binaries, examples and feature targets
|
||
|
||
| Target | Kind | Relevant manifest | Notes |
|
||
| --- | --- | --- | --- |
|
||
| `designcraft-ui-egui` | library | `crates/ui-egui/Cargo.toml` | The swappable L6 UI. Production depends on `craft-ui`, `egui`; dev depends on `egui_kittest` (`wgpu`, `snapshot`) and `designcraft-fonts` (`testing`). D1 adds an explicit `egui/default_fonts` dev feature (see §3.7). Build script only bakes contributor credits. |
|
||
| `designcraft` | binary | `apps/designcraft/Cargo.toml` | eframe features `default_fonts`, `wgpu`, `accesskit`, `wayland`, `x11`, `links`. D1 adds `clipboard` to preserve Copy/Cut and Paste on 0.37 (0.36.2 forced it through eframe; see §3.5). Native-only `rfd`, `arboard`, `winit`; macOS `muda`, `objc2*`, `fmv-macos-events`; Windows `winsafe`. |
|
||
| `designcraft-web` | wasm binary | `apps/designcraft-web/Cargo.toml` | `eframe` features `default_fonts`, `wgpu`, `accesskit`; `rfd`, `wasm-bindgen`, `web-sys`; `trunk` build. Browser clipboard/drop paths are web-specific. |
|
||
| `designcraft-cli` | binary | `apps/designcraft-cli/Cargo.toml` | No egui/eframe/winit/rfd; headless and MCP. |
|
||
| `ui_shot` | example | `crates/ui-egui/examples/ui_shot.rs` | `egui_kittest::Harness` with wgpu at 1440×900, 2×; drives control requests and saves whole-window PNGs. It steps one UI pass per `harness.step()` and is not native presentation evidence. |
|
||
| `xtask` | workspace tool | `xtask/Cargo.toml` | `cargo xtask ci`, `wasm`, `layers`, `assets`, `pdf-deps`, `test`, `stats`, `parity`. |
|
||
|
||
There are 22 workspace manifests (`Cargo.toml` plus `apps/*` and `crates/*`), no nested
|
||
workspace, no `vendor/` directory, no examples or benchmarks that use egui other than
|
||
`ui_shot`, and no `[[bench]]`, `[[test]]` or `[[example]]` entries in any manifest. The only
|
||
workspace patch section is the root `[patch.crates-io]` in `Cargo.toml`.
|
||
|
||
Relevant platform cfgs:
|
||
|
||
| Cfg | Files | What must remain true |
|
||
| --- | --- | --- |
|
||
| `target_arch = "wasm32"` | `apps/designcraft-web/src/{main,web}.rs`, `crates/ui-egui/src/lib.rs` (`now_ms`, recovery, render worker) | Browser entry point and async file/drop/download paths build and run; native-only recovery, dropped-file paths and render worker stay out. |
|
||
| `not(target_arch = "wasm32")` | `crates/ui-egui/src/lib.rs`, `apps/designcraft/Cargo.toml` | Native recovery, `arboard`, `rfd`, control server, `winit` and the enabled clipboard path keep compiling. |
|
||
| `unix` and `not(target_os = "macos")` | `apps/designcraft/src/main.rs` X11 event-loop builder | XWayland selection and file drops still work; `winit` pin changes do not remove `EventLoopBuilderExtX11`. |
|
||
| `windows` | `apps/designcraft/{Cargo.toml,src/gpu.rs}`, `build.rs` | `winsafe` display-GPU query and Windows resource build remain; DX12 backend selection and `WgpuError` fallback are tested. |
|
||
| `target_os = "macos"` | `apps/designcraft/src/{main,native_menu,mac_window,apple_events}.rs`, `crates/fonts` CoreText | Native menu, Apple events, `movable_by_window_background`, IME marked-text discard and cancelable termination keep their winit W12/W14/W22/W26 behavior. |
|
||
| X11/Wayland | `egui-winit` clipboard path, `apps/designcraft/src/main.rs` | D1 enables the explicit clipboard feature; D3 adds E4/W15P native PRIMARY on the winit data device. One regular data device only; password exclusion and unchanged-selection policy preserved. |
|
||
| `cfg(test)` / `cfg(all(test, unix))` | `crates/ui-egui/src/*`, `apps/designcraft/src/main.rs` | Test-only counters and atomic-writer tests do not leak into release; kittest strict names and missing-glyph policy are explicit. |
|
||
|
||
### 1.3 Current CI and validation commands
|
||
|
||
All future commands below run through `maintenance/task.py run` with a unique `--job-id`, the
|
||
owning `--task`, exact `--source` and durable `--evidence`; inspect with `status` and complete
|
||
with `finish` after reviews.
|
||
|
||
- `cargo xtask ci`: `fmt --all -- --check`, `pdf-deps`, `clippy --workspace --all-targets --
|
||
-D warnings`, `nextest run --workspace --lib --bins --tests` (or `cargo test`), doctests,
|
||
`assets`, `layers`, `wasm`.
|
||
- `cargo xtask wasm`: one `cargo check --target wasm32-unknown-unknown --keep-going` over
|
||
L0–L5 plus `designcraft-ui-egui`.
|
||
- `cargo test -p designcraft-ui-egui --lib` is the package-scoped UI row. It must run with the
|
||
explicit `egui/default_fonts` dev feature and with `theme::install_fonts` before layout.
|
||
- Clean-checkout dependency gate: after publication, a fresh clone runs
|
||
`cargo fetch --locked` and `cargo metadata --locked --format-version 1` against the final
|
||
storytold revisions. A missing/unreachable revision or a machine-local path fails the gate.
|
||
- CI workflow `.github/workflows/ci.yml` runs `cargo xtask ci` on Ubuntu 24.04 with pinned
|
||
craft-fonts `abb83316d96aa59c1cf64784289e378fe9fa5695`, nextest, and the apt/winit build
|
||
dependencies.
|
||
- `windows-arm64.yml` packages and installs ARM64; `freebsd.yml` runs the workspace in a
|
||
FreeBSD VM; `release.yml` uses the shared release workflow.
|
||
- Manual UI evidence: `cargo run --release -p designcraft -- --sample --control 7979`,
|
||
control requests, `ui.screenshot`, and `cargo run -p designcraft-ui-egui --example ui_shot
|
||
-- script.jsonl` for offscreen whole-window screenshots.
|
||
- Native/browser workflows are listed in §6.4; `ui_shot` alone is not native focus, IME,
|
||
picker, clipboard or browser-presentation evidence.
|
||
- There is no performance benchmark target today. `ui.inspect` exposes
|
||
`perf.frameMs`, `perf.renderMs`, `perf.fps`, `perf.patches`; `perf.frameMs` uses
|
||
`now_ms()` (`SystemTime` on native, `0.0` on wasm) and is not a monotonic acceptance
|
||
timer. §7 defines the test-only raw-sample harness.
|
||
|
||
## 2. What 0.37 changes that actually affects DesignCraft
|
||
|
||
DesignCraft already uses `eframe::App::logic`/`App::ui` and a root `Ui` on 0.36.2. The
|
||
0.37 migration must preserve that structure; it is not a reason to rewrite panels into
|
||
`Context::root_ui` calls or to move every action out of `logic`.
|
||
|
||
| 0.37 change | Source anchor | DesignCraft impact |
|
||
| --- | --- | --- |
|
||
| `Event::MouseWheel` gains `source: MouseWheelSource` | `egui/src/data/input/event.rs`, `MouseWheelSource` | Every synthetic wheel event in tests must set `source` (`Unknown` for most; `Wheel`/`Trackpad`/`Momentum` where the behavior under test depends on it). Production matching already uses `..`, but the canvas's Alt-wheel state machine must be rechecked. |
|
||
| `Event::Screenshot` and `egui::UserData` are removed; `ViewportCommand::Screenshot(ScreenshotCallback)` | `egui/src/data/screenshot_callback.rs`, `viewport.rs`; upstream #8489 | `ScreenshotCallback` is one-shot and does not repaint. Replace token/`UserData` correlation with a per-request completion slot owned by the request record and a weak callback; see §3.4. |
|
||
| `WidgetType` is replaced by `accesskit::Role` (`egui::Role`); alias deprecated | `egui/src/lib.rs` compatibility table; #8555 | Map `SelectableLabel` → `Button`, `TextEdit` → `TextInput`, `CheckBox` → `CheckBox`, `Other` → `Unknown`. The full file list is in §3.7: `new_document.rs`, `panels/font_menu.rs`, `panels/pages.rs`, `panels/mod.rs`, `panels/styles.rs`, `panels/cell_stroke.rs`, `panels/properties.rs`, `panels/swatches.rs`, `panels/layers.rs`, `object_control.rs`, `widgets.rs`, `toolbar.rs`, `chrome.rs`, `taskbar.rs`, `icons.rs`, `dialogs.rs`. |
|
||
| `Id::new` deprecated for `Id::unique`; `Ui::id` renamed `Ui::scope_id`; `UiBuilder::id` renamed `scope_id` | `egui/src/id.rs`, `egui/src/ui.rs`; #8534/#8542/#8586 | Mechanical but warning-sensitive because `clippy -D warnings` treats deprecations as failures. Use `Id::unique` for root IDs and `scope_id` for child scopes. Do not change the hashed values: `Id::new` now delegates to `Id::unique`, so state keys remain stable. |
|
||
| `Panel` constructors take `impl AsIdSalt`; panel ID is `parent_ui.scope_id().with_salt(salt)`; `CentralPanel` remains last; root `Ui` is the normal app surface | `egui/src/containers/panel.rs`; #8592 | Existing `Panel::left("tools")`, `Panel::right("dock")`, etc. still type-check. Panel IDs are now parent-salted. DesignCraft does not persist egui memory (eframe persistence is off); update in-memory test lookup keys and retain the actual app-owned UiState/docking preferences. No PanelState migration is needed. Panel order must not change. `CentralPanel::default().show(ui, …)` is correct; `Context::root_ui` must not be called inside `App::ui`. |
|
||
| `default_fonts` is no longer a default `egui`/`epaint` feature | `egui/Cargo.toml`; #8583 | `eframe` still enables it in the app, but `cargo test -p designcraft-ui-egui` does not build the app and `egui_kittest` disables egui defaults. Add `egui/default_fonts` explicitly to the ui crate's dev feature set and install DesignCraft fonts before layout; do not rely on workspace unification. |
|
||
| `FontData.font` is now a shared `Blob` (`Arc<dyn AsRef<[u8]> + Send + Sync>`), not `Cow<[u8]>` | `epaint/src/text/font_data.rs` | Production struct-update code in `theme.rs` still works; tests that compare `data.as_ref()` must use `data.bytes()` or `data.font.as_ref().as_ref()`. This caused the historical `theme.rs:766/793` errors. |
|
||
| `egui_kittest` fails unnamed input widgets and panics on missing glyphs by default | `egui_kittest/src/accessibility.rs`, `builder.rs`; #8589/#8601 | The first green stage must satisfy strict names/glyph policy, not disable it globally. D1 adds names/policy for the existing test surface; D4 completes coverage. Deliberate tofu fixtures use `HarnessBuilder::allow_missing_glyphs()` with a named reason. |
|
||
| Native eframe can discover system fonts through `egui_system_font_provider`, and `NativeOptions::system_font_fallback` defaults true | `eframe/src/native/epi_integration.rs`; #8512 | DesignCraft already catalogs system fonts in `designcraft-fonts` with script-specific families and baseline tweaks. Set the option false and keep the feature off, or font order and line metrics change. Add the #8512 cold-start workload in §7. |
|
||
| Clipboard is no longer forced on by eframe; `egui-winit` clipboard is an explicit feature | `eframe-0.36.2/Cargo.toml` target dep forces `egui-winit/clipboard`; `eframe-0.37.0/Cargo.toml` exposes `clipboard = ["egui-winit/clipboard"]` and depends on egui-winit with defaults off; `egui-winit-0.37.0/src/clipboard.rs` stores only in-process without arboard/smithay | 0.36.2 wrote Copy/Cut to the OS clipboard. On 0.37 without the feature, `ctx.copy_text` is in-process only and `RequestPaste` cannot read the OS clipboard. D1 enables `eframe/clipboard` to preserve native Copy/Cut/Paste, with the control-channel guard. D3 adds E4/W15P PRIMARY on the same winit data-device path. |
|
||
| X11/Wayland PRIMARY selection is new in `egui-winit`, and the candidate's E4 path replaces arboard with winit's data device | `egui-winit/src/clipboard.rs`, `lib.rs`; #8428; E4 | Enabling clipboard makes PRIMARY reachable. The final fork must include E4 plus native W15P; test pointer- and keyboard-triggered ownership (including keyboard-only focus), serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfers on wlroots/KWin/Mutter, password exclusion and no second regular data device. |
|
||
| `TextEdit` selection behavior changed on click-away | upstream #8763, unmerged PR #8764 | The fork owns the selected fix. Source audit finds no DesignCraft control that reads an egui `TextEdit` selection in the same pass: formatting acts on the engine `TextSel`; dialog/search buttons read buffers. The regression test documents stock 0.37 click-away behavior and asserts the app's engine selection/target is unchanged; if a future control reads `TextEdit` state, carry #8764 or defer the command with a tested app change. |
|
||
| Text tessellation is reported ~60% slower for plain text | upstream #8731 | UI frame cost may rise even though layout is unchanged. The fix must keep color emoji correct; branch once per row, not clamp-only. Measure the fixed 120-label workload. |
|
||
| DX12 pipeline creation is reported ~4× slower from `wrap_texel_coord` | upstream #8736 | Windows startup can get ~40 ms slower. Measure renderer/shader construction separately from first frame; do not call shader compile time UI performance. |
|
||
| `#8748` hidden-parent/deferred-child is closed and unmerged | upstream #8748 | Do not assume hidden-window child viewport updates are fixed by 0.37. The fork owns a tested solution; DesignCraft must open/update/close its immediate second viewport while the parent is hidden/occluded/overdue, with unknown visibility (`visible() == None`) as well as `Some(false)`. |
|
||
| `Context::root_ui`, `run_ui`, `run_pass`, `run_logic`; root `Ui` cannot be recursively entered; both native and web `App::logic` run per `run_ui` pass | `egui/src/context.rs`; `eframe/src/native/epi_integration.rs`; `eframe/src/web/app_runner.rs`; #8535; lifecycle audit | Use the passed `&mut Ui` in `DesignApp::ui`; use `run_ui` for UI tests and `run_logic` only for side-effect tests. Recursive `root_ui` panics. Do not use `visible()` as a logic/UI phase signal; a forced hidden paint is still a UI pass. |
|
||
| Style defaults changed (roomier buttons, larger corner radius) | #8617/#8445 | Custom `Tokens` still drive most colors, but egui widget spacing/rounding inherited from `Visuals` can shift geometry. Visual baselines need an explained intended change, not a blind update. |
|
||
| E21b/P7b outgoing IME surrounding text is missing | `IMEOutput`/`egui-winit` candidate source; `ime-surrounding-gap.md` | Add the bounded transient payload, built-in `TextEdit` producer and W27 Wayland adapter; DesignCraft's custom canvas editor supplies a real model/preedit/UTF-8 adapter, not `None`. |
|
||
|
||
## 3. Source audit and concrete migration design
|
||
|
||
### 3.1 Existing frame flow
|
||
|
||
The desktop host implements `eframe::App` in `apps/designcraft/src/main.rs`:
|
||
`App::logic` calls `startup.enter`, `take_notice`, the `frames == 2` presented check,
|
||
`frames += 1`, startup error-log installation, macOS native menu/Apple-event polling, then
|
||
`DesignApp::logic`; `App::ui` calls `DesignApp::ui`, then macOS IME/movable handling.
|
||
`App::raw_input_hook` delegates to `DesignApp::raw_input_hook`. The web shell does the same
|
||
in `apps/designcraft-web/src/web.rs`, with asynchronous drop-file reading and a graphics
|
||
health check.
|
||
|
||
Source facts that constrain the design (verified against the pinned trees):
|
||
|
||
1. Native `EpiIntegration::update` (`eframe-0.37.0/src/native/epi_integration.rs:349-361`)
|
||
calls `Context::run_ui(raw_input, |ui| { app.logic(ui.ctx(), …); app.ui(ui, …) })`.
|
||
Web `AppRunner::logic` (`eframe-0.37.0/src/web/app_runner.rs:290-293`) does the same.
|
||
A multipass frame therefore calls host and app `logic` once per pass, not once per frame.
|
||
2. `Context::run_logic` (`egui-0.37.0/src/context.rs:1174-1202`) only refreshes viewport/
|
||
window/focus input; events and time stay from the previous UI pass.
|
||
3. `run_dyn` passes `new_input.take()` to each `begin_pass`; the second pass has no raw
|
||
events. `PointerState::button_clicked` scans `pointer_events`, which are cleared and
|
||
repopulated from that empty event list, so a button does not re-fire merely because a
|
||
pass was discarded. Command-producing UI still needs a one-pass/undo test because a
|
||
discarded first pass may act on pre-discard geometry; it does not need a global
|
||
`!will_discard()` ban.
|
||
4. After a completed `run_ui`, `ContextImpl::end_pass` takes `viewport.output`; the returned
|
||
output owns the completed-pass count and `current_pass_index()` reads the now-zero
|
||
viewport output. `run_logic` does not restore a stale index. The earlier comment alleging
|
||
a leftover nonzero count was wrong.
|
||
5. `ViewportInfo` has `minimized`/`occluded` fields and a `visible() -> Option<bool>` method;
|
||
there is no public `visible` field. `visible() == None` is unknown, and eframe forces a
|
||
real UI pass for `RequestPaintWhileHidden`/pending `Screenshot` even when hidden.
|
||
6. `update_logic_only` stores the prepared raw input in `pending_raw_input`; the next
|
||
`update` prepends it to new input. Normal pointer/key/text/IME events can therefore be
|
||
replayed after hidden logic.
|
||
|
||
`DesignApp::logic` (`crates/ui-egui/src/lib.rs:1102`) currently does all of:
|
||
`egui_ctx` capture, `window_maximized`, one-time CMS activation through `color_applied`,
|
||
`ctx.set_zoom_factor` from `ui.ui_scale`, `zoom_with_keyboard=false`, font installation and
|
||
the 250 ms system-font poll, `theme::apply`/`restyle`, FPS smoothing from `i.time`, recovery
|
||
autosave from `i.time`, control drain, stale new-document preview cleanup, inbox/open-OS-
|
||
documents, synthetic repaint requests, screenshot collect/issue, shortcut handling, window-
|
||
command flush, native dropped-file handling and quit handling. `DesignApp::ui` (`lib.rs:1472`)
|
||
does layout: app bar, control bar, status bar, Tools, dock, central canvas, second viewport,
|
||
floating panels/story editor/dialogs/about/palette, resize borders, URLs and frame timing.
|
||
|
||
This is a 0.36.2-shaped flow, not a 0.37 regression. However, 0.37 makes the latent problems
|
||
more important:
|
||
|
||
1. Every per-frame side effect in `logic` runs again on a discarded second pass. That
|
||
includes font/theme setup, recovery checks, FPS sampling, screenshot issue/collect and
|
||
side effects that are not input-driven.
|
||
2. While minimized/occluded, eframe normally runs `Context::run_logic` and no UI pass. All
|
||
`ctx.input` except viewport/window/focus information is stale. A forced hidden paint is
|
||
different: `run_ui` does run, with fresh prepared input, even though `visible()` may be
|
||
`false` or `None`.
|
||
3. Host `App::logic` counts passes in `frames`; on a multipass frame `frames` advances twice,
|
||
so `startup.presented()` and startup error logging happen at the wrong pass.
|
||
4. Input buffered by `update_logic_only`, and synthetic control events injected in
|
||
`raw_input_hook` during a hidden tick, can replay into a later UI pass with a different
|
||
focus or document state.
|
||
|
||
The correct migration is to make the frame stages explicit, not to merge `logic` into
|
||
`ui`, not to add a generic `Context` bridge, and not to use `visible()` as a phase signal.
|
||
|
||
### 3.2 Frame-stage contract
|
||
|
||
Introduce an explicit, testable split inside `DesignApp` while keeping the public
|
||
`logic(&Context)` and `ui(&mut Ui)` entry points that hosts and tests use.
|
||
|
||
```rust
|
||
// crates/ui-egui/src/lib.rs
|
||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||
// Every call (first pass, later discarded pass, or hidden run_logic) may poll consumed
|
||
// queues and deadlines. It must never replay an already-consumed message.
|
||
self.logic_tick(ctx);
|
||
// current_pass_index is 0 on the first pass and on hidden run_logic, and >0 on later
|
||
// passes of the same run_ui. It is not a freshness test and must not gate input reads.
|
||
if ctx.current_pass_index() == 0 {
|
||
self.logic_frame(ctx);
|
||
}
|
||
}
|
||
|
||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||
let first_pass = ui.ctx().current_pass_index() == 0;
|
||
self.layout(ui, first_pass);
|
||
}
|
||
```
|
||
|
||
The guard is deliberately narrow: it removes duplicate per-run_ui side effects, but a hidden
|
||
`run_logic` tick still has index 0, so autosave, control commands, inbox results and deadlines
|
||
continue. A command that arrives between passes is retained in its queue (or its reply is
|
||
pending); the control server requests a repaint, and the next frame's first pass consumes it.
|
||
Never put an unconditional `return` for `current_pass_index() != 0` around queue draining,
|
||
screenshot completion or background deadlines.
|
||
|
||
`logic_tick` (every call) owns work whose identity is a consumed queue item, revision or
|
||
monotonic deadline:
|
||
|
||
- `drain_control`; each request is taken once. Screenshot requests add a request record and
|
||
do not issue from the queue.
|
||
- `drain_inbox` and `open_os_documents` after fonts are ready; inbox items are consumed once.
|
||
- `handle_quit` (fresh `close_requested` in `i.viewport()`), `flush_window_commands`.
|
||
- Screenshot result draining, late-result rejection and deadline replies; take results out
|
||
of the slot under a short lock before encoding/writing.
|
||
- While a control gesture still has queued synthetic events, request a paint while hidden
|
||
(§3.3). Never inject a synthetic event during a logic-only tick.
|
||
- Filter buffered OS input after a hidden tick using the focus/document generation recorded
|
||
at the tick (§3.2 input policy).
|
||
|
||
`logic_frame` (first pass only) owns genuine once-per-run_ui work:
|
||
|
||
- `egui_ctx` capture; `window_maximized` update (`lib.rs:1106`).
|
||
- One-time CMS activation through `color_applied` (`lib.rs:1107-1112`).
|
||
- `ctx.set_zoom_factor` from `ui.ui_scale` and `zoom_with_keyboard=false` (`lib.rs:1114-1119`).
|
||
- Font installation/reinstallation from `theme::install_fonts`, `fonts_system` readiness and
|
||
the 250 ms `request_repaint_after` while the font scan runs (`lib.rs:1121-1131`).
|
||
- `theme::apply`/`restyle` (`lib.rs:1132-1135`).
|
||
- Recovery autosave (`lib.rs:1136-1147`), changed from `ctx.input(i.time)` to a monotonic
|
||
`Instant` clock so it continues during hidden logic and never uses a stale frame time.
|
||
Recovery is native-only (`#[cfg(not(target_arch = "wasm32"))]`); wasm uses its browser
|
||
time source rather than `Instant`/`now_ms()`.
|
||
- FPS smoothing only on a real UI pass, using a monotonic `Instant` on native and the
|
||
browser frame time on wasm; do not inject zero/duplicate samples from hidden logic.
|
||
- `new_document::end_stale_preview(self)` (`lib.rs:1157`) and the synthetic/control
|
||
`ctx.request_repaint()` while events remain (`lib.rs:1160-1162`).
|
||
- Screenshot issue after the 120 ms/3-frame settling rule (`lib.rs:1636-1648`).
|
||
- `menus::shortcuts` moves out of `logic` into the first-pass `input_stage` below.
|
||
- Native dropped files move out of `logic` into `pending_drops`, captured in
|
||
`raw_input_hook`; `logic` never reads stale `ctx.input` drops.
|
||
- `handle_quit` stays in `logic_tick` because `close_requested` is fresh viewport data and a
|
||
close request must not wait for a first pass.
|
||
|
||
`layout` calls `input_stage(ui)` only on the first pass, before the app bar:
|
||
|
||
- `menus::shortcuts(self, ui.ctx())` runs against live input;
|
||
- native dropped files are taken from `pending_drops` (captured in `raw_input_hook`), not
|
||
read from stale `ctx.input` in logic;
|
||
- no other input event is read twice. `canvas::show` and text fields continue to use egui's
|
||
normal per-widget input. Shortcut consumption happens before layout, preserving the
|
||
existing order.
|
||
|
||
`DesignApp::ui` must never call `ctx.root_ui(...)`: the root `Ui` is already borrowed by
|
||
the host callback, and `Context::root_ui` panics in that situation (#8535). The passed
|
||
`&mut Ui` is the one to use for `Panel`, `CentralPanel`, `Area`, and `show_viewport_immediate`.
|
||
|
||
Host guard: `apps/designcraft/src/main.rs` must not increment `frames` or run macOS menu/
|
||
Apple-event polling on every pass. Wrap the host body in the same first-pass condition (or
|
||
dedupe with `ctx.cumulative_frame_nr()`), while always delegating to `self.app.logic(ctx)`,
|
||
which has its own narrow guard. `frames` then counts presented `run_ui` frames, preserving
|
||
the `frames == 2` startup.presented semantics. Add a host-level forced-multipass test through
|
||
the actual `App` wrapper, not only `DesignApp`.
|
||
|
||
`raw_input_hook` remains the one place that sees the exact `RawInput` before a pass. It
|
||
continues the current batching: one pointer event per host iteration, a key press/release
|
||
pair together, with trailing Text retained for a later iteration (`lib.rs:1456-1469`). Add:
|
||
|
||
- native `raw.dropped_files` capture into `pending_drops` and removal from the raw input, so
|
||
a hidden `run_logic` cannot re-open the last drop;
|
||
- a focus/document generation snapshot on each hidden logic tick;
|
||
- a filter that drops buffered `Text`/`Ime`/`Paste` events when the focus target or active
|
||
document changed since the hidden tick, and drops an incomplete pointer chord (press
|
||
without release or release without press) across a hidden->visible transition. Complete
|
||
pointer/key events are replayed in order; pointer hover is refreshed or cleared with
|
||
`PointerGone`. Record the policy and counts in the test-only instrumentation.
|
||
|
||
On web, `WebShell::raw_input_hook` is called in both the visible and hidden `AppRunner`
|
||
paths. It must capture dropped files and start the asynchronous `bytes_async()` read there,
|
||
pushing the result into the existing inbox. `WebShell::logic` must not take
|
||
`ctx.input_mut` for drops. This is a source correction: the current
|
||
`apps/designcraft-web/src/web.rs:351-370` reads drops in `logic`, while `raw_input_hook`
|
||
(`web.rs:374-379`) does not.
|
||
|
||
Test-only instrumentation may record stage counts:
|
||
`#[cfg(test)] logic_tick_ticks`, `logic_frame_ticks`, `input_ticks`,
|
||
`screenshot_callbacks`, so forced-multipass and hidden tests can prove one side effect and
|
||
one input stage per frame. The counters must not exist in release builds.
|
||
|
||
`Context::run_ui` is the test entry point for layout and `Context::run_logic` for
|
||
side-effect-only tests. Do not initialize fonts inside `run_logic` in a UI test: no pass
|
||
runs, so font installation/layout does not behave as in a visible frame. `ui_shot` and
|
||
kittest harnesses call `app.logic(ui.ctx())` and `app.ui(ui)` inside the UI closure as they
|
||
do now, with the same pass-index guard. Tests must also drive the real host/web callback
|
||
order, not a fictional once-per-frame native host.
|
||
|
||
### 3.3 Control input while hidden
|
||
|
||
The control channel is used for automation even when the window is minimized. Under
|
||
0.37, a hidden window normally runs only `logic`; synthetic pointer/key/text/IME events
|
||
injected through `raw_input_hook` are not applied unless a UI pass runs. A
|
||
`ViewportCommand::RequestPaintWhileHidden` holds for **one frame** and must be sent again for
|
||
another (`egui/src/viewport.rs:1239-1247`); a plain `request_repaint` does not force a hidden
|
||
paint (`egui-winit/src/lib.rs:1922-1924`).
|
||
|
||
Policy:
|
||
|
||
- Do not use `visible()` as a phase signal. Use it only as a hint for whether to request a
|
||
hidden paint. `None` (unknown visibility) is treated like hidden for this purpose.
|
||
- raw_input_hook runs before both host paths, so deliberately allow its drain-based
|
||
injection into pending_raw_input during a logic-only iteration. Eframe retains those
|
||
events for the next full pass. Send RequestPaintWhileHidden while an admitted gesture
|
||
has queued OR injected-but-unapplied events; acknowledge delivery only from known UI
|
||
phase. Preserve current pointer/key batching, generation filtering and event order.
|
||
Test hidden injection followed by forced paint: one action/reply, no dropped or replayed
|
||
events. An empty synthetic queue alone does not mean pending input was already applied.
|
||
- Pure engine commands (`engine.execute`) never request a hidden paint.
|
||
- `ui.screenshot` already forces a paint through the `Screenshot` viewport command; do not
|
||
add a second `RequestPaintWhileHidden` for it. Still send `Focus` as today.
|
||
- `ui.dialog.confirm` and other methods whose result depends on layout follow the same
|
||
pending-gesture rule.
|
||
- The control reply may be `ok` before all events are applied (current behavior). Tests and
|
||
documentation must assert the final command/undo/document result, not the number of
|
||
events sent or a single paint request.
|
||
|
||
### 3.4 Screenshot callback migration
|
||
|
||
Current code (`lib.rs:587`, `drain_control`, `issue_screenshots` at 1636,
|
||
`collect_screenshots` at 1652) stores `(token, path, reply, deadline)` in `pending_shots`,
|
||
queues `(token, at, frames)` in `queued_shots`, sends
|
||
`ViewportCommand::Screenshot(UserData::new(token))`, then scans `Event::Screenshot` for
|
||
`user_data.downcast_ref::<u64>()`. All of that changes.
|
||
|
||
Shared contract: the suite selects one bounded, cancelable completion slot per admitted
|
||
request. Eleven existing craft-ui creative consumers adopt its helper; DeckCraft, FigCraft and ArtCraft Native keep a
|
||
small local contract-compatible adapter. DesignCraft uses craft-ui. The helper is staged as
|
||
CL-2 (provisional) and frozen as CL-2F after DesignCraft and GridCraft adapters are reviewed.
|
||
|
||
DesignCraft request record:
|
||
|
||
- Keep the existing token counter, path, reply channel, 8 s deadline and 120 ms/3-frame
|
||
settling rule in `DesignCraft`. These are app-owned.
|
||
- Each `pending_shots` record owns one `craft_ui::capture::CaptureSlot` (provisional API;
|
||
final name/API after CL-2F). The slot holds the request id, target `ViewportId`, deadline,
|
||
a weak wake handle and at most one `Arc<ColorImage>`.
|
||
- `issue_screenshots` sends
|
||
`ctx.send_viewport_cmd(ViewportCommand::Screenshot(ScreenshotCallback::new(move |image| {
|
||
if let Some(slot) = slot.upgrade() {
|
||
slot.complete(image); // weak slot; no DesignApp/Context strong capture
|
||
}
|
||
})))`.
|
||
The callback captures only a `Weak` reference to the slot. It must not capture
|
||
`egui::Context` strongly (no context self-retention cycle), must not encode PNGs, write
|
||
files, run commands, hold a long lock or borrow mutable app state.
|
||
- `ScreenshotCallback` is one-shot; clones share one callback and complete at most once.
|
||
- Bound admission by both request count and aggregate pending image bytes before issuing a
|
||
capture. Compute the allowed surface dimensions, reject an oversized/stale image on
|
||
completion, release its reservation and image, and account for renderer/readback transient
|
||
memory separately in peak RSS. A post-callback size check alone is not a bound.
|
||
- Capture the target `ViewportId`; use `request_repaint_of(viewport)` from the live slot, not
|
||
an implicit current/default viewport. Release the slot lock before waking.
|
||
- `drain_screenshot_results` runs in `logic_tick`, matches the slot record, takes the image
|
||
out under a short lock, then calls `control::save_screenshot(self, image.as_ref(), path)`
|
||
on the UI thread. Late callbacks for timed-out/canceled/closed records release the image
|
||
and send no reply/file.
|
||
- Correlate only `ViewportId::ROOT` for `ui.screenshot` until the protocol grows an explicit
|
||
viewport field; do not guess from the pointer.
|
||
- Account for E2 surface fitting: the returned image may be smaller than the window. Derive
|
||
x/y scale from the returned image dimensions and the recorded logical viewport rect/DPI;
|
||
a resize/DPI generation mismatch retries within the deadline or returns an error. Never
|
||
combine old pixels with current geometry.
|
||
|
||
Tests: two outstanding tokens/viewports resolve to their own paths; completion is one-shot;
|
||
out-of-order completion; timeout plus late callback; cancel/close/shutdown; delayed readback;
|
||
hidden-window wakeup; aggregate memory returns to zero after drain; a reused viewport/document
|
||
generation does not bind an old capture to a new document; no file/reply from a canceled
|
||
capture; `ui_shot` still renders whole-window PNGs.
|
||
|
||
### 3.5 Input, IME, clipboard and keyboard layouts
|
||
|
||
`Event::MouseWheel` constructors in `canvas.rs`, `menus.rs`, `dialogs.rs`, `chrome.rs`,
|
||
`new_document.rs`, `object_control.rs`, `panel_docking_tests.rs` and `lib.rs` must add
|
||
`source`. Use `egui::MouseWheelSource::Unknown` for synthetic wheel input unless a test
|
||
is specifically about `Trackpad`/`Momentum`; the canvas's `wheel_alt` only depends on
|
||
`phase` and `modifiers`, so behavior is preserved.
|
||
|
||
IME:
|
||
|
||
- The canvas (`canvas::ime_event`, `canvas::delete_surrounding`) currently handles
|
||
`Preedit`, `Commit`, `DeleteSurrounding { before_chars, after_chars }`.
|
||
`delete_surrounding` loops `text.delete` up to 64 times (`canvas.rs:2695-2701`), and each
|
||
`text.delete` is an ordinary undoable command (`crates/engine/src/lib.rs:565-585`), so the
|
||
current path can create up to 64 undo entries.
|
||
- E21 adds `ImeEvent::DeleteSurroundingBytes { before_bytes, after_bytes }`. Implement a
|
||
single mandatory engine command, `text.deleteSurroundingBytes { beforeBytes, afterBytes }`
|
||
(or an equivalent single `edit`), that deletes the exact UTF-8 ranges around the caret in
|
||
one `s.edit` call, producing one undo entry. Do not make this optional and do not
|
||
approximate bytes as characters/`text.delete` loops.
|
||
- Validate both counts are bounded (default cap 4096 bytes each and no larger than the story/
|
||
selection), both ranges are valid UTF-8 boundaries via `str::get`, no range extends past
|
||
the story, and no split grapheme is produced. Preserve track-changes behavior by marking
|
||
deleted text and stepping the caret, as `text.delete` does. Test replacement, deletion-
|
||
only batches, combining mark/flag/ZWJ, preedit/commit sequencing, `tool_composing()`,
|
||
focus/serial changes and canceled compositions.
|
||
- The web text agent keeps its existing `DeleteSurrounding` character behavior.
|
||
- E21b/P7b: add the bounded outgoing surrounding-text payload to `IMEOutput`, the built-in
|
||
`TextEdit` producer and the egui-winit W27 Wayland adapter. DesignCraft's custom canvas
|
||
editor (`canvas::ime_output`, `canvas.rs:2720-2753`) supplies a real model/preedit/UTF-8
|
||
adapter: logical committed document text around the caret, preedit excluded, UTF-8 byte
|
||
cursor/anchor, at most 4000 bytes, explicit Unavailable context (distinct from available-empty) when the selection
|
||
cannot fit, and clearing on focus loss, no IME, reset and password/no-context. It must not
|
||
pass display-bidi text or set the payload to `None` as a placeholder. Audit every
|
||
`IMEOutput` constructor/destructure. Tests cover outgoing text after delete+commit and
|
||
deletion-only batches, supplementary/combining/RTL text, preedit exclusion, long lines,
|
||
field/window switching and the full E21b contract: owner/context generation even
|
||
when unavailable, available-empty distinct, W27 committed clear boundary, latest-pass
|
||
None replacing earlier payload, old-focus-epoch invalidation, and current-epoch done
|
||
serial mismatch applying incoming edits once while deferring outgoing until a match.
|
||
Include initially empty→non-empty and normal→password→normal protocol cases.
|
||
|
||
Clipboard:
|
||
|
||
- Source contract: `eframe-0.36.2/Cargo.toml:234-237` forces
|
||
`egui-winit` with `features = ["clipboard"]` even with eframe defaults off. 0.37's eframe
|
||
exposes `clipboard = ["egui-winit/clipboard"]` and depends on egui-winit with
|
||
`default-features = false` (`eframe-0.37.0/Cargo.toml:60,239-241`). Without the feature,
|
||
`egui-winit-0.37.0/src/clipboard.rs:113-141` stores `set_text` only in-process. DesignCraft's
|
||
Copy/Cut calls `ctx.copy_text` (`crates/ui-egui/src/lib.rs:806-824`), and its focused-field
|
||
Paste path sends `ViewportCommand::RequestPaste` (`menus.rs:2717`).
|
||
- Enable `eframe/clipboard` in the D1 source-compatibility PR to preserve native OS
|
||
Copy/Cut/Paste. This activates arboard/smithay in egui-winit alongside the app's arboard
|
||
read path; keep the app-owned `install_clipboard_guard` so control actions never write or
|
||
read the user's clipboard, and keep `Services::clipboard_text` for the menu Paste path that
|
||
must not go through egui.
|
||
- Native evidence in D1: select text, Copy/Cut, read from an independent arboard
|
||
process/handle on X11 and Wayland; test object copy and SVG/text paste; verify focused
|
||
`TextEdit` paste and canvas paste are not doubled; verify control-channel privacy.
|
||
- D1 includes E4/W15P in its complete fork pin; D3 expands native policy coverage. E4 routes regular clipboard through winit's Wayland data device with an
|
||
arboard fallback; W15P adds a native PRIMARY path on the same connection/event queue and
|
||
per-seat lifecycle using SCTK 0.19.2 `PrimarySelectionManagerState`/devices/sources/offers.
|
||
Do not bind a second regular `wl_data_device`. Test PRIMARY pointer- and keyboard-triggered ownership (including keyboard-only focus), serial
|
||
freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfers,
|
||
password exclusion and no unchanged-selection reclaim on wlroots/KWin/Mutter. Native
|
||
capability evidence is still required; compile-only is not acceptance.
|
||
- `Event::PasteImage` is new but DesignCraft has no image-paste contract. Leave it
|
||
unhandled rather than inventing a placement path. Add a test that it does not panic and
|
||
does not leak into text paste.
|
||
|
||
Keyboard layouts: after the fork pin, `RawInput::keyboard_layout_is_latin` and
|
||
`RawInput::latin_layout_chars` (E19) must preserve tool and command shortcuts on Russian,
|
||
Arabic and Hebrew layouts. W25 covers Linux Latin-layout lookup, W26 macOS deletion and W27 Wayland surrounding
|
||
text; Windows keeps its own native input path. E19 also needs a real Windows test
|
||
with a non-Latin layout; a synthetic `KeyboardEvent` test is not enough
|
||
for the OS layout table.
|
||
|
||
### 3.6 Panels, docking and viewports
|
||
|
||
`DesignApp::ui` keeps the existing order:
|
||
|
||
1. `chrome::app_bar` (`Panel::top("app_bar")`)
|
||
2. optional `chrome::control_bar`
|
||
3. `chrome::status_bar` (`Panel::bottom("status_bar")`)
|
||
4. `toolbar::show` (`Panel::left("tools")`)
|
||
5. `dock::show` / `panel_docking::show` (`Panel::right("dock")`,
|
||
`Panel::right("shared-panel-dock")`, icon columns)
|
||
6. `CentralPanel::default()` last among panels
|
||
7. second-window `show_viewport_immediate`
|
||
8. floating panels/flyouts, story editor, dialogs/about/palette (areas/windows/modals)
|
||
9. custom-titlebar resize borders
|
||
|
||
0.37's `Panel` constructors accept `AsIdSalt`; the string salts remain stable, but the
|
||
panel `Id` is now `parent_ui.scope_id().with_salt(salt)` (`egui/src/containers/panel.rs:974-
|
||
975`), whereas the 0.36 panel id was the salt alone. D1 updates test helpers such as `dock.rs:473` and `panel_docking_tests.rs:639` to
|
||
load the parent-salted key using the actual parent Ui scope. Eframe persistence is disabled:
|
||
there is no saved egui PanelState migration. Preserve app-owned UiState/docking layout through
|
||
`save_prefs` and test its save/reopen behavior and panel rectangles. Keep the docking identity
|
||
`designcraft-panel-docking` unchanged for saved layout and live drag state.
|
||
|
||
`craft-ui` CL-1:
|
||
|
||
- Do not use PR199's `b68de8c`/`ec8840a` trees as a base. They are stale relative to current
|
||
`4919088`, omit `crates/platform`, and revert current security/settings/docking/printer
|
||
fixes (including floating-group move/resize guards `d66e345`/`fd9cf54`).
|
||
- Apply only the API-port hunks from `8614e4a` (and relevant README changes from
|
||
`1764bca`) onto current `4919088`, preserving intervening fixes, `crates/platform` and
|
||
unrelated crates. Review a diff against both `5c874d51` (the current DesignCraft pin) and
|
||
`4919088`; pin the exact resulting immutable revision.
|
||
- `WidgetType` → `Role` in `buttons/mod.rs`, `docking/surface.rs`, `layout/resize.rs`,
|
||
`tabs/mod.rs`; `Id::new` → `Id::unique`; `Ui::id`/`UiBuilder::id` → `scope_id`.
|
||
- The corrected revision keeps the same docking transaction model and `DockArea` API
|
||
DesignCraft uses. A model/permission change is not incidental: if intentionally changed,
|
||
state it as a product decision with tests.
|
||
- CL-3 adds the explicit opt-in `DockArea::direction(DockDirection::{LeftToRight,
|
||
RightToLeft})` visual policy. DesignCraft calls `LeftToRight` explicitly for now (no E9
|
||
global mirroring) and relies on CL-3's geometry/hit/drag/keyboard tests. Saved physical
|
||
splits, rectangles and logical tab order must be unchanged by direction; never reverse
|
||
saved panel order or pass display-bidi text into shaping/clipboard.
|
||
|
||
The second window uses
|
||
`ViewportId::from_hash_of("designcraft_second_window")` and
|
||
`ViewportBuilder::with_title(...).with_inner_size([960, 720])`. Keep the ID and the
|
||
immediate viewport. Add tests for: opening/closing the second window through
|
||
`window.newWindow`; the main window's pane focus returning on click; close-request
|
||
cancellation of the main viewport; screenshot correlation to the root viewport only;
|
||
`CentralPanel` not consuming the second window's available rect; and #8748 hidden-parent/
|
||
deferred-child behavior while the root is occluded/minimized/overdue and while
|
||
`visible() == None` (open, update, close, and acknowledge without restoring the parent).
|
||
|
||
### 3.7 Fonts, glyphs, RTL and accessibility
|
||
|
||
`theme::font_definitions` builds `FontDefinitions` from bundled Source Sans 3/JetBrains
|
||
Mono plus optional craft-fonts (`CRAFT_FONTS_DIR`) and installed system faces selected by
|
||
`SYSTEM_UI_FONTS`. `install_fonts` returns false while the background font scan is
|
||
running and the app reinstalls when `system_fonts_ready()` becomes true. This must stay the
|
||
source of truth.
|
||
|
||
Concrete changes:
|
||
|
||
- `theme::SystemFont::new` and the `FontData { index, tweak, ..from_owned }` constructions
|
||
are source-compatible with 0.37's `FontData`; only test assertions that read
|
||
`definitions.font_data[name].font` or `data.as_ref()` need `data.bytes()`/`font.as_ref()`.
|
||
- Set `NativeOptions::system_font_fallback = false` explicitly and keep eframe's
|
||
`system_fonts` feature off. This prevents `egui_system_font_provider` from adding fonts
|
||
ahead of DesignCraft's script-specific order and makes the policy visible in code.
|
||
Document that DesignCraft's own `FontDb` provides system fallbacks.
|
||
- `FontDefinitions::default()` is empty when `default_fonts` is off
|
||
(`epaint-0.37.0/src/text/font_definitions.rs:111-117`). Add `egui/default_fonts` explicitly
|
||
to the `designcraft-ui-egui` dev feature set and install DesignCraft's fonts before layout
|
||
in package-scoped tests, together with explicit `monochrome_emoji_fonts` where used.
|
||
This restores egui's built-in icon/emoji fallback without relying
|
||
on the app binary's feature unification.
|
||
- Production keeps `MissingGlyphPolicy::Tofu` (the default). Tests that deliberately draw
|
||
unsupported scripts use `HarnessBuilder::allow_missing_glyphs()` with a named reason; tests
|
||
proving coverage keep the panic policy. Do not set tofu globally in production tests
|
||
merely to silence a regression.
|
||
- Test fixtures use the licensed fonts pinned in the egui plan §10.6 (craft-fonts
|
||
`2481a8a`, Noto Sans Arabic/Thai/CJK, BIZ UDPGothic; Noto Sans Hebrew and Noto Color Emoji
|
||
from their upstream OFL sources with the recorded SHA256/licences). DesignCraft keeps its
|
||
own policy: Latin UI/document fonts stay in `assets/fonts/`; central craft-fonts remains an
|
||
optional `CRAFT_FONTS_DIR` input; do not copy new font binaries into this repository.
|
||
Fetch fixtures into the task's explicit fixture directory and retain licences.
|
||
- Add a glyph-coverage test over every `i18n` catalog string and every UI font family,
|
||
using `FontDb` faces, not egui's `has_glyph` alone (it can report false for the face
|
||
that supplies the replacement glyph). Gate craft-fonts-specific CJK expectations on
|
||
`CRAFT_FONTS`; when absent, assert the built-in coverage and skip CJK glyph assertions
|
||
explicitly. Existing Ukrainian/Russian tests in `theme.rs` are the model.
|
||
|
||
RTL/bidi single owner:
|
||
|
||
- At D1 the fork's E5/E12 becomes the only bidi owner for egui-rendered UI/label text.
|
||
`rtl::widget`/`rtl::label` pass logical `LayoutJob`s (with explicit language/direction
|
||
where needed) to `fonts.layout_job`; remove the manual `BidiInfo::visual_runs` reordering,
|
||
section reversal, neutral-character mirroring, Arabic family swap and per-row
|
||
`row.pos.x += width - row.size.x` right-alignment in `rtl.rs`. Keeping them would process
|
||
Arabic/Hebrew a second time in epaint's `BidiLevels::new` (the fork's
|
||
`text_layout.rs:120` always runs bidi; `LayoutJob::direction` defaults to `Auto`).
|
||
The document composer's own `crates/compose/src/bidi.rs`/`shape.rs` remains the bidi
|
||
owner for document text and canvas glyph placement; this change is about the egui label
|
||
path, not a rewrite of document composition.
|
||
- Keep `Galley::job.text` as the logical source and use logical strings for clipboard,
|
||
save, accessibility, search and commands. Never pass visual-order/display-bidi text to
|
||
shaping or the clipboard. Keep the bounded layout cache (256 labels/4096 bytes) keyed by
|
||
the logical job, font token and language/direction.
|
||
- Editable `TextEdit` paths use the fork's logical text and grapheme/caret mapping; the
|
||
custom canvas editor must not feed visual-order strings.
|
||
- D3 differential tests: Arabic/Hebrew/mixed fixtures assert visual glyph order, logical
|
||
`Galley::job.text`, row widths, caret/hit positions, selection across boundaries, marks,
|
||
end caret and exactly one alignment (no double right-shift). ASCII fixtures must remain
|
||
identical to stock 0.37 where the API is shared. Run the fixtures at D3, not "only if
|
||
replaced".
|
||
|
||
`Context::set_language` (E17, fork only) is applied once when `ui.language` changes,
|
||
after fonts are installed and before layout. It must not change stored document text,
|
||
ids, commands or persisted workspace data. `Context::language` is the fallback on the
|
||
web if eframe does not set it.
|
||
|
||
Do not enable `Style::right_to_left` (E9). DesignCraft keeps an LTR panel/docking model with
|
||
RTL text inside labels, matching the persisted `craft_ui::docking::Layout` and existing
|
||
workspaces. Enabling E9 would mirror menus, scrollbars and docking chrome without a product
|
||
decision and would double-mirror shaped text. The explicit `DockArea::direction(LeftToRight)`
|
||
above is the only docking direction policy in this migration.
|
||
|
||
Accessibility labels: kittest's `check_accessibility` now runs by default, and the first
|
||
green stage must satisfy strict names/glyph policy. Add explicit names where a control is
|
||
an input (`Role::Button`, `TextInput`, `CheckBox`, `Slider`, `SpinButton`, `ComboBox`, `Tab`,
|
||
`Splitter`):
|
||
|
||
- `icons::button`, `widgets::icon_toggle_sized`, `widgets::checkbox`,
|
||
`widgets::dropdown`, toolbar/fill/stroke chips: use the existing localized tip/`label`
|
||
via `Response::accessible_name` or `WidgetInfo`.
|
||
- `widgets::NumField`: add a label field or `.accessible_name`; do not leave the
|
||
internal `id` as the only name.
|
||
- `dialogs::text_field`: pass the visible label id and use `ui.label_inputs_by(label.id,
|
||
…)` or `ui.name_inputs(tr(key), …)`; section labels added as plain `ui.label` do not
|
||
automatically name the field.
|
||
- `panels/font_menu.rs`, `panels/pages.rs`, `panels/layers.rs`, `panels/library.rs`,
|
||
`story_editor.rs`, `new_document.rs`: bind each field to its visible label; icon-only
|
||
star/expand buttons use their localized tooltip.
|
||
- Role migration files: the plan's earlier list omitted `panels/properties.rs`,
|
||
`panels/swatches.rs`, `panels/layers.rs` and `object_control.rs`. Map: swatch/color well →
|
||
`Role::Button` with the swatch name/value; layer row → `Role::ListItem`/`Role::Button`
|
||
matching its selectable row behavior; object-control label → `Role::Label`; combo/dropdown →
|
||
`Role::ComboBox`; numeric field → `Role::SpinButton`; text field → `Role::TextInput`;
|
||
checkbox → `Role::CheckBox`; tab → `Role::Tab`; split/resize → `Role::Splitter`.
|
||
- `craft_ui::docking::DockArea` already names tab/overflow/resize controls after CL-1/CL-3;
|
||
keep `Role::Tab`, `Role::Button`, `Role::Splitter` and labels stable.
|
||
- `Response::labelled_by` is preferred when a visible label exists;
|
||
`Response::accessible_name` for icon-only controls; `Ui::name_inputs` only for
|
||
third-party/grouped widgets that cannot be named at their construction site. Do not
|
||
paper over a missing label with a generic "field" name.
|
||
|
||
### 3.8 Theme and visual defaults
|
||
|
||
`theme::apply` sets `Visuals`, text styles and widget palette from `Tokens`; most colors
|
||
are explicit. 0.37's roomier button defaults and larger corner radius can change layout
|
||
even when tokens are unchanged. Audit:
|
||
- `v.button_padding`, `v.menu_margin`, `v.window_margin`, `v.item_spacing`,
|
||
`v.interact_size` and `CornerRadius` in `theme::apply`;
|
||
- `widgets::FIELD_H`, `BUTTON_H`, `SEG_W`;
|
||
- panel default sizes in `dock.rs`, `panel_docking.rs`, `toolbar.rs`;
|
||
- menu popup width/height and `menus::menu_button`;
|
||
- `chrome::app_bar` / `control_bar` / `status_bar` heights.
|
||
|
||
Do not replace explicit geometry with 0.37 defaults. A visual diff is an intended change
|
||
only after review and is recorded in evidence.
|
||
|
||
## 4. Dependency and publication topology
|
||
|
||
### 4.1 Intended suite graph
|
||
|
||
For suite validation, DesignCraft resolves exactly one copy of each egui-family crate and
|
||
one winit. The revisions below are placeholders until the reviewed `storytold` publications
|
||
exist; candidate `aiongg` hashes are comparison inputs only.
|
||
|
||
```
|
||
designcraft / designcraft-web / designcraft-ui-egui
|
||
-> egui 0.37.0 git <FINAL_STORYTOLD_EGUI_REV> (contains E1-E21, E21b, E4/P2b)
|
||
-> eframe 0.37.0 git <FINAL_STORYTOLD_EGUI_REV>
|
||
-> egui-winit 0.37.0 git <FINAL_STORYTOLD_EGUI_REV>
|
||
-> egui-wgpu 0.37.0 git <FINAL_STORYTOLD_EGUI_REV>
|
||
-> egui_kittest 0.37.0 git <FINAL_STORYTOLD_EGUI_REV> (dev)
|
||
-> epaint/ecolor/emath/epaint_default_fonts git <FINAL_STORYTOLD_EGUI_REV>
|
||
-> winit 0.30.13 git <FINAL_STORYTOLD_WINIT_REV> (W1-W27 plus W15P PRIMARY)
|
||
-> craft-ui git <FINAL_CRAFT_UI_REV> (CL-2F after adapter review; includes CL-1/CL-3)
|
||
-> wgpu 30.0.1 registry
|
||
-> accesskit 0.25.1 registry
|
||
```
|
||
|
||
`egui_extras` is not used by DesignCraft; do not add or patch it. `egui_plot` and
|
||
`egui_dock` are not used; do not assume version numbers for them. `egui_system_font_provider`
|
||
is not used because the `system_fonts` feature stays off.
|
||
|
||
### 4.2 Root `[patch.crates-io]` additions
|
||
|
||
A dependency's own `[patch]` section does not propagate to this consumer. Add all of the
|
||
following to the root `Cargo.toml`, each with the same full immutable git revision and no
|
||
moving branch or machine-local path:
|
||
|
||
```toml
|
||
[patch.crates-io]
|
||
egui = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
eframe = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
egui-winit = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
egui-wgpu = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
egui_kittest = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
epaint = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
epaint_default_fonts = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
ecolor = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
emath = { git = "https://github.com/storytold/egui", rev = "<FINAL_STORYTOLD_EGUI_REV>" }
|
||
winit = { git = "https://github.com/storytold/winit", rev = "<FINAL_STORYTOLD_WINIT_REV>" }
|
||
```
|
||
|
||
The egui remote must be the published, reachable `storytold/egui` mirror containing the
|
||
reviewed fork history. Pinning `aiongg/egui` directly is not the production plan; it would be
|
||
an explicit policy decision, not a default. The winit remote must likewise be the final
|
||
reviewed `storytold/winit`, not the candidate URL/revision. A branch name, a machine-local
|
||
path, or a `path` override must never appear in a shipped manifest.
|
||
|
||
Workspace dependency versions change from `0.36` to `0.37` for `egui`, `eframe` and
|
||
`egui_kittest`. `egui_extras` is present in the workspace dependencies but is not used by
|
||
DesignCraft; bump it only if a future consumer adds a real dependency on it.
|
||
`apps/designcraft` keeps its eframe feature list and adds `clipboard` in D1 (see §3.5). Do
|
||
not add `system_fonts`.
|
||
|
||
### 4.3 Lockfile, clean checkout and duplicate checks
|
||
|
||
After generating a lockfile through the managed build workflow, verify:
|
||
|
||
- `Cargo.lock` has exactly one `egui`, `epaint`, `ecolor`, `emath`, `eframe`,
|
||
`egui-winit`, `egui-wgpu`, `egui_kittest` and `winit` entry.
|
||
- Every egui-family source is `git+https://github.com/storytold/egui?rev=<FINAL...>#<FINAL...>`;
|
||
no `registry+.../egui-0.36.2` or `-0.37.0` remains and no candidate `aiongg` source is
|
||
shipped.
|
||
- `winit` source is the exact final storytold revision; no registry 0.30.13 and no old
|
||
`3d51077f...` copy remains.
|
||
- `wgpu` stays `30.0.1`; `accesskit` moves to the version egui 0.37 requires
|
||
(`0.25.1` in the reference graph).
|
||
- `cargo metadata --locked --format-version 1` resolves the same graph as the lock.
|
||
- `cargo tree -d` shows no duplicate egui-family crate and no duplicate winit. A
|
||
duplicate `accesskit_consumer` from unrelated crates is a warning to inspect, not an
|
||
automatic failure.
|
||
- `cargo xtask layers` still reports `designcraft-ui-egui` as the only egui consumer
|
||
below L7, and `cargo xtask wasm` includes `designcraft-ui-egui`.
|
||
- `cargo xtask ci` passes with the exact lock and with and without `CRAFT_FONTS_DIR` for
|
||
the font tests that support both.
|
||
- Clean checkout: `cargo fetch --locked` and `cargo metadata --locked --format-version 1`
|
||
succeed without local overrides. Negative fixture: replacing the final egui/winit revision
|
||
with a candidate hash or the old DesignCraft winit override must fail the gate.
|
||
|
||
The root `[patch.crates-io]` also contains the unrelated PDF entries. Keep
|
||
`krilla`, `hayro`, `hayro-interpret`, `hayro-syntax` and `hayro-jbig2` at
|
||
`storytold/craft-libs` `89dcbb3060377a26c99a6d80caa16084366704c4` unchanged. They are not
|
||
egui vendors and have their own provenance, tests and removal conditions.
|
||
|
||
### 4.4 craft-libs CL-1/CL-2 and language work
|
||
|
||
CL-1 is a rebased API port, not a merge of PR 199's tree. Start from current
|
||
`4919088`, apply only the `8614e4a` hunks (plus relevant `1764bca` README changes), and
|
||
preserve `d66e345`/`fd9cf54`, `crates/platform`, security/settings/printing and unrelated
|
||
crates. Review the diff against both `5c874d51` (DesignCraft's current pin) and `4919088`.
|
||
The corrected revision must:
|
||
- keep `craft-ui` production source compatible with **stock** egui 0.37.0;
|
||
- expose the same docking transaction model and `DockArea` API DesignCraft uses, plus the
|
||
CL-3 opt-in direction API;
|
||
- provide the CL-2 capture slot and CL-2F frozen API used by DesignCraft's screenshot adapter;
|
||
- pass its registry-egui 0.37 check, all-feature clippy and tests;
|
||
- publish an immutable revision that DesignCraft can pin.
|
||
|
||
DesignCraft does not take fork-only API from `craft-ui`: the fork lives in the dependency
|
||
patch table, not in `craft-ui`'s manifest. This keeps `craft-ui` publishable and lets
|
||
DesignCraft choose the fork independently.
|
||
|
||
Language work (#198) enters DesignCraft only through the final fork pin and the selected app
|
||
changes in §3.5/§3.7. It must not introduce ICU locale conversion, change document language
|
||
semantics, mirror the whole UI, add fonts, or replace the docking model.
|
||
|
||
## 5. Existing patches and vendored fixes: keep/remove audit
|
||
|
||
DesignCraft has no `vendor/` directory. Its relevant non-registry inputs are:
|
||
|
||
| Patch / input | Current revision | Provenance | Decision for this migration | Removal condition |
|
||
| --- | --- | --- | --- | --- |
|
||
| `winit` | `storytold/winit` `3d51077f...` (W1–W14) | storytold/winit `craft/0.30.13`; X11 `with_x11`, macOS `with_close_requested_on_terminate`, `mac_window::sync_movable` | Replace with `<FINAL_STORYTOLD_WINIT_REV>` (W1–W27 plus W15P). W12/W14 remain; W15/W15P/W25/W26/W27 are needed by E4/E19/E21. | Upstream winit 0.30/0.31 contains the equivalent patches and the suite no longer needs the fork. |
|
||
| `egui`/`eframe`/`egui-winit`/`egui-wgpu`/`egui_kittest`/`epaint`/`ecolor`/`emath` | registry 0.36.2 | crates.io | Replace by `<FINAL_STORYTOLD_EGUI_REV>` containing E1–E21, E21b and E4/P2b in D1 after the isolated stock-0.37 comparison, including P7c/P8. Candidate `a4f9a17f` is comparison input, not the pin. | Upstream egui release contains the equivalent behavior (or the suite drops each patch when its `PATCHES.md` condition is met). |
|
||
| `egui_extras` | registry 0.36.2, only in `craft-ui` dev-dependencies | crates.io; not used by DesignCraft | Leave alone. Do not add a patch entry unless a DesignCraft crate directly uses it. | No removal needed. |
|
||
| `krilla` | craft-libs `89dcbb30...`, version `=0.8.2` | PDF backend vendor patch in craft-libs; unrelated to egui | Keep exactly. | Its own PDF backend gate. |
|
||
| `hayro`, `hayro-interpret`, `hayro-syntax`, `hayro-jbig2` | craft-libs `89dcbb30...`, pinned versions | PDF parser vendor patches in craft-libs | Keep exactly. | Their own PDF parser gates. |
|
||
| `craft-ui` | `5c874d51...` | craft-libs published revision | Move to the reviewed CL-1/`<CL-2_PROVISIONAL_REV>` in the D1 capture pilot, then `<FINAL_CRAFT_UI_REV>` (CL-2F/CL-3). | Replaced by the final reviewed revisions. |
|
||
| `craft-pdf`, `craft-atomic`, `craft-cms`, `craft-docpath`, `craft-print` | individual craft-libs revisions | Shared libraries, not egui | Keep. Do not conflate their revisions with the UI migration. | Their own release cadence. |
|
||
|
||
Do not delete an unrelated vendor or patch merely because the lockfile changes. Do not copy
|
||
`aiongg/egui`'s workspace `[patch]` section into DesignCraft's manifest; only the root
|
||
`[patch.crates-io]` entries above apply.
|
||
|
||
## 6. Test and regression strategy
|
||
|
||
### 6.1 Fixtures and determinism
|
||
|
||
- Use `file.newSample` / `designcraft_engine::sample::magazine()` for a deterministic
|
||
multi-page document. Avoid user files and environment-dependent font caches.
|
||
- Run native tests with `DESIGNCRAFT_NO_PREFS=1` and a dedicated
|
||
`DESIGNCRAFT_CONFIG_DIR`, or an explicit empty override, so preferences and recovery do
|
||
not leak between runs.
|
||
- Build with the pinned craft-fonts ref `abb83316d96aa59c1cf64784289e378fe9fa5695` for
|
||
release/font evidence, and also without `CRAFT_FONTS_DIR` to keep the bundled-font path
|
||
tested. Use the §10.6 font fixtures for Arabic/Thai/CJK/Japanese/Hebrew/color-emoji
|
||
coverage with their recorded SHA256/licences; keep them out of app source.
|
||
- Use fixed window sizes 1440×900 and 1280×800, at 1× and 2× `pixels_per_point`, and
|
||
fixed themes `mediumDark`, `light`, `highContrast`. Record the exact fixture, font
|
||
directory, DPI and OS/renderer with every screenshot.
|
||
- Use `CRAFT_UI_DOCKING_FIXTURES` for the existing explicit docking visual fixtures;
|
||
do not turn those optional captures into unconditional CI jobs.
|
||
- Every command runs through `maintenance/task.py run/status/finish` with an allocated
|
||
target/VM and durable evidence; do not run tests directly.
|
||
|
||
### 6.2 Unit / kittest regressions
|
||
|
||
| Area | Test (new or adapted) | User-observable assertion |
|
||
| --- | --- | --- |
|
||
| Frame staging | `logic_frame_runs_once_across_a_discarded_pass` | Force a `request_discard` by making the Tools panel single-column too tall; assert one font/theme/recovery/FPS tick and one input stage, not two. |
|
||
| Host staging | `host_multipass_counts_one_presented_frame` | Drive the real host `App` wrapper through a forced discard; `frames`/startup.presented/error logging advance once, and macOS menu/Apple-event polling is not duplicated when available. |
|
||
| Hidden logic | `hidden_logic_ticks_after_completed_multipass_keep_working` | A completed multipass frame followed by repeated `run_logic` ticks runs font readiness/recovery/queue checks each tick with fresh viewport data and no stale index assumption. |
|
||
| Hidden forced paint | `forced_hidden_paint_is_a_ui_pass` | `RequestPaintWhileHidden` with `visible() == Some(false)` and with `visible() == None` runs `logic` + `ui`; visibility is not used as a phase guard. |
|
||
| Control while hidden | `control_gesture_requests_paint_until_all_events_apply` | A hidden multi-frame `ui.click`/`ui.drag`/`ui.key` sends `RequestPaintWhileHidden` on each hidden frame while events remain; the final command/undo result is correct. Pure `engine.execute` requests no paint. |
|
||
| Screenshots | `two_screenshot_viewports_resolve_to_their_own_paths` | Internal capture-helper test (the `ui.screenshot` control method stays ROOT-only): two outstanding requests with different tokens/viewports receive the matching image; no cross-write. |
|
||
| Screenshot lifecycle | `screenshot_cancel_timeout_and_late_callback_release` | Timeout/cancel/close writes no file and sends no second reply; a late callback releases its image; aggregate pending bytes return to zero. |
|
||
| Screenshot memory | `screenshot_admission_bounds_aggregate_bytes` | Oversized/concurrent captures are rejected or bounded; peak retained bytes stay within the admitted budget; E2 surface-fit scaling is correct. |
|
||
| Mouse wheel | `wheel_events_keep_their_source_and_phase` | Alt-wheel zoom, plain-wheel scroll, trackpad phase end and momentum source do not latch modifiers. |
|
||
| Roles/IDs | `role_and_id_migration_keeps_existing_state_keys` | A saved `UiState`/workspace round-trips; panel tabs, layer rows and styles resolve by the same stable IDs. |
|
||
| Panel state | `saved_panel_state_migrates_to_parent_salted_ids` | Old-format panel rects/widths load into the new parent-salted keys; the same outer rects and panel order result. |
|
||
| Panels | `panel_order_and_central_rect_are_unchanged` | App bar/control/status/tools/dock occupy the same outer rects; central canvas rect is unchanged at 1440×900 and after narrow-resize. |
|
||
| Docking | `saved_workspace_restores_tree_float_and_hidden_panel` (existing plus 0.37) | Docking tree, floating geometry and hidden placement survive serialization; invalid actions are atomic; direction changes do not reorder saved splits/tabs. |
|
||
| Second window | `second_window_opens_closes_and_keeps_main_focus` | Popout exists, close request removes it, main pane focus returns; main close can still be cancelled. |
|
||
| Hidden child (#8748) | `hidden_parent_child_viewport_updates_and_closes` | With the parent occluded/minimized/overdue and with `visible() == None`, the child opens/updates/closes and acknowledges without restoring the parent. |
|
||
| Accessibility | `every_visible_input_has_an_accessible_name` | `Harness::unnamed_widgets()` is empty; specific roles/labels for tabs, icon buttons, numeric fields, combo boxes, layer rows, swatches and text fields. |
|
||
| Glyphs | `every_catalog_character_has_a_face_or_a_declared_gap` | Bundled fonts cover Latin/Cyrillic catalogs; craft-fonts CJK assertions run when present; tests use panic policy for covered text and explicit tofu only for deliberate gaps. |
|
||
| IME exact bytes | `ime_replacement_deletes_exact_bytes_in_one_undo_step` | `DeleteSurroundingBytes` replaces a combining mark/letter and Undo restores the exact prior text in one step; `DeleteSurrounding` character path still works; caps/UTF-8/track changes are enforced. |
|
||
| IME surrounding text | `outgoing_surrounding_text_is_logical_and_bounded` | DesignCraft canvas emits logical model text after delete+commit/deletion-only; preedit excluded; <=4000 bytes at UTF-8 boundaries; focus/password/no-IME clears; no `None` placeholder; latest completed pass including None replaces the descriptor/payload atomically; owner remains present for Unavailable and W27 clear/epoch/serial behavior follows P7b. |
|
||
| IME composition | existing `canvas`/`story_editor` preedit-commit-cancel tests | Marked text does not duplicate, click-away discards on macOS, no undo step for a cancelled composition. |
|
||
| Text selection (#8763/#8764) | `click_away_then_button_acts_on_engine_selection` | A button reading the app's engine `TextSel` after clicking away from an egui `TextEdit` still acts on the intended range; stock 0.37's TextEdit cursor/selection loss is recorded and does not change the engine target; if a future control reads TextEdit state, the command is deferred with an explanation. |
|
||
| Commands/undo | `multipass_edit_is_one_undo_step` | A text edit, live-preview commit, dialog apply or toolbar/app-bar button produces exactly one history entry across discarded passes (including the second viewport). |
|
||
| Save/reopen | `save_reopen_preserves_document_and_workspace` | Save to a temp `.designcraft`, reopen through `file.openBytes`, compare document model/selection/style/swatches; saved workspace and panel layout restore. |
|
||
| Clipboard native | `copy_cut_write_the_system_clipboard` | On X11 and Wayland, select text/objects, Copy/Cut, read from an independent arboard reader; paste back; control actions never read or replace the clipboard. |
|
||
| CLI control privacy | existing `control_paste_never_reads_the_system_clipboard` | Control actions still never read or replace the user's clipboard. |
|
||
| Web drop | `web_drop_is_captured_in_raw_input_hook` | A browser `drop` with a synthetic File starts the async read in `raw_input_hook`, reaches the inbox once, and opens the document; no drop read remains in `WebShell::logic`. |
|
||
| Buffered input | `hidden_restore_drops_stale_text_ime_and_incomplete_pointer` | After a hidden tick, changed focus/document discards buffered text/IME/paste; a partial pointer chord is dropped; complete pointer/key events replay in order; two windows stay separate. |
|
||
| Menu/palette | existing `menus` tests plus 0.37 role checks | Menus open/close, keyboard activation, overflow within viewport, Quick Apply runs one command. |
|
||
| Canvas | existing guide/column/frame-hit tests plus new 0.37 geometry | Hit tests and smart guides do not regress from panel rect changes. |
|
||
|
||
### 6.3 Semantic accessibility assertions
|
||
|
||
Add `Harness::check_accessibility()` (the default) to UI tests and explicit queries:
|
||
|
||
- Menus: root menu items have `Role::MenuItem` or `Role::MenuBar`; disabled items remain
|
||
reachable by name.
|
||
- Panel tabs: `Role::Tab`, selected state and localized label; tab drag still starts from
|
||
the tab rect.
|
||
- Icon-only controls: tooltip text is the accessible name; star/expand, eye/lock, fill/
|
||
stroke chips, screen-mode toggles, toolbar overflow.
|
||
- Text fields: each has a visible `labelled_by` relationship or a `placeholder`; a blank
|
||
label is not an acceptable name. Numeric fields expose `Role::SpinButton` and a value.
|
||
- Swatches/color wells, layer rows, object-control labels, combo boxes: the mappings in
|
||
§3.7, with name/value/selected state.
|
||
- Sliders and split handles: `Role::Slider`/`Role::Splitter`, value/range and accessible
|
||
name; keyboard arrow changes still work.
|
||
- Dialog modals: `Role::Dialog` or a named `Area`; the title is the name; Escape cancels.
|
||
- Focus: Tab order reaches the control bar, panels, canvas and dialog buttons; focus ring
|
||
uses `Tokens::accent`/`focus_stroke`; no invisible widget is exposed.
|
||
|
||
These are semantic assertions, not only screenshot checks. A screen reader sees the same
|
||
names and roles across 0.36.2 -> 0.37 except where a deliberate mapping (e.g.
|
||
`SelectableLabel` -> `Button`) requires a new role.
|
||
|
||
### 6.4 Visual, native and browser workflows
|
||
|
||
- Whole-window PNGs at 1440×900/1× and 2×, medium-dark and light, with the same sample
|
||
document and fonts. Compare against 0.36.2 baseline produced in the same environment.
|
||
- Justify tolerances in advance: geometry assertions use exact `Rect` comparisons for
|
||
panel/canvas/tab rects (within 0.01 pt), and pixel comparisons allow antialiasing noise
|
||
only where the change is explained (for example 0.37 corner radius). Do not accept a
|
||
golden because it changed; inspect `.new` vs `.old`, list the intended deltas, and keep
|
||
the old baseline for rollback.
|
||
- Add high-contrast, Arabic, Hebrew, CJK and color-emoji fixtures. For RTL, compare glyph
|
||
order, logical `Galley::job.text`, row width and caret/hit positions, not only pixels.
|
||
- Windows: use a uniquely owned clone and `C:\CraftApps\Launch-CraftDesktop.ps1` through
|
||
the scheduled-task helper. Drive control-channel edit/save/reopen, dock/float/popout and
|
||
`ui.screenshot`, inspect fresh screenshots, and record the software renderer
|
||
(Microsoft Basic Render Driver) as a separate row.
|
||
- Linux: native X11 and Wayland runs with compositor identity; exercise panels, second
|
||
window, copy/paste/PRIMARY, file drop, IME (Wayland) and visible edited/saved/reopened
|
||
workflows. No inherited display/compositor exists at planning time; provision an
|
||
isolated display/window manager and compositor before claiming this row.
|
||
- Browser: run the Trunk build in a pinned browser with WebGPU and `?webgl`; load the sample,
|
||
edit, save/download, drag-drop a synthetic file, take screenshots and record the browser/
|
||
adapter/backend. Wasm compile and `ui_shot` are not browser presentation evidence.
|
||
- Keep software-renderer and real-GPU results separate. Capture native screenshots with
|
||
`ui.screenshot` or a native window capture; `ui_shot` is offscreen only.
|
||
|
||
### 6.5 Deliberate broken-behavior probes
|
||
|
||
Before accepting the suite, temporarily break each behavior and confirm the test fails:
|
||
|
||
1. Remove an `accessible_name` from a toolbar button -> accessibility test names it.
|
||
2. Change a panel salt (`"dock"` -> `"dock2"`) or load the wrong parent-salted test key
|
||
-> panel identity/geometry test fails; app-owned saved layout remains separately asserted.
|
||
3. Call `ctx.root_ui` inside `DesignApp::ui` -> test panics with the documented recursive
|
||
borrow message (run in a `catch_unwind` probe, not production).
|
||
4. Remove the first-pass `logic_frame` guard -> forced-multipass test sees two logic-frame
|
||
ticks. Do not use `visible()` to "fix" it.
|
||
5. Correlate screenshots by arrival order instead of the per-request slot -> two-token/
|
||
two-viewport test writes the wrong file; cancel a slot and let a late callback arrive ->
|
||
no file/reply.
|
||
6. Drop `DeleteSurroundingBytes` handling or fall back to the 64-step `text.delete` loop ->
|
||
exact-byte/one-undo test leaves replacement text or extra undo steps.
|
||
7. Remove the E21b surrounding-text adapter or emit display-bidi text -> logical/bounded
|
||
outgoing-text test fails.
|
||
8. Restore `rtl.rs`'s manual reorder alongside fork E5/E12 -> RTL differential test shows
|
||
double processing/double alignment.
|
||
9. Restore `Id::new`/`Ui::id` -> `clippy -D warnings` fails, proving the warning gate.
|
||
10. Reintroduce `Event::Screenshot` scanning or a strong `Context` capture in the callback ->
|
||
compile/test or cancel/shutdown test fails.
|
||
11. Move web drop reading back into `WebShell::logic` -> browser drop test fails.
|
||
12. Remove the host first-pass guard -> host multipass test sees two frame counts.
|
||
|
||
## 7. Performance measurement protocol
|
||
|
||
### 7.1 Baseline timing and environment
|
||
|
||
Record a baseline **before** changing production code, from the exact current SHA
|
||
`e74208dd...`, lockfile, feature set and craft-ui revision. Run the candidate from the
|
||
exact migration revision. Use separate fresh processes for each sample; do not compare a
|
||
warm process with a cold one.
|
||
|
||
The benchmark harness is test-only work and must exist in both baseline and candidate
|
||
trees. Stage it in a separate test-only commit, cherry-pick it into a read-only baseline
|
||
worktree at `e74208dd...`, and run the baseline first through `task.py`. It records
|
||
`Instant`-based raw samples around `run_ui`/operation boundaries; it must not use
|
||
`perf.frame_ms`/`now_ms()` (`SystemTime` on native, `0.0` on wasm) as the acceptance timer.
|
||
`ui.inspect` reports only the last frame and `ui_shot` prints control replies, not per-frame
|
||
timings; neither is a sample source.
|
||
|
||
Record with every result:
|
||
- source SHA (baseline/candidate/stock-0.37 control), `Cargo.lock` SHA-256, feature list,
|
||
`CRAFT_FONTS_DIR` and craft-fonts ref, fixture name/hash;
|
||
- OS and version, CPU/GPU, driver, backend (`Vulkan`/`DX12`/`Metal`/`GL`/`WebGPU`),
|
||
renderer type (hardware vs WARP/llvmpipe), display scale, window size, pixels-per-point;
|
||
- profile (`release` unless a debug-only diagnostic is named), warmups, repetitions,
|
||
raw samples, median/p95/p99, peak RSS, allocations, texture upload bytes and idle
|
||
CPU/repaint count.
|
||
|
||
Host capacity/qualification facts: read-only inspection finds an i915 PCI `8086:3E96`
|
||
render device and a readable/writable `/dev/dri/renderD128` to the parent; the worker
|
||
sandbox may hide it. No display session or compositor is inherited. Availability is not a
|
||
qualified Vulkan/OpenGL/native compositor result; the managed renderer probe must identify
|
||
the actual backend. The Windows baseline is software-rendered (Microsoft Basic Render
|
||
Driver). A 128 GiB Windows full clone may not fit current capacity; use an allocated
|
||
baseline-backed overlay with an adequate reservation or retain the platform row as blocked.
|
||
macOS and physical Windows/audio remain separate rows.
|
||
|
||
### 7.2 Operations
|
||
|
||
1. **Startup**: at least 20 fresh processes; measure wall/`Instant` from process start to
|
||
first presented frame and to first interactive sample document. Report RSS and the
|
||
#8512 system-font discovery/first-missing-glyph component separately. The #8736 DX12
|
||
renderer/shader/pipeline construction is a separate Windows row, never a UI frame.
|
||
2. **Idle UI**: open the sample, leave it untouched for 60 s, count frames/repaints and CPU.
|
||
No continuous repaint storm; the font scan's 250 ms poll must stop once cataloged.
|
||
3. **Frame distribution**: at least 1,000 individual frames per configuration, with at
|
||
least 10 warmups and raw per-frame samples; report p50/p95/p99. Run the sample, the Tools
|
||
panel two-column state and a floating dock. Record shapes, vertices/indices, texture
|
||
set/free count/bytes, galley cache size and requested-repaint count.
|
||
4. **Steady operations**: at least 30 timed iterations per operation after 10 warmups:
|
||
layout, panel open/float/dock/activate latency, canvas zoom/pan/damage patches,
|
||
selection and caret movement, text layout/tessellation.
|
||
5. **Text tessellation #8731**: fixed 120-label ASCII scene plus a mixed/color-glyph
|
||
control; time `Context::tessellate` separately for 0.36, stock 0.37 and the final fork.
|
||
If the ~60% plain-text regression reproduces, require a color-safe row-level fix or a
|
||
documented exception; clamp-only is not a fix.
|
||
6. **#8736 DX12**: Windows clone; time renderer/pipeline/shader construction separately from
|
||
first frame, with predictable filtering disabled/enabled as applicable. Record adapter
|
||
and driver.
|
||
7. **#8512 fonts**: cold process with bundled fonts, with `CRAFT_FONTS_DIR` and with a
|
||
test-only system provider fixture; compare discovery/startup and glyph coverage. Do not
|
||
enable the system provider in production to win the measurement.
|
||
8. **#8763/#8764 selection**: select text then click a toolbar/context action; record
|
||
command latency and assert the intended range/undo. This is correctness evidence; the
|
||
tail timing is a separate sample.
|
||
9. **#8748 hidden child**: open/update/close the second viewport while the parent is
|
||
hidden/occluded/overdue; record first visible child frame and close acknowledgement.
|
||
10. **Screenshots**: visible and hidden `ui.screenshot` requests; record reply latency,
|
||
file size, retained bytes, cancellation/timeout and late-callback cleanup.
|
||
11. **Memory**: RSS after sample load, after 10 pan cycles, after 30 screenshots and after
|
||
a drain/shutdown; retained image bytes must return to zero.
|
||
12. **Stock-0.37 control**: where the operation exists unchanged on stock 0.37, run the same
|
||
minimal adapter/harness against 0.36, stock 0.37 and the final fork; explain semantic
|
||
differences. Unavailable equivalence is an attribution limitation, not proof an
|
||
upstream regression is unavoidable.
|
||
|
||
Use matched commands and the same fonts/fixture for baseline and candidate. Do not report
|
||
`cargo build` time as UI performance.
|
||
|
||
### 7.3 Thresholds and noise policy
|
||
|
||
Predefine before looking at candidate numbers. The shared suite floors are minimums; an app
|
||
may tighten, never loosen:
|
||
|
||
- UI frame median: candidate <= baseline + 5%; p95 <= baseline + 10%; p99 (>=1,000 frames)
|
||
<= baseline + 15%.
|
||
- Canvas render median: <= baseline + 5%; p95 <= +10%; patch count no increase beyond
|
||
fixture determinism.
|
||
- Idle CPU: <= baseline + 20% and no more than one extra repaint per second after fonts
|
||
are ready; zero paints in 2 s after settle.
|
||
- Startup: <= baseline +5% median / +10% p95, with the #8736 DX12 shader-compilation component reported
|
||
separately. A #8736-sized regression needs the upstream/fork fix or explicit approval.
|
||
- Screenshot reply p95: <= baseline + 10%; retained bytes bounded and zero after drain.
|
||
- Peak RSS: investigate a repeatable increase over +5% or +16 MiB, whichever is reached
|
||
first; report absolute and percentage. (Use the app's existing stricter budget if one is
|
||
found.)
|
||
- Text tessellation: if the isolated probe reproduces #8731's ~60% regression, require a
|
||
color-safe row-level fix or a documented exception; never accept a clamp-only shader
|
||
change.
|
||
- Sub-millisecond operations: the relative increase must also exceed 0.05 ms or be
|
||
corroborated by stable instruction/allocation counts.
|
||
|
||
Sample counts: at least 10 warmups and 30 timed iterations per steady operation; at least 20
|
||
fresh processes for startup; at least 1,000 individual frames for frame distributions. Use
|
||
five independent sessions of at least 200 frame samples for tail percentiles, and pool the
|
||
raw samples for p99; do not report p99 from 30 samples. Alternate baseline/candidate rounds;
|
||
discard a session if a build/test/profiling process competes for CPU or the coefficient of
|
||
variation of medians exceeds 3% (5% for microbenchmarks), then rerun. If the difference is
|
||
within 2×MAD of paired samples, call it equal and do not chase it. Re-run and profile before
|
||
filing or accepting a regression. Never auto-update a snapshot or lower a threshold to hide
|
||
a timing or geometry change.
|
||
|
||
## 8. Staged implementation PRs and rollback
|
||
|
||
The dependency DAG is:
|
||
|
||
```text
|
||
plans land -> baseline harness + measurements
|
||
completed winit -> egui P8 (P2b/P7/P7b/P7c included)
|
||
-> craft-libs CL-1 -> CL-2 reviewed capture prototype
|
||
-> DesignCraft D1 / GridCraft G1 pilot adapters -> CL-2F API freeze
|
||
DesignCraft D1 -> D2 lifecycle -> D3 extended native/language qualification
|
||
craft-libs CL-3 + D1 -> D4 docking/accessibility
|
||
D2/D3/D4 -> D5 visual/workflow acceptance -> D6 final platform/performance evidence
|
||
```
|
||
|
||
- **D1 — atomic final-fork compatibility pilot.** Pin the complete final storytold
|
||
egui/winit graph and reviewed craft-ui CL-1/CL-2 prototype; regenerate the lock.
|
||
Apply roles/IDs/FontData/wheel changes, explicit native clipboard and test fonts,
|
||
strict minimum names/glyphs, the shared capture-slot adapter and its cancellation/
|
||
bounds/wake tests, parent-salted test lookup keys, and the required E21/E21b custom
|
||
editor adapter. Remove `rtl.rs` duplicate visual reorder/alignment as the final fork
|
||
becomes the bidi owner. Preserve app-owned UiState/docking preferences; no invented
|
||
egui-memory migration. Gate: cargo xtask ci/wasm, UI/native tests, one-family graph,
|
||
clean locked fetch, capture and IME contract tests, clipboard and save/reopen. This
|
||
reviewed pilot plus GridCraft's adapter permits CL-2F; stock 0.37 is an isolated
|
||
benchmark/control workspace, never a production handoff. Rollback restores complete
|
||
e74208dd source/manifests/lock/executable and prior preferences.
|
||
- **D2 — frame stages and hidden input.** Add the narrow once-per-layout-frame guard,
|
||
host pacing/queue separation, monotonic deadlines, buffered synthetic-input policy
|
||
and per-step hidden paint. The shared capture API is already adopted in D1; extend
|
||
hidden/multipass/restore stress tests here. Do not replace it with a local interim
|
||
slot. Revert D2 as a behavior/test unit only if D1's compatibility checks still pass.
|
||
- **D3 — extended native/language qualification.** After D2 and CL-2F, use the same
|
||
complete final fork and frozen helper; qualify E4/W15P, P7 key release, E21/E21b and
|
||
P7c selection/hidden-child behavior on native platforms and add language policy as
|
||
required. Required IME/bidi API adaptations already land in D1. Rollback removes
|
||
only these later policy/test changes; it never strips fork patches while retaining
|
||
fork-only source. A dependency rollback restores the full D1 predecessor set.
|
||
- **D4 — accessibility and glyph contracts.** Add the remaining names/roles, full role
|
||
file list, explicit `DockArea::direction(LeftToRight)`, and coverage tests. Gate:
|
||
`unnamed_widgets()` empty, explicit role/label queries, glyph coverage with and without
|
||
craft-fonts, direction geometry/hit/drag and saved-layout tests. Rollback: revert the
|
||
label/direction changes; no document or command behavior changes.
|
||
- **D5 — visual/geometry baselines and workflows.** Review every intended 0.37 visual
|
||
delta, update fixtures only with explanation, add the save/reopen/dock/popout, native
|
||
X11/Wayland, Windows VM and browser workflow tests. Gate: geometry/hit tests, pixel
|
||
comparisons, deliberate broken-behavior probes. Rollback: restore the previous reviewed
|
||
baselines; production remains on D3/D4.
|
||
- **D6 — qualification and performance.** Windows VM, Linux native, browser, macOS gate,
|
||
paired performance protocol with the stock-0.37 control. No production behavior change
|
||
unless a measured regression requires a separately reviewed fix. Rollback: no code
|
||
rollback expected; keep the evidence and the old dependency set until acceptance.
|
||
|
||
Each PR must be one coherent outcome, use one task id per commit, and carry the validation
|
||
results at the reviewed revision. No PR is published without fresh direct human approval.
|
||
|
||
## 9. Risks, unresolved questions and blocking acceptance criteria
|
||
|
||
### Blocking unknowns
|
||
|
||
1. **Final dependency publications.** The final reviewed `storytold/egui` and
|
||
`storytold/winit` revisions do not exist yet. Candidate `aiongg` hashes are comparison
|
||
inputs only. D1 cannot ship until the complete P8 handoff and winit revisions are published with E1–E21,
|
||
E21b, E4/P2b, W1–W27 and W15P, and a clean checkout fetches them `--locked`.
|
||
2. **craft-libs CL-1/CL-2F revisions.** The 0.37-compatible craft-ui revision must be the
|
||
`8614e4a` port onto current `4919088`, preserving `d66e345`/`fd9cf54`, `crates/platform`
|
||
and unrelated fixes. PR199's `b68de8c`/`ec8840a` trees are not bases. The capture API
|
||
freezes only after the DesignCraft/GridCraft adapter review.
|
||
3. **Clipboard/PRIMARY.** D1 enables `eframe/clipboard` to preserve OS Copy/Cut/Paste. D3
|
||
needs E4/W15P native evidence on X11 and Wayland (wlroots/KWin/Mutter): pointer-
|
||
and keyboard-triggered ownership including keyboard-only focus, serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/
|
||
slow transfers, password exclusion, no unchanged-selection reclaim and no second regular
|
||
data device. A compile-only or X11-only result is not acceptance.
|
||
4. **Windows GPU fallback.** E2/E3 turns some egui-wgpu configure/renderer failures into
|
||
`WgpuError` values rather than panics. DesignCraft's `gpu::finish` already handles
|
||
`eframe::Error::Wgpu(_)`, but the exact startup fallback behavior must be tested on the
|
||
Windows clone with a deliberately failing adapter/backend. Do not claim it from unit
|
||
tests only.
|
||
5. **macOS native evidence.** macOS menu shortcuts, press-and-hold IME, `deleteSurrounding`
|
||
bytes, `movable_by_window_background`, Dock Quit and viewport screenshots need real
|
||
hardware or remain explicitly unresolved. A software/VM screenshot is not native
|
||
evidence.
|
||
6. **`#8731` text tessellation.** The upstream report is a measured regression, not a
|
||
DesignCraft result. If the candidate reproduces it, a color-safe fix (row-level branch)
|
||
or an explicit exception is required before accepting the performance row.
|
||
7. **`#8736` DX12 startup.** The shader-compile cost is a Windows startup risk. Measure it
|
||
separately and do not hide it inside a UI frame average.
|
||
8. **`#8748` hidden-parent/deferred-child.** Closed and unmerged. The final fork must own a
|
||
tested solution; DesignCraft's immediate second viewport must open/update/close while the
|
||
parent is hidden/occluded/overdue and while visibility is unknown.
|
||
9. **Accessibility scope.** The exact number of unnamed inputs will only be known after
|
||
D1's first strict `Harness::check_accessibility()` run. D1 must fix the existing test
|
||
surface or record an explicit per-test policy; D4 completes the remaining controls. If
|
||
the D4 remainder is large, split "core chrome/panels" and "dialogs/rare panels"; do not
|
||
disable the check globally.
|
||
10. **Text selection #8763/#8764.** The source audit finds no control that reads an egui
|
||
`TextEdit` selection in the same pass, so no app workaround is planned. The fork owns
|
||
the upstream fix. If implementation finds such a control, carry #8764 or defer the
|
||
command with a tested app change; do not hide it in the test.
|
||
11. **E21b/P7b.** The incoming E21/W27 path is not full surrounding-text integration.
|
||
DesignCraft's canvas needs a real logical model/preedit/UTF-8 adapter, bounded at 4000
|
||
bytes and cleared on focus/password/no-IME; `None` placeholders are not acceptance.
|
||
Native Wayland IME trace remains required.
|
||
12. **DockArea direction.** CL-3's opt-in direction is a visual policy, not a schema change.
|
||
DesignCraft passes `LeftToRight` explicitly for now; saved physical splits/rectangles
|
||
and logical tab order must remain unchanged. No display-bidi text reaches shaping or
|
||
clipboard.
|
||
13. **Platform capacity/qualification.** The parent can see i915 PCI `8086:3E96` and
|
||
`/dev/dri/renderD128`; the worker sandbox may hide it, and no display session/compositor
|
||
is inherited. Windows needs a uniquely owned clone/overlay within allocator capacity
|
||
(a 128 GiB full clone may not fit). Browser WebGPU/WebGL and real native X11/Wayland
|
||
workflows need provisioning. These rows remain blockers until executed; availability
|
||
alone is not a pass.
|
||
14. **Capture API freeze.** CL-2F must be reviewed after both adapters. A strong `Context`
|
||
capture, unbounded pending images, a late file write/reply or a lock held across wakeup
|
||
is a blocking failure, not a follow-up.
|
||
15. **`Cargo.lock` coherence.** Regenerate, never hand-edit. A lock with both 0.36.2 and
|
||
0.37, a candidate `aiongg` source, or a machine-local path is an immediate failure.
|
||
|
||
### Non-blocking but important
|
||
|
||
- Style default changes may move toolbar/panel geometry even with explicit tokens.
|
||
- `egui_kittest` 0.37's missing-glyph panic can make an unrelated test fail first; fix the
|
||
fixture/name or use a named per-test allow with a reason, not a global tofu/disable switch.
|
||
- The web build's new glyph rasterizer can change emoji/fallback rendering. Verify with
|
||
WebGPU and `?webgl`, and keep texture filter/wrap expectations.
|
||
- `Cargo.lock` must be regenerated through the managed workflow, not hand-edited.
|
||
- The `designcraft-testkit` workspace dependency points at a missing `crates/testkit` in
|
||
the baseline; it is unused, but the dependency audit must not mistake it for an egui
|
||
consumer. Do not add or remove it as part of this migration.
|
||
- Saved docking direction must never be implemented by reversing saved panel order or
|
||
serialized tab order.
|
||
|
||
## 10. Acceptance checklist
|
||
|
||
- [ ] Exact baseline SHA and dependency revisions recorded; old 0.36.2 set retained for
|
||
rollback.
|
||
- [ ] All 22 manifests, root lockfile, root patch section, build scripts, `ui_shot`,
|
||
platform cfgs and CI workflows audited.
|
||
- [ ] Exactly one egui family and one winit resolve from the final reviewed storytold
|
||
revisions; no registry 0.36.2, candidate `aiongg` source or machine-local path remains.
|
||
- [ ] Clean checkout passes `cargo fetch --locked` and `cargo metadata --locked`.
|
||
- [ ] `cargo xtask ci`, `cargo xtask wasm`, `cargo test -p designcraft-ui-egui --lib`,
|
||
`trunk build --release`, native Windows/Linux and browser workflows pass at the
|
||
reviewed revision.
|
||
- [ ] `App::logic`/`App::ui` root-`Ui` flow preserved; native and web per-pass logic tested;
|
||
no recursive `root_ui`; multipass, repeated hidden ticks, forced hidden paint and
|
||
host counters tested; visibility is not used as a phase.
|
||
- [ ] No egui PanelState persistence is invented; parent-salted test keys, panel order/rects and app-owned workspace
|
||
restore.
|
||
- [ ] Screenshots use the craft-ui completion slot: weak callback, viewport-aware wakeup,
|
||
one-shot, timeout/cancel/close, out-of-order, bounded aggregate bytes and no strong
|
||
`Context` cycle.
|
||
- [ ] Commands, undo transactions, save/reopen, workspace persistence, clipboard privacy
|
||
and native Copy/Cut/Paste are unchanged.
|
||
- [ ] E4/W15P PRIMARY is evidenced natively on X11/Wayland, or explicitly unresolved;
|
||
password/unchanged-selection policy holds.
|
||
- [ ] E21b outgoing surrounding text is logical, bounded at 4000 bytes, preedit-excluded and
|
||
cleared on focus/password/no-IME; no `None` placeholder.
|
||
- [ ] Fork E5/E12 is the only bidi owner for egui UI/label text (the document composer
|
||
remains the document-text owner); no display-bidi text reaches UI shaping or clipboard.
|
||
- [ ] Accessibility role/name assertions pass under strict kittest; glyph coverage is
|
||
explicit with and without craft-fonts.
|
||
- [ ] Visual baselines have explained intended deltas; geometry/hit/direction/dock tests
|
||
pass; #8763/#8764 and #8748 tests have the documented expected outcome.
|
||
- [ ] Paired performance results meet the predefined thresholds with >=10 warmups/30
|
||
steady iterations/20 fresh startups/1,000-frame distributions and a stock-0.37
|
||
control, or every regression has a reviewed fix/exception.
|
||
- [ ] macOS, real-GPU, Wayland/GNOME and non-Latin-layout gates are either evidenced or
|
||
listed as unresolved; no result is implied from a compile or software renderer.
|
||
|
||
## 11. References
|
||
|
||
Primary source inspected locally (read-only; no checkout switch):
|
||
|
||
- egui upstream `0.37.0` = `841a328ba2dd8f6996beb7189ebb7fc00e668a2a`:
|
||
`crates/egui/src/data/input/event.rs`, `data/input/mouse_wheel_source.rs`,
|
||
`data/input/viewport_info.rs`, `data/screenshot_callback.rs`, `viewport.rs`, `context.rs`,
|
||
`containers/panel.rs`, `id.rs`, `ui.rs`, `data/output.rs`, `accessibility.rs`,
|
||
`input_state/mod.rs`; `crates/epaint/src/text/font_definitions.rs`,
|
||
`crates/epaint/src/text/font_data.rs`;
|
||
`crates/eframe/src/{epi.rs,native/epi_integration.rs,native/wgpu_integration.rs,web/app_runner.rs}`;
|
||
`crates/egui-winit/src/{clipboard.rs,lib.rs}`; `crates/egui-wgpu/src/{setup.rs,winit.rs}`;
|
||
`crates/egui_kittest/src/{builder.rs,accessibility.rs}`.
|
||
- Candidate fork for comparison: `aiongg/egui` `craft/0.37` =
|
||
`a4f9a17f64a6bae61350fb85804c9df0d9865916`, including `PATCHES.md` (E1–E21; E21b/P7b
|
||
still to be added). This is not the production pin.
|
||
- Candidate winit for comparison: `aiongg/winit` `craft/0.30.13-lang` =
|
||
`8b35388bd719e8cc86a95971ba0bc026c9a8f058`, including `PATCHES.md` (W1–W27; W15P still
|
||
to be added). This is not the production pin.
|
||
- `storytold/craft-libs` PR 199 head =
|
||
`b68de8c5f4f1c060904d211a7b1320aa0f7bce78`; audit merge
|
||
`ec8840af8c5fda695cf195cdd05e5ae078415c07`; API-port source
|
||
`8614e4a04fae516fba16c22b31ff1008cb6be10b`; current craft-libs baseline
|
||
`49190884278f16c9a9f5823d7698a8ffb5deb062`.
|
||
- `storytold/egui` published 0.36.2 =
|
||
`767b1cad294120689a514bb4bd68c077e46679b9`; `storytold/winit` published 0.30.13 =
|
||
`ffae5198770d7730e718043f035a772b7dbc3b1b`.
|
||
- DesignCraft baseline =
|
||
`e74208dd7e0c801d15dfdf926d64cfe5e3b346d3`.
|
||
- Lifecycle evidence:
|
||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/lifecycle-source-audit.md`
|
||
(native/web per-pass logic, zero pass index after completed `run_ui`, `visible()` is not a
|
||
phase).
|
||
- IME evidence:
|
||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/ime-surrounding-gap.md`
|
||
(E21b/P7b gap and custom-editor inventory).
|
||
- Shared suite contract:
|
||
`/home/codex/work/CraftApps/egui037-migration-20261022/craft-libs/docs/egui-037-suite-rollout.md`
|
||
(completion slot, DockArea direction, performance sample counts, stock-0.37 control,
|
||
platform evidence) and `craft-libs/docs/egui-037-migration-plan.md` (§7.7, §7.8, §10).
|
||
- Font fixtures: `egui/docs/egui-037-migration-plan.md` §10.6 (craft-fonts `2481a8a`,
|
||
Noto Sans Arabic/Thai/CJK, BIZ UDPGothic, Noto Sans Hebrew, Noto Color Emoji; exact
|
||
SHA256/licences). No font binaries are added by this plan.
|
||
- Historical diagnostics:
|
||
`/home/codex/work/ssd2/task-evidence/egui037-audit-20261022/{fork,stock}/designcraft.{errors,log,prepare.diff}`.
|
||
- Upstream reports and notes reviewed through the local snapshot
|
||
`/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/upstream-discussions.json`:
|
||
[#8731](https://github.com/emilk/egui/issues/8731),
|
||
[#8736](https://github.com/emilk/egui/issues/8736),
|
||
[#8763](https://github.com/emilk/egui/issues/8763) / PR
|
||
[#8764](https://github.com/emilk/egui/pull/8764) (unmerged),
|
||
[#8748](https://github.com/emilk/egui/pull/8748) (closed, not merged),
|
||
[#8631](https://github.com/emilk/egui/pull/8631),
|
||
[#8535](https://github.com/emilk/egui/pull/8535),
|
||
[#8489](https://github.com/emilk/egui/pull/8489),
|
||
[#8555](https://github.com/emilk/egui/pull/8555),
|
||
[#8583](https://github.com/emilk/egui/pull/8583),
|
||
[#8512](https://github.com/emilk/egui/pull/8512),
|
||
[#8428](https://github.com/emilk/egui/pull/8428),
|
||
[#8657](https://github.com/emilk/egui/pull/8657),
|
||
[#8577](https://github.com/emilk/egui/pull/8577),
|
||
[#8641](https://github.com/emilk/egui/pull/8641).
|
||
|
||
These links identify the behavior and reports inspected. They are not fresh DesignCraft
|
||
validation, and an upstream status may have changed since the snapshot.
|
||
|
||
|
||
## Local implementation update — 2026-10-23
|
||
|
||
The user clarified that reviewed plans remain local and implementation proceeds before publication. The earlier requirement to publish/land this plan first is superseded; pushes and PR publication still require explicit approval.
|
||
|
||
Canonical `storytold/designcraft` `main` now resolves to `2e36dcf909627c8f0e5662fe6b4825b1c1c24420`. The owned checkout was fast-forwarded to this revision. Earlier baseline SHAs and line anchors above are retained as historical review evidence; use this new revision and its exact lockfile for current-main integration and paired measurements. The original refs remain reachable.
|
||
|
||
Preserve browser page zoom and canvas touch-action:none alongside packaging fixes.
|
||
|
||
The complete static delta review and exact old/new object identities are retained in the task evidence: `canonical-diffs-refresh-20261023.json` and `overnight-consumer-review-20261023.md`. This amendment claims no new build, native GUI or performance acceptance.
|
||
|
||
### Local pinned font inputs (2026-10-23)
|
||
|
||
Parent fetched and verified all eight manifest fonts at `storytold/craft-fonts`
|
||
`2481a8a324b986a45954e90045ed1f8f2efaaf3c` into
|
||
`/home/codex/work/CraftApps/egui037-migration-20261022/fixtures/craft-fonts`.
|
||
Use this exact directory as the optional `CRAFT_FONTS_DIR` build input. All
|
||
manifest SHA256 hashes match; licenses retained. This does not waive the
|
||
required no-corpus test configuration. The separately pinned Hebrew fixture
|
||
is at `/home/codex/work/CraftApps/egui037-migration-20261022/fixtures/noto-sans-hebrew/NotoSansHebrew.ttf`
|
||
with adjacent OFL.txt/provenance.json. Keep these fonts out of app source;
|
||
required test fixtures may use an explicit external input with verified hash
|
||
and clear missing-input behavior, never silently disable glyph checks.
|
||
|
||
## Final local landing record — 2026-10-25
|
||
|
||
This section records the current local landing state. Everything above remains historical planning and
|
||
implementation evidence: the older candidate hashes, baseline anchors and "planning only" wording
|
||
describe earlier states and are not the current pins. No commit, index write, ref update, fetch,
|
||
push, pull request, comment, merge, build, fmt, test, Cargo, VM, resource or cleanup action was
|
||
performed while writing this record; the qualification results below were produced separately by
|
||
the parent through the managed `task.py` workflow and are cited as evidence, not rerun here.
|
||
|
||
### Current dependency pins
|
||
|
||
| Item | Revision |
|
||
| --- | --- |
|
||
| egui family (`egui`, `epaint`, `ecolor`, `emath`, `eframe`, `egui-wgpu`, `egui-winit`, `egui_kittest`, `egui_extras`, `epaint_default_fonts`) | `storytold/egui` `6d518fc799681571963842f95b55a6b68431b15a` |
|
||
| winit | `storytold/winit` `a517b3846ee8457d84ee4975110bfda6797c44dc` |
|
||
| craft-libs (`craft-ui`, `craft-print`) | `storytold/craft-libs` `25da1314d443171b4adeaad2cd9454b273f35c6f` |
|
||
| CI craft-fonts | `storytold/craft-fonts` `b076417b5880bd4154bd22f00695b1d28c6bea83` |
|
||
|
||
`Cargo.toml` and `Cargo.lock` resolve one egui family and one winit from these storytold revisions;
|
||
the lock has no registry 0.36.2 egui family and no candidate `aiongg` source. `crates/fonts/build.rs`
|
||
continues to treat craft-fonts as an optional `CRAFT_FONTS_DIR` input, not a manifest dependency.
|
||
|
||
### Exact dirty union
|
||
|
||
| Item | Value |
|
||
| --- | --- |
|
||
| Checkout | `/home/codex/work/CraftApps/egui037-migration-20261022/designcraft-implementation` |
|
||
| Git worktree HEAD (unchanged) | `80d13911b2d20fbb5cd9398696627bb3a715417b` |
|
||
| Earlier integrated main base in the dirty state | `d8d78851729a2c1a10e5bfad7c591f3de78651a0` |
|
||
| Imported new main | `45ed6cfce0f86e59ff66ab18a05c3ec03ccef531` (`refs/migration-observations/20261024-1610-main`) |
|
||
| New-main distance from the earlier base | 141 commits, 127 changed paths |
|
||
| Files in the union | 619 = 613 tracked + 6 untracked |
|
||
| Worktree status | 186 entries = 180 modified + 6 untracked |
|
||
| Merge state | no `MERGE_HEAD`; index empty; no conflict markers; `git diff --check` clean |
|
||
|
||
The six untracked paths are exactly:
|
||
|
||
- `crates/compose/src/placed.rs`
|
||
- `crates/idml/src/tests/facing_ltr.rs`
|
||
- `crates/idml/src/tests/frame_grid.rs`
|
||
- `crates/ui-egui/src/page_history.rs`
|
||
- `crates/ui-egui/src/panels/story.rs`
|
||
- `docs/egui-037-migration-plan.md`
|
||
|
||
The 16:28 reconciliation packet recorded 185 status entries: the 92 pre-existing dirty paths plus the
|
||
127 imported paths with 34 overlaps. The current tree adds one later modified tracked path,
|
||
`apps/designcraft/src/native_menu.rs` (the native-menu/action host-order repair), for 186. Of the 127 imported paths, 103 were adopted whole
|
||
and 24 were reviewed 3-way content merges; later reviewed deviations are the Windows-only test fixtures
|
||
in `apps/designcraft-cli/tests/it/cli.rs` and `crates/engine/src/file_access.rs`, the documented
|
||
`Id::new` → `Id::unique` port in `crates/ui-egui/src/panels/story.rs`, and the host-order changes above.
|
||
|
||
`80d13911` is an ancestor of `45ed6cf`, so an ordinary merge would fast-forward. The eventual manual
|
||
union commit therefore needs both parents, `80d13911b2d20fbb5cd9398696627bb3a715417b` and
|
||
`45ed6cfce0f86e59ff66ab18a05c3ec03ccef531`, to preserve both the migration branch and the imported
|
||
main history. No such commit exists yet.
|
||
|
||
### Review-1643 findings on the current source
|
||
|
||
Both actionable defects from the 16:43 review are fixed in the current source:
|
||
|
||
- **Document generation identity.** `InputGeneration` and `ScreenshotGeometry` now compare
|
||
`DocState::identity_epoch` through `active_document_epoch()` (`crates/ui-egui/src/lib.rs`), so a
|
||
`file.revert` that preserves the UID still invalidates buffered input, native drops and admitted or
|
||
in-flight screenshots. Regression tests cover a buffered input batch, a buffered native drop and
|
||
screenshots before issue and in flight; all pass in the Linux full-CI job below.
|
||
- **NumField disabled presets.** The preset segment now uses `Sense::hover()` and reports
|
||
`self.enabled == false` when the field is disabled, does not open its menu, and dims its chevron;
|
||
`disabled_numeric_presets_are_disabled_and_do_not_open` passes.
|
||
- The earlier `panels/properties.rs` evidence mismatch is also closed: the current file is
|
||
`9515f944ddbbf61bb2099324ac6825fd7603262adc605686c2a932948a81f768`, matching the later
|
||
Windows-qualified manifest and the current worktree.
|
||
|
||
No other real source defect was found in this bounded audit. Residual items below are qualification
|
||
limits, not claimed defects.
|
||
|
||
### Current qualification evidence
|
||
|
||
- **Linux full CI** `egui037-designcraft-final-characterization-ci-20261025-0050`: all 9 steps
|
||
passed (fmt, pdf-deps, clippy, test, i18n-report, doctest, assets, layers, wasm); 77 result
|
||
summaries total **3230 passed, 0 failed, 6 ignored**. The job's captured unstaged patch is
|
||
byte-identical to the current tracked worktree diff, so it qualifies the current tracked source.
|
||
The run includes the corrected canvas characterization test over three batch shapes and the
|
||
discard/no-discard matrix.
|
||
- **Windows qualification** job 102: all 9 steps passed, **3198 passed across 77 result summaries**
|
||
on the 619-path source manifest `5208ddb75ad8d06a48c334b48e5e80dc193ae2b9739b119dd50942aa3ebd5a40`.
|
||
Exactly two test-only Windows fixture paths differ from the applied prior manifest
|
||
(`apps/designcraft-cli/tests/it/cli.rs`, `crates/engine/src/file_access.rs`); the engine production
|
||
prefix is unchanged. Qualified desktop executable
|
||
`a34478f09ccae2571320b3ada489665adae450031ca24918ec4e8b165edfc47a`; qualified CLI
|
||
`318ec6cb7521f941aaab01a63b2c0f970bd70bfb50e5545aa645f63d1589cbee`.
|
||
- **Native Windows GUI** `native-gui-2238`: edit, undo, redo, selection, save and reopen were captured
|
||
with document, UI and window evidence; the native input diagnostic ordered three production
|
||
click/End replays on the qualified executable, each reaching click caret 6 and End caret 12, with
|
||
all three text strings unchanged and the document clean. Six screenshots were reviewed, but the state JSON is the exact caret
|
||
oracle because a blinking caret can make two screenshots share pixels.
|
||
- **Native input diagnostic** `native-order-review0048.txt`: no application event loss is shown. The
|
||
trace records the End press/release pair arriving before the click pair; the app acted correctly
|
||
for the order it received. The temporary diagnostic build was fully restored to the exact 619-file
|
||
qualified source and binaries in the VM before the ordered production replays.
|
||
- **Exact-source boundary.** The Windows 619-path manifest predates the host-only canvas
|
||
characterization test. In the current tree, `crates/ui-egui/src/canvas.rs` differs from that
|
||
manifest only inside `mod tests` (the first difference starts at line 6016; the test module starts
|
||
at line 4046), so production bytes are unchanged. The Windows run did not execute that new test;
|
||
Linux full CI did. This document is amended after the Windows manifest, so the documentation file
|
||
is also not byte-identical to that earlier snapshot.
|
||
|
||
### Remaining limits and regression risk
|
||
|
||
The union includes 141 upstream main commits and 127 changed paths, only part of which is egui 0.37
|
||
API work. It also imports page history/Go Back, the Story panel, notes and table work, optical-margin
|
||
options, linked-file read caps, SVG preflight, IDML facing/frame-grid tests, and other current-main
|
||
features. Review the 180-file tracked diff and the 24 manual overlap resolutions as product changes,
|
||
not as a mechanical dependency bump; the imported tests reduce but do not remove that risk.
|
||
|
||
Still unqualified for this exact union: macOS native menus/IME/Apple events, real-GPU rendering and
|
||
the DirectX 12 startup row, native Linux X11/Wayland presentation, native Windows IME and clipboard/
|
||
PRIMARY selection, physical-device input ordering, browser presentation beyond the wasm check,
|
||
paired performance against the thresholds in §7, and packaging/release artifacts. The Windows VM
|
||
uses a software renderer. Cross-device QMP command order is not a guest-delivery guarantee; the
|
||
ordered replay gates input on an observed click state rather than zero-delay host issuance.
|