110 KiB
PhotoCraft: egui 0.37 migration plan
Status update (2026-10-25): local implementation includes main b278ff72459cbf9754863365edd822ba2248aa07, preserving its JPEG dependency, Affinity import and raster changes. All nine CI stages passed: 11,590 passing test executions, zero failed, 91 ignored across 93 summaries; the ignored panic hunt, strict lint and WASM compilation passed. The current-main integration received independent review. Native/browser GUI, native IME, PRIMARY, packaged WASM size, real-world 0.36 settings 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.
Current status — 2026-10-25: the port is implemented and reconciled with main
486afd82cce99255e999224bcfe28de00fd766aa. The full managed CI run passed all nine stages:
formatting, strict clippy, layers, scorecard, workspace tests, the ignored panic hunt,
doctests, WASM compilation and the translation report. It recorded 11,580 passed and
91 ignored test executions across 93 summaries, with zero failures. These totals include
overlapping test invocations and are not a count of unique tests.
The production graph uses canonical GitHub sources: egui 6d518fc799681571963842f95b55a6b68431b15a,
winit a517b3846ee8457d84ee4975110bfda6797c44dc, and shared UI/print
25da1314d443171b4adeaad2cd9454b273f35c6f. The checked-in Cargo-generated lock has SHA256
714ce3bc78f32bc35bd69e27c4a785ac2f38983c77df9896dcddc8e5fa79ccb2; no absolute path
patch is required. The lifecycle/capture port, Type-tool surrounding-text and byte-deletion
adapter, reset acknowledgement handling, and selection-action changes are implemented.
The research and progress entries below retain their original sequence. Their statements
about missing APIs, private path patches, unresolved pins and absent build results are historical
and superseded by this status. Native/browser GUI and IME acceptance, PRIMARY behavior, packaged
WASM size, broad visual/accessibility sweeps and paired app performance remain open. The synthetic
settings fixtures qualify migration rules, not a captured real-world 0.36 save. Human review
and human merge are required.
Revision 2 (2026-10-23): resolves the independent review at
plan-reviews/photocraft-v1.md, incorporates the shared E21b/W15P contracts and the
lifecycle source audit, and removes the superseded sampling, PRIMARY, screenshot and
rollback wording. The reviewed v1 document hash is retained in the evidence ledger;
this revision needs its own independent review before publication.
Baseline: ff4738810134d7f7e68b49f2e4f231b686282ff3 ("Release 0.14.0", 2026-10-22).
Cargo.lock baseline: sha256 36a1de83fea16781dc7186d33d8064cb5dfa878a2a0847228737640b1e64e2d0,
git blob 5c96f56119b6ddb0feb2da9cff3e23232f2dc63c.
Target: egui/eframe/epaint family 0.37.0, not upstream main. Upstream tag object
418687fa18fdac8a9b1b6c8f4a2eb1afd2df4c47 points at commit
841a328ba2dd8f6996beb7189ebb7fc00e668a2a. The audited Craft fork candidate is
aiongg/egui craft/0.37 at a4f9a17f64a6bae61350fb85804c9df0d9865916; the audited
winit candidate is aiongg/winit craft/0.30.13-lang at
8b35388bd719e8cc86a95971ba0bc026c9a8f058, based on storytold/winit
craft/0.30.13 ffae5198770d7730e718043f035a772b7dbc3b1b.
Those revisions are historical, read-only evidence. They are not accepted revisions and are not publication authority. Every implementation claim below must be re-run against the final published revisions.
Research basis: upstream tag 0.37.0 source and changelogs in the read-only
checkout /home/codex/work/CraftApps/egui037-migration-20261022/egui, the
candidate fork commits and PATCHES.md, and the PhotoCraft source at the
baseline SHA. No web research was needed for the API facts below; upstream
release notes and source are the primary evidence. Historical fork/audit notes
were used only to locate refs, not as acceptance evidence.
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. Decision summary
- Move directly to 0.37.0; do not port 0.36.2 patches to a new 0.36.x branch.
- Keep the existing
App::logic/App::uisplit. PhotoCraft already uses it (crates/ui-egui/src/lib.rs:2244and:2371), so there is no blanketContext-bridge migration. The work is preserving the split across hidden windows, screenshots, control input and multiple passes. - Pin the whole egui family and winit by immutable git revisions. A consumer's
[patch]section does not propagate to dependency manifests, so the root workspace must patch every family crate it resolves. - Keep the existing
eframeopt-out from default features. Add only the features PhotoCraft actually needs:wgpu,accesskit,wayland,x11,persistence, and the newclipboard. Addmonochrome_emoji_fontsto theepaint_default_fontsbuild-dependency. Do not enablesystem_fontsin the first cut; the existingcjk_fontssystem-provider path is measured separately (#8512). - Reproduce 0.37's
egui-iconsface and family order. The current build script fails at once becauseNOTO_EMOJI_REGULARandEMOJI_ICONare now feature-gated (crates/ui-egui/build.rs:42,:44), andEGUI_ICONSis unconditional. The glyph, accessible-name and feature fixes are part of the first green PR, not a later fixture sweep. - Migrate
WidgetTypetoaccesskit::Role(the old name is a deprecated alias; several variants were renamed),Id::newtoId::unique,Ui::idtoUi::scope_id, panel IDs toAsIdSalt, screenshots to a boundedScreenshotCallbackcompletion slot, and removeUserData/Event::Screenshot.Ui::scope_idis the renamed oldUi::id;Ui::unique_idis a new globally unique but unstable id and must never be used for persisted state. - Migrate two persisted schemas deliberately, in the first compatible PR:
egui::PanelStateIDs andegui_winit::WindowSettings.PanelStategains a parent-scoped id;WindowSettingschanges from flat fields tonormal_geometry. The panel migration is additive (the old key stays), but the window rewrite is one-way. Preserve panel widths and window size/fullscreen/ maximized state explicitly; document the rollback loss of window position. - Adopt the language fork as a second, separately reviewed stage. E5/E12/E13/E16/
E17/E20 change text layout and AccessKit semantics;
bidi_text.rsmust not pre-reorder text once E5 is active. This stage also adopts E21b:IMEOutputgains a bounded logical surrounding-text payload, and the Type tool supplies a real model adapter. E21b is new work, not existing E21/W27 capability. - Treat E11 as optional. PhotoCraft does not enable
epaint/multithreading; the patch's forced single-threaded glyph context is a suite fix for Servo/VectorCraft, not a PhotoCraft requirement. - No PR may contain an absolute
pathpatch. Final production manifests pin the reviewed, publishedstorytoldfull 40-hex SHAs for the whole family and winit, including E21b and W15P. Stock 0.37.0 and the aiongg candidate revisions are research controls, never staged release pins. - Every performance comparison is paired on one machine. The committed
perf/baseline.jsonis a macOS M4 Pro measurement and must not be used as a cross-machine verdict for this Linux/Windows work. - Both native and web run
App::logicandApp::uiinsideContext::run_ui, once per layout pass. Logic-only ticks leave events and time stale, and a completedrun_uiresetscurrent_pass_indexto zero. Fresh input is interpreted only in a known UI phase, with a forced hidden paint when the host is hidden. Background timers use monotonic clocks, notctx.input(|i| i.time), and every queue is consumed exactly once. - The before/after harness exists on the 0.36.2 baseline before any production edit. The baseline PR adds the text-tessellation, renderer-construction, selection-action and screenshot-lifecycle probes and records the paired baseline; it does not change production behavior.
2. Dependency model and publication
2.1 Current resolved family
Cargo.toml:55-59 pins eframe, egui, egui_kittest at 0.36;
crates/ui-egui/Cargo.toml:33 pins egui_extras 0.36;
crates/ui-egui/Cargo.toml:73 pins epaint_default_fonts 0.36;
winit is =0.30.13 (apps/photocraft/Cargo.toml:86) and the lock resolves
egui-family 0.36.2, winit 0.30.13, wgpu 30.0.1, accesskit 0.24.1.
craft-ui is pinned to 5c874d51eeb3de70e735c8ce87a59a7dedd47675,
craft-print to 3d37da389a3618ab9b76109da11416e9ca4ea719, and
craft-docpath to 82a24b006eefa036b43c09a67fb9a3fbd17ffe63
(Cargo.toml:13, :42-43).
2.2 Target family
Target the whole compatible set together:
| Package | Target | Source |
|---|---|---|
egui, eframe, egui-winit, egui-wgpu, egui_glow, egui_extras, egui_kittest, epaint, emath, ecolor, epaint_default_fonts |
0.37.0 | one reviewed, published storytold/egui full 40-hex final P8 revision containing E1-E21, P2b/P7/P7b/E21b and P7c |
winit |
0.30.13 | one reviewed, published storytold/winit full 40-hex revision containing W1-W27 and W15P |
wgpu |
30.0.1 | registry (unchanged; egui-wgpu 0.37 is built for wgpu 30) |
accesskit / accesskit_winit |
0.25.1 / 0.34.1 | registry, pulled by egui 0.37 |
The root workspace needs a [patch.crates-io] entry for each of those egui-family
packages and winit; patching only egui splits the graph. The same revision for all
egui crates is required because the fork's workspace crates depend on each other by
path. epaint_default_fonts is a build-dependency and must be patched too.
egui_extras must be version = "0.37"; do not assume crate version numbers match
the release name (egui_plot, if ever added, is independently versioned).
The candidate fork currently uses a mutable branch:
aiongg/egui commit 137398e6 sets winit = { git = ".../aiongg/winit", branch = "craft/0.30.13-lang" }. The plan requires replacing that with an exact rev. A branch
pin is not reproducible and can change between two CI runs.
Pin identities are deliberately split:
- production pins: reviewed, published
storytold/eguiandstorytold/winitfull SHAs, reachable from the public URLs, carrying every selected E/W patch, E21b and W15P, withPATCHES.mdand removal conditions; - research controls: upstream
0.37.0(841a328b...), upstream0.36.2(49682f8b...), aiongga4f9a17f.../8b35388b...and any stock-only experiment used to attribute #8731/#8736. These are benchmark controls and are never a staged release, a consumer pin or a rollback baseline.
2.3 Targets and feature matrix
| Target | Package / binary | egui work |
|---|---|---|
| Desktop app | photocraft binary (apps/photocraft) |
eframe 0.37 native, wgpu, accesskit, wayland, x11, persistence, clipboard; no default_fonts, no system_fonts in the first cut |
| Browser app | photocraft-web binary (apps/photocraft-web) |
eframe web 0.37, wgpu (WebGPU + WebGL2), accesskit; browser glyph rasterizer; web_file |
| Browser bootstrap | photocraft-web-boot binary |
no egui dependency; only wasm-bindgen/web-sys; keep out of the UI changes |
| Headless CLI | photocraft-cli |
no egui dependency; control bridge/MCP only |
| UI library | photocraft-ui-egui |
egui 0.37, eframe 0.37 (wgpu + accesskit + clipboard), egui_extras 0.37 (svg), craft-ui at the reviewed PR 199 port |
| Tests/benches | crates/ui-egui/tests/it, examples/*, photocraft-testkit |
kittest 0.37 (wgpu, snapshot, eframe), snapshot, perf_scenarios, interactive_bench, fx_bench, bevel_bench, camera_raw_bench, type_bench, large_image_bench, large_file_bench |
| Platform helper | photocraft-tablet |
no egui dependency; keep its winit-independent X11/AppKit/Windows path; E1 is only an enabler for a future native Wayland pen |
apps/photocraft is the only target with the direct Linux winit dependency
(=0.30.13, x11 feature, for EventLoopBuilderExtX11). It must resolve to the
same patched winit revision as eframe. apps/photocraft-web must not add a
native winit dependency.
2.4 Manifest and workspace audit
- The root workspace is
resolver = "3", edition 2024, MSRV 1.95; members arecrates/*,apps/*,xtask; default members include the desktop app, CLI and xtask (Cargo.toml:1-6). crates/*/fuzz/Cargo.tomlare independent fuzz workspaces. None depends on egui, eframe, winit, epaint or craft-ui; no change is expected. Verify withrgafter the bump.examples/plugins/invert-rs/Cargo.tomlis a separate[workspace]for a wasm plugin and has no UI dependency.apps/photocraft-web-bootandapps/photocraft-clihave no egui dependency.- There is no
vendor/directory,[source]replacement, or machine-local.cargo/config.tomlpatch in this checkout. The only.cargo/config.tomldefines thextaskalias. Do not introduce a vendor tree for this migration. [profile.wasm-release.package.egui|epaint|eframe]entries remain valid because the package names do not change; re-check wasm size after the bump.
2.5 Publication and rollback
Publication gates, in order:
- Canonical public
storytold/eguicraft/0.37revision, one commit per E-patch (including E21b), withPATCHES.md, tests and removal conditions. - Canonical public
storytold/winitcraft/0.30.13revision, one commit per W-patch (including W15P), withPATCHES.md, tests and removal conditions. storytold/craft-libsrevision onmaincontaining only the reviewed port hunks (8614e4a/1764bca) applied to currentmain. Do not pin a PR ref or merge the staleb68de8c/ec8840atrees.- PhotoCraft PRs below, each with a
Cargo.lockgenerated by--lockedresolution from the public revisions.
Rollback is the previous coherent pair: old Cargo.lock plus the 0.36.2 registry
family and winit 0.30.13. Keep that exact lockfile under evidence before the first
bump. A revert of the dependency commit restores the source graph.
Persistence is not symmetric. The PanelState migration is additive: the old
Id::unique(name) key remains in ui.ron and 0.36.2 continues to read it. The
WindowSettings rewrite is one-way: 0.36.2 cannot deserialize the nested
normal_geometry value and opens with default geometry after a rollback. The first
0.37 run therefore keeps a pre-migration ui.ron.pre-0.37 backup, and the plan
documents the window-geometry/position loss rather than calling both migrations
additive. Add a reverse-compatibility test with a 0.36-shaped settings struct and
fixtures for old macOS inner_position_pixels, old outer position, maximized and
fullscreen.
Reachability checks before publication:
git ls-remotereturns the exact revision from the public URL.cargo tree -dshows one egui, one eframe, one winit.rg 'path\s*=\s*"/' Cargo.tomlis empty.- Every
[patch]URL ishttps://github.com/storytold/...withrev = "<40 hex>".
3. What the source actually does
3.1 Native entry point
apps/photocraft/src/main.rs:720 calls eframe::run_native. The app creator
installs fonts/theme through PhotocraftApp::setup_context
(crates/ui-egui/src/lib.rs:2850), attaches the GPU canvas, control server,
native menu, single-instance service and platform services, then returns
PhotocraftApp.
native_options (apps/photocraft/src/main.rs:104) sets a custom viewport,
persistence_path ui.ron, and a window_builder hook restored_window
(:132) that enforces minimum size and disables restored fullscreen.
options.centered is decided from ui_state::has_saved_position
(apps/photocraft/src/main.rs:667, apps/photocraft/src/ui_state.rs:33).
gpu_startup configures options.wgpu_options; presentation applies present
mode and frame-latency overrides. eframe::Error::Wgpu(_) is the signal for the
CPU-retry path (apps/photocraft/src/main.rs:934,
apps/photocraft/src/gpu_startup.rs:477). E2/E3 add WgpuError::ConfigureSurface
and CreateRenderer; the existing Error::Wgpu(_) match keeps working, but the
retry decision must be tested with the new variants.
3.2 Web entry point
apps/photocraft-web/src/web.rs:47 starts eframe::WebRunner. WebShell
implements raw_input_hook, logic and ui (:325, :332, :362); its
logic drains dropped files, serves fonts, calls the app's logic, and saves
presets. Its ui adds a browser preset-storage banner and calls the app's ui.
apps/photocraft-web/index.html is Trunk-based and the wasm is gzipped and split
into parts by packaging/web/package.sh. Its 24 MiB limit applies to each shipped
file, not to the raw wasm; the boot module inflates the parts. 0.37 adds
send_wrapper and changes epaint/vello_cpu; the raw wasm and the gzipped total
must both be measured against baseline, not assumed to fit a raw-wasm cap.
docs/development.md:459 is the reference.
3.3 Logic versus UI, and the pass model
The pass model is the one verified in
/home/codex/work/ssd2/task-evidence/egui037-migration-20261022/lifecycle-source-audit.md
against 0.37.0:
- Native (
crates/eframe/src/native/epi_integration.rs::update) and web (crates/eframe/src/web/app_runner.rs) both callApp::logicandApp::uiinsideContext::run_ui. Each can run once per layout pass, andrequest_discardadds passes. Logic is not once per rendered frame. - A hidden/occluded window may be ticked through
update_logic_only->Context::run_logic. That path still callsprepare_raw_inputandApp::raw_input_hook, but does not begin a pass:ctx.input().events,time,screen_rectand viewport geometry remain from the previous UI pass.ViewportCommand::RequestPaintWhileHiddencan force a real UI pass whilevisible()is false, so visibility does not identify a logic-only callback. - After a normally completed
run_ui,current_pass_index()is zero becauseend_passtakes the output. Do not build a first-pass guard on a stale nonzero index. A guard may protect genuinely once-per-layout-frame work only with a regression proving normal hidden calls still run; it must not suppress fresh async results, control commands or widget actions.
Consequences for PhotoCraft:
PhotocraftApp::logic (crates/ui-egui/src/lib.rs:2244) does stateful work that
must survive a hidden window: frame accounting, sync_views, GPU checks, control
drain, native menu, commit/end operations, screenshot issue/collect, preferences
tick, monitor polling, i18n sync, work-area fit, close guard, job tick, shortcuts,
file drops, file dialogs. Because it runs per pass, every item must consume its
queue once, use a revision/generation or monotonic deadline, and tolerate a
second pass with no new input. self.frame += 1 (:2257) is a logic-pass count,
not a rendered-frame identity; screenshot settling and GPU startup checks must use
ctx.cumulative_frame_nr() (or an equivalent UI-phase rendered-frame counter).
PhotocraftApp::ui (:2371) draws panels, windows, dialogs, notices and the
central canvas, and does UI-local persistence (brush_picker::persist,
last_brush::persist, tool_memory::persist, view_cmds::persist) plus
native_menu::sync, gpu_status::check_window, and the current synthetic-input
wakeup. Re-running a sizing pass must not duplicate persistence, commands or
notices; move work that must happen with no paint to logic, and make the rest
idempotent.
raw_input_hook (:2338) normalizes synthetic input, clipboard key events,
legacy pinch, pen-hold, titlebar resize and stroke tracing. It runs for the root
and deferred viewports, not immediate viewports (unless E1 is installed), and it
also runs before a hidden run_logic. take_synthetic_step (:2356) removes
events from the app queue even on a logic-only tick; eframe preserves the events it
appends in pending_raw_input, but PhotoCraft must set a synthetic_in_flight
flag in the hook and send RequestPaintWhileHidden from logic (which runs
after the hook in the same tick) so a UI pass consumes them. Do not start stroke
tracing or interpret fresh input as if a paint occurred: either gate it on that
flag or move the start to App::ui. egui-winit turns
Cmd/Ctrl+C/X/V into Event::Copy/Cut/Paste; PhotoCraft maps those back to key
presses in shortcuts::clipboard_keys (crates/ui-egui/src/shortcuts.rs:282).
3.4 Panels, docking and the root Ui
The root Ui is passed to App::ui; panels are shown on it:
panels::title_bar (crates/ui-egui/src/panels.rs:592),
options_bar (:969), status_bar (:1726), toolbar (:93),
right_dock (:1792), and dock_panels (:1866). The custom docking surface
uses craft_ui::docking::DockArea with the stable ID
photocraft-panel-docking (crates/ui-egui/src/panel_docking.rs:424) and an
egui::Panel::right("shared-panel-dock") (:448).
craft_ui is a real runtime dependency (panel_docking.rs:4-5,
tab_strip.rs:11), not a stale manifest entry. craft_ui's docking and tab
components are in the 0.37 API blast radius; PR 199 is required.
3.5 Fonts and text
theme::install_fonts (crates/ui-egui/src/theme.rs:847) builds a
FontDefinitions from egui_fonts::definitions, prepends Inter/JetBrains Mono,
adds named medium/semibold families, scales faces with UiFontSizePlugin
(:946), calls ctx.set_fonts, then installs the lazy CJK/script fallback
(crates/ui-egui/src/cjk_fonts.rs:352, :375). The fallback is app-specific:
script order follows the UI locale, reads at most one system font per frame, and
tracks its own tried/loaded state. It reads system fonts even when eframe's
system_fonts feature is off, so #8512's discovery cost is still on PhotoCraft's
startup/first-use path and must be measured separately.
cjk_fonts::add_to_all_families reads FontData.font as bytes
(cjk_fonts.rs:354, :355, :358) and a test compares the pointer
(:879). In 0.37 FontData.font becomes
Arc<dyn AsRef<[u8]> + Send + Sync> (epaint::text::FontData), so those four
uses do not type-check as written.
3.6 Screenshots and control
control.rs:1265 handles ui.screenshot; PhotocraftApp::drain_control
(lib.rs:2156) queues (token, earliest_time, frames) in
queued_screenshots; issue_screenshots (:2183) sends
ViewportCommand::Screenshot(UserData::new(token)); collect_screenshots
(:2196) reads Event::Screenshot { user_data, image } and calls
control::save_screenshot (control.rs:1410). This is exactly the API 0.37
removes: ViewportCommand::Screenshot now takes a ScreenshotCallback, and
Event::Screenshot / UserData no longer exist. The current records are removed
only by a matching callback, so a zero-sized surface, hidden viewport, device
loss or dropped capture leaves the reply pending until the 60 s deadline and
retains its image path. Section 9.3 replaces that with a bounded, cancelable
slot and an explicit timeout drain.
3.7 Persistence
ui.ron contains two independently versioned payloads:
- eframe window geometry, restored by
egui_winit::WindowSettings; - egui memory, including
PanelStateentries and persisted widget data.
apps/photocraft/src/ui_state.rs sanitizes the file before eframe reads it and
decides whether a saved position exists. saved_position is platform-dependent:
macOS looks for inner_position_pixels, everywhere else for
outer_position_pixels (ui_state.rs:41-44). 0.37 replaces the flat
WindowSettings with normal_geometry: WindowGeometry containing
outer_position_pixels, scale_factor and inner_size_points. The old value
does not deserialize, eframe ignores it, and a top-level probe becomes a false
positive: the window opens uncentered at the OS default. Section 9.2 specifies the
explicit pre-load rewrite and the new platform-independent position key.
PanelState is keyed by an Id. In 0.36 Panel::left("dock") used
Id::new("dock"); in 0.37 the panel ID is
parent_ui.scope_id().with_salt(IdSalt::new("dock")) (Id::with, not
Id::with_salt + an AsIdSalt). For the root Ui that is
ctx.viewport_id().root_ui_id().with(salt). Existing saved panel widths are under
the old IDs and will not be restored unless migrated. Tests that read
PanelState::load(&ctx, Id::unique("dock")) also break
(apps/photocraft/src/main.rs:1004, crates/ui-egui/src/panels.rs:6604,
:6934, crates/ui-egui/src/workspace_ui.rs:594,
crates/ui-egui/src/panel_docking_tests.rs:569). Id::unique(name) has the same
hash as the old Id::new(name), so the old key can still be read.
3.8 Tests and performance infrastructure
The repository already has the right harnesses:
crates/ui-egui/examples/snapshot.rsrenders the full UI offscreen withegui_kittest+ wgpu, deterministicRendererOptions, scripts, themes and scales.crates/ui-egui/src/panel_docking_tests.rs:179captures theme/width/scale docking fixtures whenCRAFT_UI_DOCKING_FIXTURESis set.crates/ui-egui/src/panels.rs:6904captures compact chrome fixtures underCRAFT_UI_VISUAL_FIXTURES.perf/budgets.tomlmaps P1…Pn rows toperf_scenarios,interactive_bench,fx_bench,bevel_bench,camera_raw_bench,type_bench,large_image_bench,large_file_bench, plus the newtext_tessellationrow;xtask perfruns them in release and writes p50/p95 (and p99 for frame rows with enough samples), peak RSS, GPU bytes and load averages.crates/ui-egui/tests/ithas wgpu kittest tests for canvas, GPU loss, color management, exports and workspace presets.apps/photocraft/tests/mac_menu_appkit.rsis the existing real-macOS menu check; it is not a substitute for IME/focus testing.
There are 481 Harness::builder() uses (source count at
ff473881). They will inherit 0.37's check_accessibility: true and
missing_glyph_policy: Panic (egui_kittest/src/builder.rs:50-51). Both defaults
are good, but they turn latent UI defects into test failures. The minimal
accessible-name, glyph-policy and feature fixes therefore belong in the first
green PR; do not disable either check globally and do not defer them to a later
fixture sweep.
3.9 Multiple viewports
PhotoCraft creates extra document windows with
ctx.show_viewport_immediate(ViewportId::from_hash_of(("docwin", w.id)), ...)
(crates/ui-egui/src/canvas.rs:6551). These are immediate viewports, not deferred
ones. The implications are:
App::raw_input_hookdoes not run for immediate viewports; E1'sViewportHooks::raw_inputis the only per-viewport hook that does. PhotoCraft has no consumer today, but the extra-window path must be tested with E1 present and absent.- The root-only services (
tool_cursor,monitor_status,caps_lock, screenshot capture, control server) correctly guard onViewportId::ROOT(tool_cursor.rs:103,monitor_status.rs:274); do not accidentally make them viewport-global. - #8748 (hidden-parent deferred children) is closed and unmerged, so it is not
behavior in 0.37.0. PhotoCraft has no
show_viewport_deferredcall sites (canvas.rs:6551,screen_picker.rs:296,camera_raw_ui.rs:1224are allshow_viewport_immediate), so the upstream deferred-child bug is not directly applicable. Still test the hidden/occluded root plus an immediate child's open/update/close, because that is the path PhotoCraft actually uses. ui.screenshotis documented as main-window only (docs/control-protocol.md); do not promise child-window screenshots in 0.37.- Extra windows use the same
PhotocraftAppstate; a pass for a child viewport may callcanvas_viewwhile the root UI is not being drawn. Keep document mutation and undo transactions insidelogicor the existing single-window paths; test open/close of an extra window during a stroke, a dialog and a screenshot request.
3.10 Platform cfg inventory
The migration can compile on one platform and still break another; these cfg boundaries need an explicit check:
- Native-only:
sys-locale,rayon,winsafe,xcap,x11rb,wl-clipboard-rs,zbus,muda,dispatch2,fmv-macos-events,core-graphics2inapps/photocraftandcrates/ui-egui. - macOS:
apple_events,mac_menu,mac_window,muda,dispatch2,objc2*,photocraft-tablet::macos,tests/mac_menu_appkit.rs. - Windows:
window_frame,cursor,photocraft-tablet::windows,photocraft-ipp,winsafe. - Linux:
portal_save,print_portal,icon_theme,tablet::spawn_x11,linux_libs,winitx11 event-loop builder. - wasm32:
apps/photocraft-web/src/web.rs,cjk_fonts::Sources::system(no system fonts),file_ui/web_preview.rs,gpu_canvasweb adapter limits,photocraft-web-boot.
At minimum run cargo check --target wasm32-unknown-unknown (cargo xtask wasm), Linux native, Windows native and macOS native for the changed crates.
Do not treat a Linux-only compile as platform acceptance.
4. 0.37 breaking changes mapped to PhotoCraft
| Change | PhotoCraft impact and exact work | Count / anchor |
|---|---|---|
WidgetType is now a deprecated alias of accesskit::Role; many variants were renamed (SelectableLabel→Button, Checkbox→CheckBox, ColorButton→ColorWell, CollapsingHeader→DisclosureTriangle, Other→Unknown) |
Mechanical mapping. Button→Button, SelectableLabel→Button, CheckBox→CheckBox, RadioButton→RadioButton, Label→Label, Image→Image, CollapsingHeader→DisclosureTriangle, ComboBox→ComboBox, ColorButton→ColorWell, Slider→Slider, Other→Unknown. Keep selection in WidgetInfo::selected. |
114 uses in 43 files, e.g. shape_stroke_ui/editor.rs:47, props_layout.rs:73; PR 199's mapping is the reference |
Id::new deprecated |
Id::new → Id::unique (same hash; persisted IDs stay stable). |
394 uses in 121 files |
Ui::id deprecated |
ui.id() → ui.scope_id() (a rename of the old id). Do not substitute Ui::unique_id: that is a new globally unique but unstable id used for auto ids and AccessKit identity. |
93 uses |
Panel constructors take AsIdSalt; IDs become parent-scoped |
direction.rs:71/76 currently takes impl Into<egui::Id> and passes it to Panel::left; change to impl egui::AsIdSalt. Add panels::panel_state_id(ctx, salt) = ctx.viewport_id().root_ui_id().with(salt) (Id::with, because with_salt takes an IdSalt). Migrate old PanelState keys in the first compatible PR and keep the old key readable. |
11 direct panel constructors; 5 PanelState::load sites |
UiBuilder::id → scope_id |
No PhotoCraft call sites; craft_ui PR 199 has two. |
craft_ui/docking/surface.rs |
TextEdit::hint_text requires IntoAtoms<'static> |
widgets::text_field/text_field_ex take hint: &str and pass it directly; crop_size::text_field does too. Convert to owned atoms once (hint.to_owned()) or change the helper parameter to impl IntoAtoms<'static> and update callers. |
widgets.rs:848, crop_size.rs:282 |
FontData.font is a Blob (Arc<dyn AsRef<[u8]>>) |
Use data.bytes() or data.font.as_ref().as_ref(); update pointer test. |
cjk_fonts.rs:354, :355, :358, :879 |
ViewportCommand::Screenshot callback; Event::Screenshot/UserData removed |
Replace issue_screenshots with a bounded, cancelable per-request completion slot (section 9.3), not an unbounded mpsc channel. The callback holds only a weak slot reference, stores only the image, and may run on an arbitrary callback thread. Collect in logic; use rendered-frame identity plus a monotonic settle deadline; keep token correlation. clipping_indicator_tests.rs:94/:99 also use the removed API and must be migrated in the same PR. |
lib.rs:2183, :2196, :2214; control.rs:1265; clipping_indicator_tests.rs:94, :99 |
Event::PasteImage new |
egui-winit now emits image paste when the clipboard has no text. PhotoCraft's clipboard_press only maps Event::Paste(_). Adopt it as the document paste shortcut through the existing import_os_clipboard/session path, never a second clipboard read; a test asserts one import and no text-paste double handling. |
shortcuts.rs:221, :282 |
Event::MiddleClickPaste new |
Text fields handle it; verify PhotoCraft's canvas/field interaction does not double-handle it. | shortcuts::clipboard_keys |
ScrollAreaState.offset field private; offset() deprecated |
Replace reads with clamped_offset()/unclamped_offset(). Replace the direct write st.offset.y = target with a request passed to ScrollArea::vertical_scroll_offset on the next frame, then clear the request. |
menu_nav.rs:504, :535; field_wheel.rs:173-174 |
egui-winit clipboard feature is no longer implicit in eframe when default features are off |
Add clipboard to the eframe feature list in the app and ui-egui. Without it, OS copy/paste silently falls back to an in-app-only clipboard. |
apps/photocraft/Cargo.toml:32, crates/ui-egui/Cargo.toml:34 |
system_font_fallback defaults true |
Workspace eframe has default features off, so the provider is only the "missing system fonts" warning. Set NativeOptions::system_font_fallback = false explicitly to preserve the existing CJK plugin and deterministic rendering; evaluate system_fonts separately. |
main.rs:104 |
default_fonts is no longer default; emoji fonts are feature-gated; egui-icons is new |
Add monochrome_emoji_fonts to the epaint_default_fonts build-dependency; deflate EGUI_ICONS; rebuild the family stack exactly as 0.37 does (Ubuntu-Light, egui-icons, NotoEmoji, emoji-icon-font; Hack first for monospace). Update the equality test. No new font file is committed; verify ATTRIBUTION.md needs no row, and add one in the same PR if a packaged asset is added. |
build.rs:37-45, egui_fonts.rs:47-56, :79 |
Ui::id/Id semantics in tests |
kittest queries by IDs still work after Id::unique; panel-state queries do not. Ui::unique_id is unstable and must not become a persisted/test key. |
see persistence above |
AccessKit role/label checks in kittest |
Add Response::labelled_by/accessible_name where a field has no hint; keep check_accessibility on. These minimal name fixes are in the first green PR because the default check runs on every harness. |
all 481 harnesses |
MissingGlyphPolicy::Panic in kittest |
Audit special characters (✓, ▲▼, ▶◀, ×, …, ⏵, ⋮, egui::special_emojis) and add a deterministic fallback or allow_missing_glyphs() only where tofu is intentional. Enable monochrome_emoji_fonts/EGUI_ICONS and add the glyph-coverage test in the first green PR. |
481 harnesses |
#8763 TextEdit click-away selection loss |
Reproduce select-text-then-click-toolbar/context-action. The fork owns the selected fix (with pointer and keyboard regressions) or a tested app workaround preserving range and undo; do not silently accept. | widgets.rs, text_field_menu.rs, new kittest/native row |
egui 0.37 style defaults (roomier buttons, larger radii) |
PhotoCraft overrides most Visuals/Spacing fields in theme::apply, but atom-based Button/ComboBox and popup/table internals still change. Pixel fixtures must be regenerated and reviewed, not blindly accepted. |
theme.rs:995 |
ScrollArea kinetic/rubber-band defaults |
New ScrollStyle::kinetic; PhotoCraft's thin scrollbars keep their explicit opacity, but menu wheel/arrow scrolling and canvas pan must be re-tested. |
theme.rs:1065, menu_nav.rs |
5. E1–E21 and E21b audit
Base: 841a328b. The candidate branch is a4f9a17f, with one commit per topic
and PATCHES.md at that revision. All rows need a final audit against the
published commits; the notes below state what PhotoCraft specifically needs.
| # | Change | PhotoCraft relevance and decision | Required evidence / removal |
|---|---|---|---|
| E1 | eframe NativeOptions::viewport_hooks (per-viewport window create/destroy and raw input, immediate viewports included) |
PhotoCraft does not call it today. Keep in the shared fork because craft-tablet's native Wayland pen path needs it; do not claim pen support until craft-tablet consumes it. Verify the global hook storage does not leak between contexts/tests. | Fork unit test for install/replace; PhotoCraft full-suite run; remove when upstream offers equivalent per-viewport hooks or native Wayland pen is solved another way |
| E2+E3 | egui-wgpu surface fit to max_texture_dimension_2d; configure/renderer errors returned as WgpuError::ConfigureSurface / CreateRenderer |
Directly relevant to multi-monitor/large restored windows and --safe-gpu fallback. Adopt. Add a test with a synthetic surface size above the adapter limit and a forced configure error; assert no panic and a recoverable error. |
surface_fit unit tests; PhotoCraft large-window test; remove when upstream closes emilk/egui#8361 and ships an equivalent |
| E4 | egui-winit uses winit's Wayland clipboard when available; smithay not started; PRIMARY currently falls back to arboard | Regular clipboard/file drops are relevant on Hyprland and require W15, wayland and clipboard. Adopt the regular path. PRIMARY is not accepted through arboard: the default is the shared W15P winit-owned PRIMARY path on the existing connection/event queue and per-seat lifecycle; arboard is only a measured fallback if W15P cannot meet a proven requirement, with compositor evidence and an explicit unresolved gate. |
Regular/image/drop tests; W15P PRIMARY matrix on wlroots/KWin/Mutter; remove when upstream #8657 lands |
| E5 | epaint bidi shaping, visual row order, marks, end caret/selection; word-motion underflow fix; tests without default_fonts |
Core for Arabic/Hebrew UI labels. Adopt only with E12/E13/E16 and with bidi_text.rs removed or reduced; otherwise text is reversed twice. |
Arabic/Hebrew label, mixed LTR/RTL, caret/selection tests; remove when #8577 is released |
| E6 | request_focus on the focused widget no longer interrupts IME |
Relevant to every text field and Preferences search. Adopt. | IME preedit/commit across focus requests |
| E7 | Windows letter keys from virtual-key code on non-Latin layouts | Relevant; PhotoCraft has layout-specific symbol logic. Adopt. | Windows layout matrix (Russian/Arabic/Hebrew/German) |
| E8 | TextEdit no longer clips glyphs above/below the row | Relevant to Thai/Vietnamese/Arabic marks and small fields. Adopt; pixel tests. | Text field row geometry at 1x/1.5x/2x |
| E9 | Style::right_to_left mirrors menus, scrollbars, headers, title bars, combo boxes, checkboxes, popups, resize grips |
PhotoCraft already mirrors shell chrome explicitly in direction.rs. Enabling E9 while keeping those explicit layouts will double-mirror. Adopt only after a call-site audit; default right_to_left=false means it is inert until set. |
RTL chrome fixture matrix; remove overlapping direction paths |
| E10 | eframe web: named keys not typed as text; letter keys from KeyboardEvent.code; lang on hidden input/speech |
Relevant to browser shortcuts and IME. Adopt. | Browser keyboard/IME workflow; no wasm size regression |
| E11 | epaint glyphs rasterized on a single-threaded vello context (fixes vello_cpu multithreading panic) |
PhotoCraft does not enable epaint/multithreading; forcing num_threads: 0 could slow other consumers. Keep out of PhotoCraft's qualifying revision unless the shared fork requires it; if included, benchmark glyph rasterization and test all fonts. |
epaint tests with/without multithreading; perf rows |
| E12 | LayoutJob::direction, RTL right alignment, ellipsis at visual end, UAX #9 L1, linear reorder keeping isolate initiators/PDI/marks |
Needed with E5; replaces bidi_text::visual_ltr and elide. Adopt in the language stage. |
Title bar *صورة.psd — PhotoCraft, layer-name truncation, bidi controls |
| E13 | caret/hit-test/Delete/Backspace by grapheme cluster | TextEdit and Type tool. Adopt; the Type tool has its own char-based editor (type_tool.rs:562, :751, :964), which must be audited for combining marks/emoji. |
Grapheme deletion/movement tests in both TextEdit and Type tool |
| E14 | FontTweak::fallback_align: fallback fonts on primary baseline, rows grow to fit; default unchanged |
PhotoCraft already computes y_offset_factor manually. Adopt, but do not set fallback_align unless the manual metrics are removed; test row height and CJK/Thai/Arabic baselines. |
Font fallback baseline fixtures; cjk_fonts tests |
| E15 | LineBreaker trait (SimpleLineBreaker preserves rules), grapheme-safe emergency breaks, LayoutJob::language |
Default preserves behavior; the suite uses it for craft-text line breaking, and E20 uses the language. PhotoCraft document text is parley-based, so the app impact is UI text wrapping/truncation. Adopt; test wrapping/truncation. | Wrapping fixtures with Latin/CJK/Thai |
| E16 | visual cursor movement with affinity, Home/End in reading order, password mask per cluster, neutral paragraphs inherit direction | Needed for RTL and grapheme editing. Adopt with E5/E12/E13. | Caret affinity tests, mixed-direction paragraphs |
| E17 | Context::set_language, UiBuilder::language, Response::accessible_language, AccessKit root language, one text run per bidi level |
PhotoCraft's i18n::sync_context (i18n/mod.rs:409) must call set_language; add AccessKit language assertions. Adopt. |
AccessKit root/label language; screen-reader smoke test |
| E18 | eframe Frame::set_ui_language sets Context::set_language; web falls back to Context::language |
Call from PhotocraftApp::logic when the preference changes. Adopt. |
Live language switch without losing UI state |
| E19 | one letter-key rule across Windows/macOS/Linux; RawInput::keyboard_layout_is_latin and latin_layout_chars |
Relevant to non-Latin shortcuts; requires winit W25. Adopt, but keep layout_keys.rs's Photoshop-specific German bracket table and test that the two rules do not fight. |
Layout matrix; layout_keys tests |
| E20 | widget text laid out in the language of its Ui |
Adopt with E15/E17; PhotoCraft's tl! remains the source text. |
Translated widget labels in AccessKit and layout |
| E21 | ImeEvent::DeleteSurroundingBytes: TextEdit deletes exact bytes an IME replaces, in the commit's undo step |
Relevant to macOS press-and-hold/dictation and Wayland IME. Requires winit W26/W27. The candidate emits this on macOS and Wayland only; Windows ime_delete_surrounding returns None. Restrict E21 acceptance to macOS/Wayland. The enum variant is a compile break for exhaustive matches; type_tool.rs has a catch-all but must implement the deletion for the Type tool. Adopt. |
macOS/Wayland IME replacement tests; undo one step. A Windows IME row is general coverage, not E21. |
| E21b | IMEOutput gains a bounded logical surrounding-text payload (committed text, UTF-8 byte cursor/anchor, owning widget identity; preedit excluded) and egui-winit sends it through W27 on Wayland |
New prerequisite, not existing E21/W27 capability. The candidate never calls set_ime_surrounding_text; IMEOutput has no logical text. Adopt with E21. PhotoCraft's custom Type tool must supply a real model adapter, not None: type_tool::draw_overlay constructs the IMEOutput; use the layer's logical text (current_text/layout), char-to-byte via byte_of, exclude the preedit char range, clamp to the 4000-byte payload at char boundaries, remap cursor/anchor bytes, and identify the document+layer+viewport so two windows cannot share a payload. Password fields never supply text; focus loss, no IME and unavailable context clear it. |
PhotoCraft adapter plus tests: delete+commit and deletion-only, preedit exclusion, reversed selection, emoji/combining/RTL logical order, caret movement, >4000-byte line, two documents, focus/reset/late serial, multipass final output, one undo; native Wayland predictive/Vietnamese trace. Revert with the custom-editor adapters as one API set. |
The E-patches are not a blanket "merge the branch". In particular, E5/E12/E13/E16 form a dependency cluster; E14/E15/E20 depend on text-layout changes; E9 must not be enabled with the current explicit RTL mirroring; E11 has no PhotoCraft consumer. E21b depends on E21/W27 and is a separate reviewed change (egui P7b), not a retroactive description of E21.
6. Winit patch audit and compatibility
Base: upstream v0.30.13 e9809ef5; candidate
8b35388b (storytold craft/0.30.13 ffae5198 + W17–W27). W15P is new
work in the reviewed published revision, not part of the candidate; the final
production pin must include it.
| # | Change | PhotoCraft decision |
|---|---|---|
| W1 | Windows 11 WM_DPICHANGED suggested rect |
Adopt; custom title bar and mixed-DPI drags |
| W2 | Wayland destroy seat text input only when seat is removed | Adopt |
| W3 | Windows IME caret from GCS_CURSORPOS only when set |
Adopt |
| W4 | Windows hidden maximized windows | Adopt |
| W5 | X11 focus replacement IME context | Adopt as part of W20; audit whether W20 supersedes and remove W5's refocus path |
| W6 | macOS NSTextInputClient commit flow |
Adopt |
| W7 | macOS discard marked text when IME disabled | Adopt |
| W8 | macOS emoji picker text as Ime::Commit |
Adopt |
| W9 | macOS Korean space not duplicated | Adopt |
| W10 | macOS first Apple Korean key | Adopt |
| W11 | X11 server DPI before panel physical DPI | Adopt |
| W12 | macOS mouseDownCanMoveWindow from movable_by_window_background |
PhotoCraft calls NSWindow::setMovable(false) (apps/photocraft/src/mac_window.rs) and drags only its own gap; keep suite-wide but not PhotoCraft-required. Verify no regression. |
| W13 | Wayland pixel/continuous scroll phases report Moved |
Adopt; zoom/pan latch tests |
| W14 | macOS opt-in cancelable termination | PhotoCraft quits via NSApplication terminate: (mac_window.rs); not required, harmless if opt-in. |
| W15 | Wayland file drops + winit clipboard data device | Adopt; needed by E4 regular clipboard and the app's drop support |
| W15P | winit-owned PRIMARY selection on the existing connection/event queue, with per-seat PrimarySelectionManagerState/device/source/offer lifecycle |
Required shared path. Distinct PRIMARY protocol object; do not bind a second regular wl_data_device. Adopt before E4's PRIMARY acceptance. Keyboard-only focus, serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfer, password exclusion and unchanged-selection reclaim policy are native gates on wlroots/KWin/Mutter. |
| W16 | Wayland pinch/rotation gestures | Defer; PhotoCraft has wheel_nav::fold_legacy_pinch, which must be re-tested |
| W17 | Regression tests for W2/W3/W5–W10/W12/W13 | Adopt with the branch |
| W18 | Windows hidden system caret for IME | Adopt |
| W19 | Windows IME VK_PROCESSKEY not sent as key press |
Adopt |
| W20 | X11 one input context per window (replaces W5 refocus) | Adopt; resolves W5 duplication |
| W21 | X11 IME spot after 100 ms quiet | Adopt |
| W22 | macOS ImeDocument, replacement ranges, typed text without composition |
Adopt |
| W23 | macOS ⌘ key names from layout's ⌘ table | Adopt |
| W24 | Window::reset_ime() |
Adopt; expose through eframe if a PhotoCraft call site needs it (currently none) |
| W25 | X11/Wayland KeyEventExtLatinLayout |
Adopt; required by E19 |
| W26 | macOS delete replaced text by UTF-8 byte count | Adopt; required by E21 |
| W27 | Wayland text-input-v3 surrounding text + delete-surrounding | Adopt; required by E21 and E21b. The winit state/serial/payload tests are part of the E21b acceptance, not optional coverage. |
Compatibility notes:
- Winit 0.30.13 is the last 0.30 release; the 0.31 port is not a migration target.
The fork must keep the package version
0.30.13so=0.30.13andegui-winit's dependency unify. - W14 takes
dpifrom crates.io so git consumers do not get a seconddpi. - The fork's
rwh_06andx11/waylandfeatures must remain compatible withaccesskit_winitand eframe. - The app's direct Linux
winitdependency exists only forEventLoopBuilderExtX11(apps/photocraft/src/main.rs:658); it must resolve to the same patched revision as eframe's winit. - Do not add
wayland-csd-adwaitahere; it is a suite feature decision, not a PhotoCraft migration requirement.
7. craft-libs PR 199
PR 199 is an open API-port, not a mergeable tree. Its head
b68de8c5f4f1c060904d211a7b1320aa0f7bce78 and the historical audit tree
ec8840a are stale relative to current main 49190884278f16c9a9f5823d7698a8ffb5deb062:
they omit craft-platform and revert security, settings, docking and printing
fixes. Take only the reusable port hunks from
8614e4a04fae516fba16c22b31ff1008cb6be10b plus the relevant README changes from
1764bca and apply them on current main; never merge b68de8c/ec8840a as
trees and never revert the intervening fixes.
The relevant craft-ui port changes are:
crates/ui/src/buttons/mod.rs:WidgetType::Button→Role::Button.crates/ui/src/docking/surface.rs:WidgetType→Role,UiBuilder::id→scope_id, plus a clippycollapsible_matchfix.crates/ui/src/layout/resize.rs,tabs/mod.rsand tests:Id::new→Id::unique,Ui::id→scope_id,WidgetType→Role.crates/ui/Cargo.toml:egui_extras/egui_kittest0.37.
PhotoCraft uses craft_ui::docking::{DockArea, Layout, Node},
craft_ui::tabs::{Tab, fit}, and craft_ui::layout in
panel_docking.rs, tab_strip.rs, dock.rs and state.rs. The port must be
present before PhotoCraft can compile. There is no additional shared UI helper
needed for the mechanical Role/Id migration: PhotoCraft's WidgetInfo calls are
local and the mappings are direct. Do not invent a compatibility shim in
craft-ui.
Pin policy: update only craft-ui to the published revision containing those
port hunks. craft-print (3d37da389a3618ab9b76109da11416e9ca4ea719) and
craft-docpath (82a24b006eefa036b43c09a67fb9a3fbd17ffe63) are not egui
consumers and keep their current pins; bumping all three to one main would pull
the unrelated printer-name/description change between 3d37da3 and the port. If
the suite later requires one craft-libs revision, audit that diff and run
PhotoCraft's print tests before changing these pins. Unaffected local vendors
(krilla, hayro*, lopdf, vello_common) and unrelated source fixes are
preserved.
8. Lifecycle and root-Ui design
8.1 Logic and hidden iterations
The verified model is section 3.3, not "logic when hidden, ui when visible".
Native and web both run logic and ui inside run_ui, once per pass; hidden hosts
may call update_logic_only, which invokes raw_input_hook and App::logic
without beginning a pass. run_logic leaves events, input time and layout stale.
A forced hidden paint (RequestPaintWhileHidden) runs the normal UI phase.
| Work | Current site | Decision |
|---|---|---|
| Control requests, synthetic input queue | logic drain_control; raw_input_hook take_synthetic_step |
Keep the queue in logic. take_synthetic_step may consume on a hidden logic tick, so set synthetic_in_flight in the hook, send RequestPaintWhileHidden from logic until a real UI pass consumes the preserved events, and clear the flag in App::ui after that pass. Do not use the end of ui as the only wake. |
| Screenshots | logic issue/collect |
Keep in logic; bounded slot; the screenshot command itself requests paint while hidden, so a hidden screenshot pass is not evidence that synthetic input would have woken the app. Use rendered-frame identity + monotonic settle deadline. |
| Job polling | jobs_ui::tick in logic; jobs_ui::keep_polling at end of ui |
Move keep_polling to logic or call from both. Consume job results by revision/sequence exactly once per pass; a repeated pass must not re-apply a result. |
| Autosave/recovery | format::Autosaver background thread; on_exit saves tool memory |
Use std::time::Instant (or web_time::Instant) for intervals and deadlines, never ctx.input(|i| i.time) in logic. Ensure on_exit runs for both renderers. |
| Preferences/theme/fonts | logic syncs language and calls install_fonts; ui applies theme on first frame |
Keep setup in setup_context when possible; logic must finish setup if the creator did not. |
| File drops | logic drains OS/web inboxes and dropped files |
Keep; RequestPaintWhileHidden when a drop arrives with no visible frame. |
| Native menu | logic runs menu; ui syncs it |
native_menu::sync is UI-dependent; leave in ui, but queue menu events for the next paint. |
| GPU status/device loss | logic calls check_gpu; ui calls check_window |
check_gpu stays in logic; check_window can stay in ui because it reports a painted frame, not a logic-pass count. |
UI-local persistence (tool_memory, last_brush, view_cmds) |
end of ui |
Only runs when UI runs; acceptable because no user edit happened while hidden, but on_exit must flush the last state. A second pass must not write twice. |
shortcuts::handle, drain_control, file_ui drops and jobs_ui::tick all run
per pass. Give each a consumed queue (drain once), a revision/generation check, or
a monotonic deadline; do not count logic calls as rendered frames. Do not start
stroke tracing or interpret fresh input on a logic-only tick: gate it on
synthetic_in_flight/a real UI phase, or move the start to App::ui.
Avoid a blanket Context bridge. Use ctx.input, ctx.data, ctx.memory only
where the existing code already does; do not replay the whole UI from logic.
Do not poll a prior UI event twice in run_logic; the events there are stale.
8.2 Multiple passes
Context::run_ui and Context::run_pass share the root Ui; plugins run inside
root_ui. PhotoCraft's setup_context registers CursorLifecycle,
SingleLineDrag, TextFieldMenu, UiFontSizePlugin, CjkFontPlugin.
on_begin_pass/on_end_pass and popup sizing passes can execute content more
than once per frame. current_pass_index is not a durable "first pass" flag: a
completed run_ui leaves it at zero. Guard all side effects in ui:
self.frame += 1stays inlogic, but is a pass count; usectx.cumulative_frame_nr()for rendered-frame delays;- control/screenshot queues stay in
logic; notices::showand persistence must be idempotent for repeated content passes;- no counters inside popup/window closures that change on a sizing pass.
Action playback (actions.rs) and autosave (format::Autosaver /
RecoveryStore) are not UI loops: playback advances through engine commands,
and autosave is a background thread. Still, a playback step must not run twice
because a menu/popup content closure was re-run, and an autosave request must
not be duplicated by a repeated ui pass. Undo is engine-owned
(photocraft-engine history); the rule is one user action → one command → one
history step, never a command in a sizing pass with no user event.
Add tests for: a forced popup sizing pass (Popup::sizing_pass) with unchanged
document, undo stack and control replies; a playback step whose dialog content
pass repeats with unchanged step/history counts; a forced second ui pass with
one command; and a background completion between passes applied once.
8.3 Hidden-window and control input
ui.pointer and ui.key must work while the window is occluded or minimized.
Screenshots must work while occluded; while minimized the window has no surface
that can be trusted, so the request fails with an actionable "the window is
minimized" error instead of saving a blank/stale surface. The minimized flag is
part of the geometry epoch, so a minimize/restore between issuance and collection
retries. Send ViewportCommand::RequestPaintWhileHidden when synthetic input
is in flight, input_waiters is non-empty, or a screenshot/control reply waits on
a pass. Keep the existing reply deadlines (60 s) and Dispatch/AfterInput
semantics. A hidden window must not turn a queued request into a timeout: not run error.
Test the real host order, not a once-per-frame fiction:
- visible single- and multi-pass frames;
- repeated hidden
update_logic_onlyticks after a completed multi-pass frame; - a hidden forced UI paint with queued synthetic input, then a visible pass;
- a background completion between passes, applied once;
- bare
run_logicfollowed byrun_uiproving stale input is not replayed.
Screenshots request their own paint; a passing hidden screenshot does not prove
RequestPaintWhileHidden for synthetic input. The mutation check for that is
section 13.6.
9. Panels, docking and screenshots
9.1 Panel state
0.37 derives a panel id from its parent scope: for the root Ui,
ctx.viewport_id().root_ui_id().with(salt). Id::with takes an AsIdSalt;
Id::with_salt takes an IdSalt, so the v1 helper's with_salt(salt) did not
compile. Add one helper in panels.rs:
pub fn panel_state_id(ctx: &egui::Context, salt: impl egui::AsIdSalt) -> egui::Id {
ctx.viewport_id().root_ui_id().with(salt)
}
Add a compile-level unit test for the helper (and for the direction.rs
AsIdSalt bounds), then use it in main.rs:1004, panels.rs:6604,
panels.rs:6934, workspace_ui.rs:594, panel_docking_tests.rs:569, and in an
idempotent first-frame migration:
for name in ["dock", "title_bar", "options_bar", "status_bar", "toolbar", "rail", "shared-panel-dock"] {
let new_id = panel_state_id(ctx, name);
if PanelState::load(ctx, new_id).is_none() {
if let Some(old) = PanelState::load(ctx, egui::Id::unique(name)) {
ctx.data_mut(|d| d.insert_persisted(new_id, old));
}
}
}
This migration belongs to the first compatible PR: it is the same PR that
changes the constructors to AsIdSalt. The old Id::unique(name) key (same hash
as the old Id::new(name)) stays untouched, so 0.36 rollback still reads panel
widths; the new key is what 0.37 reads. Ui::scope_id is the renamed old
Ui::id; Ui::unique_id is unstable and must not be used for a persisted key.
Test old and new ui.ron fixtures, including a real 0.14.0 save with dock
width 410 and shared-panel-dock 290; then resize in 0.37, save and restart twice,
asserting the new width survives even though the old key remains. Also test the
helper/old-key equality and partially migrated state; existing new keys always win.
9.2 Window settings
Current ui_state.rs reads the old flat schema and is platform-dependent:
macOS checks inner_position_pixels, others outer_position_pixels
(ui_state.rs:41-44). 0.37's WindowSettings is:
normal_geometry: { outer_position_pixels, scale_factor, inner_size_points }(inner_size_pointsis a plainVec2, notOption<Vec2>);fullscreen,maximized.
The old value does not deserialize into 0.37. Merely "understanding both schemas"
is insufficient because eframe reads the file before PhotoCraft runs. Add an
explicit pre-load rewrite in ui_state::sanitize (called from
apps/photocraft/src/main.rs:664 before run_native):
- Parse the outer map of key -> RON string; leave a non-RON file alone.
- If
windowis the old flat schema, write a newnormal_geometryvalue: preserve validSome(inner_size_points)using pattern matching (neverunwrap); when missing, use PhotoCraft's existing configured default 1440×900 points (native_options,main.rs:110) and record that fallback; reject non-finite/oversize values without rewriting,fullscreen,maximized; setscale_factor = 1.0for the Windows position path. Preserveouter_position_pixelsonly on Windows (physical pixels; 0.37 re-applies the physical position after creation). On macOS/Linux drop the old position and let the window center: the old macOSinner_position_pixelsis not an outer position, and the old file has no scale factor for a logical Linux restore. Never guess a scale factor or move the window after first paint. - Keep a one-time
ui.ron.pre-0.37backup before rewriting; log and leave the file untouched if the rewrite fails.
has_saved_position must look for normal_geometry.outer_position_pixels on
every OS and return true only when eframe actually restores that position (the
Windows path). It must stop looking for macOS inner_position_pixels.
Fixtures: old macOS inner position, old Windows/macOS outer position,
maximized and fullscreen, a Some/missing inner_size_points, and 0.36-schema
round-trip. Test that (616,430)/1100x760 yields a 1100x760 window without a
startup failure, and that macOS/Linux center while preserving size/maximized/
fullscreen. Document the rollback loss: after 0.37 writes the nested schema,
0.36 ignores the window entry unless the backup is restored; add a
reverse-compatibility test using a 0.36-shaped settings struct.
9.3 Screenshot callback
Replace the UserData/Event path with a bounded, cancelable per-request
completion slot, not one unbounded mpsc channel per request. Use the shared
craft-ui capture-slot API at the approved CL-2F revision, a P0/P1 prerequisite.
There is no local-slot alternative for PhotoCraft. Keep tokens,
paths, settle delays, deadlines and reply schemas app-owned.
Design:
PendingCapture record (app-owned)
token, path, reply, deadline, issued_at, viewport_id,
document/revision generation,
issued_geometry: { logical_rect, pixels_per_point, expected_surface_size, geometry_epoch },
slot: Arc<CaptureSlot> // bounded, one result, canceled flag
CaptureSlot
result: Mutex<Option<Arc<ColorImage>>>,
canceled: AtomicBool,
wake: egui::Context // live record only
callback = move |image| {
let Some(slot) = weak_slot.upgrade() else { return }; // late/canceled: drop image
if slot.store_if_pending(image) { slot.wake.request_repaint_of(viewport_id); } // after unlock
}
-
The callback holds only
Weak<CaptureSlot>; the live record holds theArc. The slot may hold aContextclone for wakeup, but the callback must not hold aContext,PhotocraftApp, the pending record, or a channel sender. No callback -> Context -> callback cycle. -
Admit at most 4 pending requests, at most 128 MiB of image bytes per request and 256 MiB in aggregate across reserved in-flight and completed images. Use checked width × height × 4 arithmetic and reject before issuance with a clear size/queue error. Test equality and one-over boundaries, arithmetic overflow and cancellation releasing capacity. Transient GPU readback/encoder RSS is measured separately; a late oversized result is dropped, not counted as proof of a pre-allocation bound.
-
The callback supplies pixels only, not frame metadata. Record issuance geometry from the target viewport's UI and a monotonically increasing geometry epoch (resize, DPI/zoom/surface changes), plus document/revision identity. Revalidate before issuing and before saving. Any intervening epoch change, even a change back to the original size, rejects/retries within the deadline; never label the image with guessed geometry. If the epoch is unchanged, derive x/y pixel scale from returned image dimensions versus the recorded logical bounds, check aspect/rounding and apply region cropping with those scales. E2 can fit the surface smaller. Do not use current DPI alone to label an older image. A native resize/DPI race must show that stale metadata cannot produce a saved image; callback-only tests assert the same rejection policy.
-
On timeout/close/cancel, mark the slot canceled and drop the record; a late callback upgrades
WeaktoNone, drops the image, writes no file and sends no reply. There is no "late file/reply" path. -
Keep PNG encoding and the existing
ColorImage-> PNG path on the app/UI thread incontrol::save_screenshot; never encode or write on the callback thread. The callback thread is arbitrary:egui-wgpucompletes it from amap_asynccallback (egui-wgpu/src/capture.rs:193), not a guaranteed render thread. -
While a capture is pending,
logickeeps a hidden-paint request alive. A screenshot command itself requests paint while hidden (ViewportCommand::Screenshotdocs), so a hidden screenshot test does not prove synthetic-inputRequestPaintWhileHidden; section 13.6 tests that separately. -
Settle with
ctx.cumulative_frame_nr()plus a monotonic animation deadline, notapp.frame(a logic-pass counter) and not a 3-pass count.
Tests: two concurrent captures with distinct tokens/paths and exactly one reply
each; callback exactly once; a callback after timeout/cancel writes no file and
sends no reply; close/shutdown drains to zero retained bytes; zero-sized surface
(egui-wgpu returns without calling the callback) times out cleanly with an
actionable error; unsupported capture format (also returns without callbacks)
times out; delayed GPU readback; out-of-order completion; reused viewport does
not bind an old capture to a new document; admission count and aggregate memory
caps (4 requests, 128 MiB/request, 256 MiB aggregate); a 4K resize/DPI race; focus: true raises first and correlates; path: null base64; bad path/authority is an error, not a panic.
For distinct-image/out-of-order tests, intercept emitted screenshot callbacks and complete them manually with different synthetic images in reversed order. Kittest's normal renderer sends the same single rendered image to all callbacks and cannot prove that correlation. Use an adapter-qualified native/GPU row for actual capture timing, readback and viewport pixels; keep ordinary kittest for single-frame rendered checks.
9.4 Docking
craft_ui::docking remains the owner of the layout tree; PhotoCraft keeps
UiState.docking serialization. The 0.37 migration must not change
Layout<String>/Location<String> serde. Test:
- float a tab, move it, redock, split resize, close/reopen, save workspace,
reload
UiState; layout equality and panel rects; Escapeduring a first drag restores the legacy workspace;workspace_lockedrejects moves atomically;- floating group strip is opaque in Studio themes;
- panel IDs stay unique after tabs reorder.
9.5 GPU resource behavior
PhotoCraft's canvas is a custom egui_wgpu paint callback
(crates/ui-egui/src/gpu_canvas.rs), not an egui image. 0.37 changes the
surface integration, not the callback contract, but these must be checked:
- E2/E3: a root or child window larger than
max_texture_dimension_2dis configured at a fitted size and drawn at a matching scale; a configure or renderer error returnsWgpuError::ConfigureSurface/CreateRendererand reaches the CPU-retry path, not a panic. GpuCanvas::newreads the device'smax_texture_dimension_2dand tile size;use_adapter_limitsrequests the adapter limits on web/desktop. Confirm the egui surface limit and the canvas texture limit are independent.- Texture deltas: assert
TexturesDelta::set/freeare applied once per frame, no texture leaks after a dialog closes, and screenshot readback does not retain a full-window buffer. - Device loss:
gpu_status/DeviceHealthstill switches to the CPU canvas; no egui-wgpu panic on a lost device;ui.gpu.simulateLosstest remains green. - Intel/DX12 texel guard:
texel_guard::installstill sees the same adapter; zeroed texels are not exposed after the 0.37 vertex/format changes. - #8736: measure Windows
Renderer::new/pipeline creation separately from the first frame, with ≥20 fresh processes. The reported 0.37 cost is around 57 ms vs 14 ms for renderer construction and 197 ms -> 258 ms first frame on an Intel/DX12 system; this is an external report, not our measurement. Do not use a 200 ms startup allowance to hide it and do not accept clamp-only shader filtering changes for Repeat/MirroredRepeat.
9.6 Accessibility
Map all 114 WidgetInfo calls as in §4. Keep check_accessibility on and add
Response::labelled_by/accessible_name for fields without hints. Add a
screen-wide unnamed_widgets().is_empty() assertion for the main screen and
each dialog. After E17/E18, assert AccessKit root language and
Response::accessible_language.
10. Fonts, glyph coverage and text
10.1 Build and definitions
Required edits:
crates/ui-egui/Cargo.toml:73:epaint_default_fonts = { version = "0.37", features = ["monochrome_emoji_fonts"] }(or the git-patched package with that feature).crates/ui-egui/Cargo.toml:57: deveguifeatures["default_fonts", "monochrome_emoji_fonts"]so the oracle test matches.build.rs:37-45: add("egui-icons", epaint_default_fonts::EGUI_ICONS); keep the two emoji fonts behind the feature.egui_fonts.rs:47-56: addegui-iconsat 0.90; preserve 0.37 family order:- monospace:
Hack,Ubuntu-Light,egui-icons,NotoEmoji-Regular,emoji-icon-font - proportional:
Ubuntu-Light,egui-icons,NotoEmoji-Regular,emoji-icon-font
- monospace:
egui_fonts.rs:79: compare toFontDefinitions::default()with the same features.
Do not enable default_fonts in production; the web size gate depends on the
deflated fonts. No new font file is committed by this change: egui-icons,
NotoEmoji and emoji-icon-font come from epaint_default_fonts. Verify
ATTRIBUTION.md needs no new row; if a packaged font asset is added, add its
path/author/title/source/license row in the same first PR. The font feature,
equality test, glyph coverage and accessible-name fixes are all part of the first
green PR.
10.2 Lazy fallback
cjk_fonts.rs remains the app's fallback owner. Set
NativeOptions::system_font_fallback = false in the first cut. If
system_fonts is enabled later, remove or reconcile cjk_fonts in the same
change; two providers would produce non-deterministic font choices.
#8512 is still on PhotoCraft's path: cjk_fonts reads at most one system font per
frame even with system_fonts off. Measure font discovery and first-use
separately from renderer construction: cold/warm startup with system_fonts
off, with the app's lazy CJK fallback, and with an explicitly configured system
provider; record glyph coverage before/after. Do not assume disabling the
eframe feature removes system-font discovery cost.
ctx.add_font / set_fonts now apply immediately when no text has been laid out
in the pass. Re-check defer_after_reset, fonts_ready, and the
UiFontSizePlugin::defer_after_install one-frame assumptions; add a test where
fonts are installed from inside the first pass and where a language change
rebuilds fonts mid-run.
10.3 Glyph coverage
Add a test that loads every tl!/menu-catalog/catalog string and asserts
Fonts::has_glyphs for each character after install_fonts (with test fonts).
Include ✓ ▲ ▼ ▶ ◀ × … ⏵ ⋮, egui::special_emojis, Arabic/Hebrew/Devanagari/
Thai samples, and the CJK samples already used by cjk_fonts tests. A missing
glyph must fail the test, not render tofu. This test and the minimal
allow_missing_glyphs() exceptions (only intentional tofu) ship in the first
green PR; do not let 481 harnesses inherit MissingGlyphPolicy::Panic without
the glyph decisions.
11. Input, IME, clipboard and RTL
raw_input_hookstays; addlatin_layout_charshandling only if E19 is in the qualifying revision. Fresh input is interpreted only in a known UI phase; the hook may move queued synthetic events into eframe's preserved input, but it setssynthetic_in_flightsologicforces a hidden paint and no other code treats the logic-only tick as a paint.shortcuts::clipboard_keyshandlesEvent::PasteImageas the document paste shortcut through the existingimport_os_clipboard/session clipboard path, not a second clipboard read; add a test for one import.- Middle-click PRIMARY paste is new; verify text fields and the canvas do not both act on it.
- E4 regular clipboard and PRIMARY are separate acceptance rows. PRIMARY's
default is the shared W15P winit-owned path on the existing connection/event
queue with per-seat lifecycle and distinct PRIMARY protocol objects; never bind
a second regular
wl_data_device. The arboard PRIMARY fallback in the candidate is not adopted as the default. If a fallback is retained, record exact compositor/capability evidence and keep PRIMARY an unresolved gate. - PRIMARY native matrix (wlroots, KWin, Mutter): keyboard-only focus, serial freshness, owner/seat changes, empty/absent offers, self-paste, canceled/slow transfers, password selection never offered, unchanged selection not reclaimed, and no second regular data device. Test regular/image/drop separately.
- With E21, add
ImeEvent::DeleteSurroundingBytesto the Type tool's event match (type_tool.rs:964-967) and to any other exhaustive match; implement deletion in bytes with char-boundary checks, one undo step. E21 acceptance is macOS/Wayland only; Windowsime_delete_surroundingreturnsNonein the candidate, so a green Windows IME test is not E21 coverage. - With E21b, extend the Type tool's custom
IMEOutputproducer (type_tool::draw_overlay,type_tool.rs:1255) with the bounded logical surrounding-text payload: committed model text, preedit excluded, UTF-8 byte cursor/anchor, owning document+layer+viewport identity. Usecurrent_text/layoutandbyte_of(char-to-byte); do not pass visually reordered display text. ResolveTextEdit.document(active_doc_idwhenNone) andlayerinto the payload identity. When removing the preedit char range, a caret/anchor inside or at the end of it maps to the range start. Clamp to the payload's 4000-byte limit at char boundaries; if the selection cannot fit, emit explicit unavailable context. Owner/context generation is present even forUnavailable;Availableempty text is a distinct case. Clear cached and compositor state with W27's committed disable/enable transition on owner change/unavailable/focus loss/reset/no IME; normal updates preserve composition. Clear old pending batches. In the current focus epoch, a mismatcheddoneserial still applies incoming edits once and defers outgoing state until a match; old epochs never edit the new owner. Latest completed UI-pass output, including None, is authoritative. Password fields never supply text. Built-inTextEditfields inherit the upstream producer. E21b's payload is notCopy; audit everyIMEOutputconstructor/destructure in PhotoCraft (the Type tool is the only custom one) and keepClone/Eqsemantics. - With E5/E12/E13/E16, remove
bidi_text::visual/visual_ltruse (layer_row_ui.rs:132/137,panels.rs:719) and theunicode-bididependency, or reducebidi_textto non-reordering helpers. Double reordering is the most likely user-visible RTL regression. - With E9, audit
direction.rscall sites before settingStyle::right_to_left. Do not mirror twice. - #8763 selection loss: reproduce "select text, click a toolbar/context action" in PhotoCraft's custom text fields/menu. The fork owns the selected fix with pointer and keyboard regressions (fork P7c, prerequisite in P0). A local workaround requires a demonstrated app-specific gap and removal condition; it is not an undecided alternative to the shared fix.
- Test keyboard/focus: Tab order, Escape, Enter, focus after dialog close, IME preedit/commit, dead keys, grapheme deletion, RTL caret affinity, and the selection-action row.
12. Implementation stages
P0 — fork qualification (outside PhotoCraft)
Publish/qualify canonical egui and winit revisions, including P2b bounded transfers, P7 key pairing, E21b/P7b with W27 clear/lifecycle,
W15P and P7c selection/viewport regression fixes, and publish the craft-libs port
and CL-2F capture helper on current main. Acceptance: PATCHES.md complete;
each patch and W15P/E21b/P7c test; native Linux/Windows/macOS compile and targeted
tests; public immutable full SHAs; no path/branch patches. Stock 0.37 and aiongg
candidates remain research controls. No PhotoCraft PR can claim 0.37 until this
exists.
P0b — baseline harness and measurement (before production)
On ff473881 / 0.36.2, add or extend the harnesses that will judge the migration,
without changing production behavior:
- the fixed 120-label plain-text tessellation scene and a benchmark row;
- Windows renderer-construction/first-frame timing around
Renderer::new; - the select-text-then-click-action regression (kittest and native);
- screenshot lifecycle probes (timeout, cancel, late callback, zero-size surface) against the 0.36 API, adapted to the 0.37 slot contract later;
- panel/window migration fixtures containing a real 0.14.0
ui.ron; - the paired baseline measurements and raw JSON/machine fingerprint.
This PR and its evidence land before P1. A harness invented after production changes is not a before/after control. Record pre-existing baseline failures separately; do not fabricate a baseline from a failing binary.
P1 — first green 0.37 PR (atomic compatibility)
This is the first change set that must pass its own tests, assets, glyph, accessibility, feature and gate checks. It includes:
- root/app/ui
Cargo.toml,Cargo.lock, root[patch.crates-io]for the whole family and winit; featureswgpu,accesskit,wayland,x11,persistence,clipboard;epaint_default_fontswithmonochrome_emoji_fonts; devdefault_fontsfor the oracle; crates/ui-egui/build.rs,egui_fonts.rs,cjk_fonts.rsFontDatauses,TextEdit::hint_texthelpers,theme.rsfamily order;- accessibility names and minimal glyph-policy decisions required by kittest's
default
check_accessibility/MissingGlyphPolicy::Panic; - mechanical
WidgetType/Id::new/ui.id/panel/direction migration, including thepanel_state_idhelper and the first-frame panel migration in this same PR; lib.rsscreenshot slot,control.rs,shortcuts.rspaste handling,menu_nav.rs/field_wheel.rsscroll offsets,main.rsNativeOptions,clipping_indicator_tests.rs;ui_state.rswindow-settings rewrite and backup, with old/new fixtures;- E21b Type-tool producer and compatible
IMEOutputliteral, E21 byte deletion, owner/clear/final-pass semantics, with native evidence tracked explicitly; - screenshot completion slot and its lifecycle tests; panel/window persistence fixtures; no duplicate replies; no lost panel widths;
- any
ATTRIBUTION.mdchange needed by a packaged asset (none expected), and the corrected perf/snapshot/wasm gate commands.
Acceptance: cargo fmt --all -- --check, cargo check --workspace --all-targets,
cargo clippy --workspace --all-targets -- -D warnings, cargo xtask layers,
cargo xtask wasm, cargo test -p photocraft-ui-egui --lib --tests,
cargo test -p photocraft --tests, the build.rs font equality test, glyph
coverage test, explicit expected roles/names on the main screen and every opened dialog,
unnamed_widgets empty as a supplementary check, Event::PasteImage one-import regression,
and packaging/web/package.sh
(per-file limit). No deprecation warnings and no globally disabled kittest check.
P1b — visual fixture expansion (follow-up, not a green requirement)
Files: snapshot.rs scripts/fixtures, panel_docking_tests.rs,
panels.rs visual capture, kittest harness helpers.
Acceptance: baseline vs candidate images for 6 representative themes × 3 scales, exact geometry JSON; deliberate broken probe fails. This expands coverage; it does not carry the names/glyph/feature fixes that P1 already needs to be green.
P2 — extended language and native IME qualification
Files: bidi_text.rs removal/reduction; direction.rs/E9 audit;
i18n::sync_context + Frame::set_ui_language; layout_keys.rs; AccessKit language
and extended native tests. The final fork pin and required E21/E21b model adapter
already land atomically in P1; P2 does not introduce a second dependency stage.
Acceptance: E5/E12/E13/E16/E17/E18/E19/E20/E21/E21b tests; RTL/grapheme/IME
matrix; E21b outgoing text after commit/deletion, preedit exclusion, byte
indices, long selection, field/window switch and multipass final output; native
Wayland predictive/Vietnamese trace or an explicit unresolved gate; no wasm size
regression.
P3 — web browser qualification
Files: apps/photocraft-web only if API changes; WebShell behavior tests.
Acceptance: cargo xtask wasm, packaging/web/package.sh, browser workflow
(open/type/paste/drop/screenshot); every shipped file ≤24 MiB and gzipped total
plus raw wasm within a stated tolerance (+3%) of baseline.
P4 — performance and cleanup
Files: benchmark additions only if needed; devlog and parity/localization docs. Acceptance: paired perf protocol below; cleanup of temporary shims; no production change without a review.
Dependency DAG:
P0 forks/craft-libs (E21b, W15P)
└── P0b baseline harness/measurement
└── P1 first green compatibility (names/glyph/features/panel/window)
├── P1b visual expansion
└── P2 extended language/native qualification ── P3 web
└── P4 perf/cleanup
Rollback: revert P1 → old lock and 0.36 family plus the preserved ui.ron
backup (panel keys remain readable; window geometry needs the backup); P1b is
additive tests/fixtures; P2 reverts to the P1 fork rev, restores bidi_text.rs
and removes the E21b adapters as one API set; P3 is web-only; P4 is
evidence/docs.
12.5 Required gates
Run through the server task wrapper with one resource task and a fresh job ID per command; do not run these directly as a delegated worker:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test -p photocraft-ui-egui --lib --tests
cargo test -p photocraft --tests
cargo test -p photocraft-web
cargo test -p photocraft-tablet
cargo test -p photocraft-automation
cargo xtask layers
cargo xtask wasm
cargo xtask parity
cargo xtask scorecard --check
cargo test -p photocraft-engine --test panic_hunt -- --ignored
Conditional gates: cargo xtask test-corpus if io, psd, codecs, compose,
gpu, text or format change (not expected in the UI-only stages);
cargo xtask perf --quick after each stage; full cargo xtask perf before the
performance sign-off, including text_tessellation and the Windows renderer row.
Windows/macOS native checks follow the corresponding server workflow and their own
VM/helper procedures.
Do not claim a gate passed from a compile-only run. A skipped optional test
(e.g. missing CRAFT_FONTS_DIR) is recorded as skipped, not passed. Baseline
evidence for P0b (raw samples, machine fingerprint, lock/source hashes) must exist
before any P1 production edit is measured.
13. Test plan: user-observable assertions
13.1 Stable fixtures
- Synthetic documents with fixed seeds: 1440×900 UI; a 400×300 document; a 20 MP layered document; a 36 MP photo; a 14000×14000 document. Store fixture generation seed and SHA256.
- Themes:
ProMedium,Studio,Classic,Adwaita,BreezeDark,SoftLight(representative; full 14-theme matrix nightly). - Scale factors: 1.0, 1.5, 2.0; viewports 800×480, 1440×900, 3840×2160.
- Persistence fixtures: a real baseline
ui.ronsaved by 0.14.0, plus a hand-edited old-schema file.
13.2 Geometry and hit tests
- Panels are shown in the expected order (title/options/status/toolbar/dock,
then central); no overlap; each panel is nested in the root Ui, not a child;
central canvas rect positive; clipping contains the canvas; panel state IDs
unique; panel sizes restore from both old and new
ui.ronkeys. - Dock: float/redock/split/resize/close/reopen; exact rects within 0.5 pt.
- Menus: long menu scrolls to last row; arrows and wheel; submenu hidden-row behavior; taskbar-aware placement.
- Toolbars: options bar controls hit at 1x/1.5x/2x; no dead zones from new atom padding.
- Canvas: brush stroke, pan, zoom, crop box, transform handles; document pixels and overlays.
13.3 Pixel comparisons
Use snapshot --out <png> --size <w>x<h> --scale <s> --system-theme <light|dark|none> --script <json> for each fixture. --system-theme only selects the OS
appearance; select a PhotoCraft theme through the control script, e.g.
--script '[["ui.set",{"theme":"studio"}]]'. Compare baseline and candidate with
the same wgpu adapter/driver/DPI. Proposed tolerance:
- 99.5% of pixels have per-channel delta ≤ 3;
- no pixel has delta > 12;
- geometry JSON (panel rects) equals within 0.5 pt;
- text antialiasing may differ only inside this tolerance; a larger diff is a font/style decision, not noise.
Justify any wider tolerance with the exact font/renderer change. Do not update goldens to make a test pass.
13.4 Accessibility and semantics
unnamed_widgets()empty for main screen and every dialog.- Roles: buttons, checkboxes, radio buttons, sliders, spin buttons, combos, color wells, text inputs, disclosure triangles.
- Names/labels: icon-only buttons have accessible names; text fields have labels or placeholders; selected state is reported.
- After E17/E18: AccessKit root language and
Response::accessible_language. - RTL: reading order and label language; no double-reversed Arabic.
13.5 Keyboard, focus, selection and IME
- Tab/Shift+Tab through every field; Enter/Escape; focus returns after dialogs.
- Selection regression (#8763/#8764): select text, click a toolbar/context action; the intended selected range reaches the action, pointer and keyboard paths agree, and undo restores one step. Run both kittest and a real native workflow.
- IME: Japanese/Korean/Chinese preedit and commit; macOS press-and-hold;
Wayland text-input-v3; Windows IME.
DeleteSurroundingBytesdeletion and one undo step are E21 coverage on macOS/Wayland only; Windows IME is general coverage, because the candidate returnsNonethere. - E21b: outgoing surrounding text immediately after delete+commit and deletion-only batches; preedit excluded; reversed selection; supplementary/ combining/RTL logical order; cursor movement; >4000-byte line; two documents; field/window switch; focus/reset/late serial; password/no-IME clears; multipass final output; one undo. A native Wayland Vietnamese/predictive-IME protocol trace is an external gate.
- Non-Latin layouts: Russian/Arabic/Hebrew shortcuts; German bracket keys;
Dvorak brackets;
⌘Von Dvorak-QWERTY ⌘. - Clipboard: text copy/paste/cut, image paste (
Event::PasteImage), PRIMARY middle-click, file-list paste, empty clipboard, occupied clipboard. - PRIMARY: keyboard-only focus, serial freshness, owner/seat changes, empty or absent offers, self-paste, canceled/slow transfer, password exclusion, unchanged-selection reclaim policy. Run on wlroots, KWin and Mutter; record capabilities. A green arboard-only run is not PRIMARY acceptance.
13.6 Broken-behavior probes (mutation checks)
- Temporarily remove one
widget_infolabel in a test-only copy → accessibility test must fail. - Temporarily pre-reorder an Arabic label with
bidi_text::visualwhile E5 is active → RTL fixture must fail. - Temporarily set the panel state key to the old ID → persistence test must fail.
- Temporarily return a fixed screenshot token → correlation test must fail.
- Temporarily disable the slot's late-callback drop → a timeout/cancel probe must fail (no file/reply).
- Temporarily disable
RequestPaintWhileHiddenfor synthetic input → hidden control test must time out/fail. Do this separately from a hidden screenshot: the screenshot command itself requests paint, so it cannot prove the synthetic path. - Temporarily replay a consumed screenshot/control event across a second pass → the duplicate-reply/command test must fail.
13.7 Headless Context::run_ui and run_logic
Every new logic/ui boundary must be exercised without eframe, with the real callback order:
Context::run_uiwith the app'suibody, a fixedRawInput, and no GPU: assert panel geometry, control replies and state transitions.- Pass the existing
&mut Uito code that needs it.Context::root_uipanics when the root Ui is already borrowed (0.37context.rs:1011-1024); test that documented panic as a negative test and test a successfulrun_pass/root_uicall separately. Never introduce a recursiveroot_uiin production. - A hidden-tick driver must call
App::raw_input_hookin eframe's order (prepare_raw_input) beforeApp::logic, not barerun_logic, and then run a laterrun_ui: assert synthetic/control input is preserved and consumed once, and that stale events/time are not replayed. Use an eframe/kittest hidden-window driver or a directupdate_logic_onlyequivalent; do not invent a native once-per-frame host. - A headless pass must not be treated as proof of native focus, IME, clipboard ownership or file pickers; those need the native rows below.
13.8 Native and browser workflows
- Linux X11 and Wayland (wlroots/KWin and GNOME/Mutter), Windows 11, macOS.
- Real focus/IME/file pickers cannot be proven by kittest; record named hardware rows and screenshots. macOS evidence is an unresolved gate until real hardware rows exist; do not mark the migration complete on Linux-only evidence.
- Browser: WebGPU and WebGL2; open a file, type in a field, paste, drop a file, take a screenshot, resize the page; verify no JS errors in the console.
- Extra document windows: open two, draw in each, close one during a stroke, screenshot the root while a child is open, and confirm control input still reaches the root; immediate-viewport raw input must not be duplicated.
- #8748 is closed/unmerged and PhotoCraft has no deferred viewports: record that explicit non-applicability, then test hidden/occluded root + immediate child open/update/close (including a child left open when the root is hidden).
14. Performance protocol
14.1 Baseline
Before P1 (the first production change), run the baseline on the same machine that will run the candidate. The P0b harness is added on 0.36.2 first; stock 0.37 and the fork are controls, not replacement baselines.
- record
uname -a, CPU model/cores, RAM, GPU adapter/vendor/driver, wgpu backend, OS build, Rust version,Cargo.lockSHA256, exact command lines and feature set; - renderer: Vulkan (or Metal/DX12 as applicable), plus a separate
--cpusoftware-renderer run; - DPI: 1.0 and 2.0; viewport 1440×900, plus 3840×2160 for large-window tests;
- profile:
--release,--locked, noCRAFT_FONTS_DIRunless the row is font-specific; record whether craft-fonts is embedded.
Run each row with the shared sample protocol; keep raw JSON. The repository's
existing 15-sample/5-sample bench counts and interactive_bench --reps 5 are
smoke settings, not acceptance evidence. For migration evidence use at least:
- 10 warmups + 30 timed iterations for steady operations;
- 20 fresh processes for startup and renderer/font-discovery rows;
- at least 1,000 rendered frames for frame distributions; report p50/p95/p99 and only claim a p99 from those frames (never from 20/30/50 samples);
- 1 warmup + 5 samples for 196 MP / 2 GB rows is an exploratory smaller run, not tail acceptance; label it as such and do not report its p95/p99 as acceptance;
- interleave baseline/candidate (A/B/A/B) to control thermal drift;
- reject a run when load average is high or another build shares the machine; rerun rather than accepting a noisy result. Use a paired confidence interval where suitable and record peak RSS, allocations/texture uploads and idle repaint/CPU separately.
14.2 Metrics and thresholds
Thresholds are investigation triggers, not permissions to accept a regression. A repeatable >5% median or >10% p95 change is investigated; existing stricter repository budgets win. Do not expand tolerance after seeing a failure; an exception needs a documented tradeoff and owner approval before acceptance. For sub-millisecond rows require the relative increase to exceed 0.05 ms or corroborate it with stable instruction/allocation counts.
| Metric | Baseline source | Gate |
|---|---|---|
| UI pass p50 | new frame_bench or perf ui_ms |
investigate >5%; no looser absolute allowance |
| UI pass p95 | same | investigate >10% |
| UI pass p99 | same, only from ≥1,000 rendered frames | report; investigate >10% when the sample supports it |
| Operation p50 / p95 | perf_scenarios rows / interactive_bench |
investigate >5% / >10% |
| Peak RSS | RssSampler / /usr/bin/time -l |
investigate >5%; report peak, not only completion |
| GPU bytes held | perf_scenarios gpu_bytes |
investigate >5% |
| Texture uploads / vertices / UI allocations | new test-only counters from FullOutput.textures_delta, tessellation and a scoped allocator around the app pass |
investigate >10% |
| Idle repaint/CPU | 60 s with no input: frame count, process CPU %, GPU util | any continuous repaint, new busy-loop or audio underrun is a failure |
| Startup to first frame | app log timestamps / gpu_startup |
investigate >5% median or >10% p95; no +200 ms allowance |
| Renderer construction (#8736) | Renderer::new/pipeline timestamps, 20 fresh processes |
investigate >5% median or >10% p95; separate from first frame |
| Font discovery (#8512) | cold/warm process trace with system fonts off/on | investigate >5% / >10%; report glyph coverage before/after |
| Text tessellation (#8731) | fixed 120-label text_tessellation row |
investigate >5% median or >10% p95; compare 0.36, stock 0.37, fork |
| Selection action (#8763) | select-text-then-click-action regression | no lost range; pointer/keyboard agree; one undo step |
| Wasm size | packaging/web/package.sh output |
every shipped file ≤24 MiB; gzipped total and raw wasm ≤ +3% vs baseline |
| Click-to-pixel latency | stroke_trace |
investigate >5% median or >10% p95 |
Do not report compile duration as UI performance. Do not compare the committed
macOS perf/baseline.json with a Linux/Windows run. Do not auto-accept a changed
pixel golden or a changed budget; profile and either fix or document an explicit
accepted regression with owner approval.
14.3 Commands
All commands run through the server task wrapper, one resource task and a fresh job ID per intentional execution:
python3 /home/codex/work/maintenance/task.py run \
--job-id <task>-perf-baseline-01 --kind build --task <task> \
--source /home/codex/work/CraftApps/egui037-migration-20261022/photocraft \
--evidence /home/codex/work/ssd2/task-evidence/<task> \
-- bash -c 'cargo xtask perf --bench perf_scenarios'
xtask perf appends its own --json <target>/perf/<bench>.json; it rejects a
user-supplied --json as an unknown option. After the run, copy
$CARGO_TARGET_DIR/perf/perf_scenarios.json and summary.md into evidence. The
bench's own --json is only for invoking the example directly.
Equivalent runs for interactive_bench, fx_bench, bevel_bench,
camera_raw_bench, type_bench, large_image_bench, large_file_bench, the new
text_tessellation row, plus cargo xtask perf --quick as a smoke check. Copy
raw JSON, summary.md, logs, machine fingerprint, command line, source SHA and
lock SHA into evidence. Startup/renderer/font rows run 20 fresh processes each;
frame rows use ≥1,000 rendered frames. Do not claim a p99 from an exploratory
small run.
15. Decisions and remaining gates
Resolved in revision 2 (they are requirements, not open choices):
- E4 PRIMARY: the shared W15P winit-owned path on the existing connection/event queue is the default; arboard is a measured fallback and an unresolved gate, never an adopted default.
- WindowSettings: preserve
inner_size_points,fullscreenandmaximizedon every OS; preserveouter_position_pixelson Windows only; center on macOS/Linux; rewrite before eframe loads and keep a backup. Document the rollback loss. - PanelState: migrate in the first compatible PR; a one-time width reset is not an accepted substitute.
- craft-libs: port only the
8614e4a/1764bcahunks onto currentmain; keepcraft-print/craft-docpathpins and unrelated vendors. Do not mergeb68de8c/ec8840atrees. - PasteImage: treat as document paste through the existing import path.
- E21 and E21b: implement both for the custom Type tool; E21 is macOS/Wayland-only, E21b adds the bounded logical surrounding-text producer.
- E21b: new prerequisite;
Noneplaceholders are not acceptance. - Screenshot: bounded, cancelable weak-slot completion with capture-time geometry, no unbounded channel and no late file/reply.
- Performance: shared 10/30/20/1,000 sample minimums and >5% median /
10% p95 investigation; no looser absolute allowance.
Remaining open gates:
- Fork publication/ownership. Canonical
storytold/eguiandstorytold/winitfull SHAs do not yet exist as published immutable commits containing E21b and W15P. Choosing aiongg branches as canonical would make an individual fork a required public dependency; choosing storytold requires pushing there. No approval is granted in this plan. - E11 inclusion. No PhotoCraft consumer enables
epaint/multithreading. Decide whether the shared fork carries E11 for VectorCraft or whether PhotoCraft's qualifying revision excludes it. - E9/
direction.rsoverlap. Keep explicit shell mirroring and leaveStyle::right_to_leftoff, or switch to E9 and remove overlapping manual mirroring. Needs RTL fixtures first. - System fonts / color emoji. Keep bundled monochrome emoji and no
system_fonts, or enablesystem_fonts(fontique + color fonts) and retire the custom CJK fallback. The latter needs a separate design and size/perf review; #8512 is measured either way. - Final winit patch list. W12/W14/W16 are not PhotoCraft-required. Confirm whether the shared published revision includes them; W15P, W26 and W27 are required regardless.
- External evidence gates. Real macOS IME/focus, Windows native GUI and renderer timing, Wayland compositor/IME/PRIMARY, and browser runtime evidence remain gates. Cross-compilation, kittest and software-renderer VM results do not satisfy them.
16. Historical planning limitations (superseded by current status above)
- Planning only: no build, test, clippy, wasm, GUI, IME, screenshot or performance command was run. The historical audit log in the evidence directory shows the same first font build failure, but it is not fresh validation.
- Revision 2 addresses the v1 findings by source inspection, but it has not been re-reviewed. A fresh independent review of this exact document is required before any publication approval.
- The E/W patch audit is based on the candidate commits and
PATCHES.md; every patch, including E21b and W15P, still needs a line-by-line review against its upstream PR/proposal and a test audit before publication. - E21b has no existing implementation. Its egui payload, TextEdit producer, egui-winit Wayland adapter, PhotoCraft Type-tool adapter and native IME trace are all future work; no claim of existing surrounding-text support remains.
- Windows, macOS, Wayland and browser behavior are unverified. Headless kittest is not proof of native focus, IME, file pickers or compositor behavior.
- No final fork revision or
Cargo.lockhash exists yet; the target lock cannot be produced until P0 is published with the reviewed full SHAs. - The performance baseline machine and exact adapter/driver are not chosen; the thresholds above are pre-registered, not measured. P0b must add the before harness on 0.36.2 before P1.
- No commit, push, PR, comment or external message was made. Publication still requires explicit human approval for every push/new PR, with the exact publication and validation shown first.
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/photocraft main now resolves to e6d6fb45a40250f518d2e824236a8fdcaa4472c8. 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.
Use release0.15.0 and its lockfile as integration baseline; preserve same-origin served fonts and encoded-path rejection, cumulative glyph raster budgets, Select and Mask tile-based edits/undo and new catalog keys. Retain0.14 upgrade fixtures too.
The complete static delta review and exact old/new object identities are retained in the task evidence: canonical-diffs-refresh-20261023.json and overnight-consumer-review-20261023.md. This amendment claims no new build, native GUI or performance acceptance.
Local implementation progress — first compatibility/lifecycle stage (2026-10-23)
Local, uncommitted work only. No build, test, format, clippy, GUI or performance command
was run for this stage; compile and runtime results are pending the parent's managed
task.py jobs. Nothing is published.
Repair round after egui037-photocraft-postport-review-20261023-root-v1.md: the test-target
compile findings were fixed (fonts_mut for has_glyphs checks, the two missing
MouseWheelSource fields, the missing-size window test), together with the runtime findings:
geometry-change budget revalidation, monotonic epoch comparison across resize-and-back, a
bounded pre-geometry admission queue, exact-once synthetic reinjection, withdrawal of a
cancelled synthetic batch from preserved input, a monotonic active-document generation bound to
captures and synthetic input, a minimized-window refusal, bounded PNG encode copies, the panel
rollback stamp and web-shell raw_input_hook forwarding. The findings that remain open are
listed at the end of this section.
Implemented in this checkout (release 0.15.0 base e6d6fb45):
- Dependency family: workspace
egui/eframe/egui_kittest0.37,egui_extras0.37,epaint_default_fonts0.37 withmonochrome_emoji_fonts, deveguiwithdefault_fontsmonochrome_emoji_fonts,eframe/clipboardon the app, UI library and web shell, and a directweb-timedependency. The working tree'sCargo.lockis modified by the parent's local path patches foregui-implementation,winit-implementationandcraft-libs-implementation; it is not a reviewable/publishable lock until the family and winit are pinned to published revisions and the lock is regenerated from them. The pinnedcraft-uirevision currently lackscraft_ui::capture(post-port review F1).
- Fonts:
build.rsdeflatesegui-iconsand the feature-gated emoji faces;egui_fonts.rsreproduces 0.37's faces, scale tweaks and family order;cjk_fonts.rsusesFontData::bytes(); a glyph test coversegui::special_emojisin both families;NativeOptions::system_font_fallback = falsekeeps PhotoCraft's own CJK/craft-fonts fallback the only provider. - Mechanical 0.37 API port:
WidgetType→egui::Role(with the renamed variants),Id::new→Id::unique(hash-preserving),ui.id()→ui.scope_id(),Panel/directiontakeAsIdSalt,TextEdit::hint_texttakes owned text,ScrollAreaoffsets useclamped_offset()and a request applied throughvertical_scroll_offset,Event::PasteImagemaps to the document paste shortcut, and everyEvent::MouseWheelconstructor carriesMouseWheelSource::Unknown. - Panel persistence:
panels::panel_state_id(ctx, salt)is the 0.37 parent-scoped key;panels::migrate_panel_statecopies the oldId::unique(name)PanelStateentries, leaves the old keys intact for a 0.36 rollback, and stamps both keys.panels::remember_panel_staterecords the 0.37 size each pass. On a later start an old key that changed while the new one did not (a 0.36 rollback resize) wins; a 0.37 resize is kept. If both changed the order is unknown and the new key is kept, so a 0.37 resize after a 0.36 rollback resize can still be lost; that ambiguity is documented and tested. Called from the first UI pass; tests cover key identity, the copy, the old-key retention, a 0.37 resize after migration and a 0.36 rollback resize (panels.rs), and panel widths across two restarts (apps/photocraft/src/main.rs). - Window settings:
ui_state::sanitizenow rewrites a 0.36 flatwindowentry to 0.37'snormal_geometrybefore eframe reads it, after a one-timeui.ron.pre-0.37backup. It preserves a valid size (defaulting to 1440×900 when absent),fullscreenandmaximized, preserves the old physical outer position only on Windows, and centres on macOS/Linux. Corrupt sizes are left untouched.has_saved_positionreads only the nested outer position. Tests cover old macOS/Windows fixtures, missing size, corrupt values, the backup, the one-way reverse-compatibility loss, and maximized/fullscreen. - Screenshot lifecycle (
crates/ui-egui/src/screenshot.rs): bounded, cancelable capture requests using the sharedcraft_ui::capture::CaptureSlotwith a weak callback. Admission enforces 4 outstanding requests, 128 MiB per image and 256 MiB aggregate over issued, completed-undrained, unissued and geometry-waiting requests, rejects before issuance with checked arithmetic, and is drained before admission so a finished request releases its budget. A request that arrives before the first UI pass has recorded the geometry waits in a count/deadline-bounded queue and is admitted on the next logic tick. When the geometry is finalized at issuance or after a retry, the per-image and aggregate budgets are recomputed and the reservation updated (or the request fails before any capture). The request identity is the document and its monotonic activation generation (revision recorded, not an invalidation: a switch away and back fails the capture). Geometry (size, scale, zoom, texture side, minimized) has a monotonic epoch updated only in a real UI pass; an epoch advance between issuance and collection retries even when the values change back, and the issuing pass must show the same activation at its start and end. Settling counts UI passes, issuance happens only fromApp::uiafter fonts are ready, and deadlines useweb_time::Instant(monotonic on native and wasm). Accepted images are the issued size within a pixel or the renderer's exact texture-side fit, never an arbitrarily smaller same-aspect surface. A late callback after timeout/cancel is inert; encoding/writing happens on the UI thread incontrol::save_screenshot_rgba, never in the callback. The retained image is dropped before the encoder allocates its RGBA copy, and that copy is bounded by the same per-image limit (finish_capturesrefuses an oversized image rather than allocating). Same-size surface recreation with no size/scale/zoom change is not observable and remains an explicit limitation. - Lifecycle: hidden
run_logicticks requestRequestPaintWhileHiddenfor unissued captures and queued synthetic input; one synthetic batch is in flight at a time, and a pass that returns early re-injects the same batch exactly once (a second hidden tick does not append it again). A document/focus change observed inraw_input_hookwithdraws the batch (including events eframe already preserved) and fails the waiting replies, so cancelled input is never applied. A change made insidelogicafter the hook cannot withdraw events the pass has consumed, so the pass delivers them and keepsautomation_inputset instead of claiming cancellation while the input lands. Control replies wait for a pass that actually delivered the batch. The web shell now forwardsraw_input_hookto the app after its touch-twin filter, so the clipboardPasteImagemapping, legacy pinch, pen-hold and titlebar normalization run in the browser too; the browser has no TCP control server, so the synthetic queue stays empty there and synthetic-input rows remain native-only evidence.gpu_canvas::now_msis now monotonic on wasm (it was a constant 0), so autosave, preference retry backoff, frame caps and stroke traces measure real durations; preference retry and link-watch timing no longer read stalerun_logicinput time.
Integration-test selector repair — 2026-10-24 (historical pre-validation record)
The parent's managed --lib --tests run (egui037-photocraft-candidate-20261024-upstream-tests-18)
passed 3847 UI unit tests and 116 integration tests, with 16 integration failures. All 16 were
kittest/AccessKit selector assumptions, not production behaviour changes:
- kittest 0.5 reads a
Role::Labelnode's text from its AccessKitvalue, so a form caption and a control that carries the same accessible name both matchget_by_label. Batch/Image Processor (Format,Input,Quality,Starting serial), custom print paper (Paper Width,Paper Height,Top,Left) and Export As (Scale,Resample) now select the actualTextInput/SpinButton/ComboBoxby role and name; existing coordinate searches and ambiguous name-only lookups were removed.custom_print_paper::shownusesquery_allbecause a caption and its field are both valid visible matches. - egui 0.37 exposes a
CollapsingHeaderasRole::DisclosureTriangle, notRole::Button. Camera Raw histogram/settings and localization tests now click and assert that role; the section IDs and translated labels are unchanged. - A Camera Raw slider row exposes both its
Sliderand its numericSpinButtonunder the same name.camera_raw_scope::alt_drag_slidernow addresses theSlidertrack directly instead of the caption, preserving the real Alt-drag and overlay/diagnostic assertions.
These edits keep the real clicks, drags, exported bytes, history/revision and engine-field assertions; they add no sleeps, retries or relaxed checks. They do not claim a passing run: the parent must re-run the managed full tests and lint. The broader 481-harness accessibility naming/glyph sweep and visual fixture regeneration remain open.
The ui.ron fixtures under apps/photocraft/tests/fixtures/ are schema probes reconstructed
from the documented layouts, not recorded machine saves (see that directory's README); they
prove the migration rules, not eframe/egui deserialization of a real save.
The compiler/API port, IME producer/adapter, byte deletion, selection-action adoption, shared capture dependency and canonical lockfile are now implemented and covered by the current host qualification above. The later fixture corrections preserve real native viewport geometry, transform completion order, retired document editors and foreign viewport identity; they do not bypass the reset acknowledgement path.
Remaining acceptance work: a real 0.36 docking-layout sample, the broader accessibility/glyph sweep, visual fixture regeneration, packaged WASM size, native/browser/IME/PRIMARY workflows and paired app performance. The app-owned docking JSON still falls back to the legacy layout if it cannot deserialize; DockArea's transient drag/focus state is not persisted.