# Shell-on-kernel playbook

The egui shell becomes a consumer of `View`. After `exit-egui` the core runs
as `view = kernel(&mut session, events, &layout)` and the model is written
down in `docs/react-frontend-interface.md`, but the one shell in the tree
still drives `Session` directly: `App` calls `canvas_frame`, `dispatch`,
`record_history` and `available_commands` itself and reads session state to
draw its chrome. This branch closes that gap without changing what the user
sees, so that at the end `App::update` is: gather egui input into a batch,
call `kernel`, replay the paints, draw the chrome from `View`, act on the
hand-off. The kernel then has a real consumer, and the shell is the shape a
React front end will copy.

Branch `shell-on-kernel`, cut from `exit-egui` at `893f547`. One phase per
commit, `cargo xtask ci` green after each (snapshots included), `todo.md`
entry and a Worklog paragraph here per phase. Behavior-preserving throughout:
the kittest pictures are the proof, and no golden is regenerated except where
a phase says so and explains why.

## Where the shell stands

`App` (`src/app/mod.rs`, 578 lines) is five parts — `Session`, `Surface`,
`Exchange`, `Library`, `Appearance` — and a frame in six phases: `sync`,
`ahead_of_the_canvas`, `gather`, `act`/`dispatch_action`, `settle`, `poll`.
Already in the model: the registry answers `Act::Edit | Act::Effect`; the
parts perform effects behind the `Dialogs` seam; results come back through
`View.handoff`; `Session::dispatch` returns nothing.

Not in the model, and the work of this branch:

| Gap | Where | What the model wants |
|---|---|---|
| ~~The canvas paints through the tool frame~~ (phase 1) | `Surface::show_canvas` → `Session::canvas_frame` over a `Recording`, replayed | `View.paints` replayed onto egui |
| ~~The chrome reads the session~~ (phase 2) | `View.tool`, `.top_bar`, `.status`, `.history`, `.nav_tree`, `.overlay`, `.notices` — owned, built by `Session` in `blockworx_kernel::chrome`; the shell's widgets take the models plus their own chrome state | owned fields on `View`, built in the kernel |
| ~~Pointer resolution is egui's~~ (phase 3) | `Session::resolve(&Input)` over `Raw` pointer events in screen space; the egui `Canvas::input()` reads them off the response | raw pointer state in, `Interaction` derived in the kernel |
| ~~The camera is the egui `View`'s~~ (phase 4) | `Session.camera` owns the vantage, the viewport, the safe region and the framing in flight; the egui `View` reads the wheel, the pinch and the drag pan and reports `Move`s | the session owns the vantage; pan/zoom/pinch are events; `View.vantage` out |
| ~~In-place editing shares a buffer~~ (phase 5) | `Session.editing: Editing { id, text, cursor, selection }`; `Event::Text(TextEvent)`; the recorder paints the draft, the caret and the selection; egui's `TextEdit` is an invisible capture | edit state in the session; edits are events; the core paints text, caret, selection |
| ~~The frame calls the session~~ (phase 6) | `App::shell_frame` gathers one batch, calls `kernel` once, replays the paints and draws the chrome from the answer; `Session` is named by that call, the effect doors and the host facts told ahead of it | `App` calls `kernel` once |

## Phases

Ordered so the app stays green at every commit and each phase leaves one
fewer reason for `App` to name `Session`. Phases 1–2 switch the *output* side
to `View` while input stays as it is; 3–5 switch the input side; 6 deletes
what is left.

| # | Phase | Produces | Proof |
|---|-------|----------|-------|
| 1 | The replayer | `blockworx_egui::replay(&[Paint], &egui::Painter, &mut ImageRegistry)` draws a display list; `Surface::show_canvas` runs `canvas_frame` over a `Recording` and replays it; `Painter` is folded into the replayer and deleted | kittest pictures pixel-identical; nothing paints through a `Painter` |
| 2 | The chrome model | `TopBar`, `Reading`, the history rows, `NavTree`, `Overlay`, `Notice`, `tool` become owned types on `View`, built by the session in `blockworx_kernel::chrome`; the shell widgets take the models | kittest pictures identical; the panels' tests build the models and a kernel test reads them off `View` |
| 3 | Pointer resolution | `compute_interaction` moves into the kernel as `pointer::Resolver`, a state machine over `Event::Pointer(Raw)` (moved/down/up/gone/cancelled/panning in screen space) and `Event::Keys`; the shell sends raw state | the tools' headless suites unchanged; kernel tests for click/drag/double-click thresholds, a long press, a hover ending, a panning frame, world mapping |
| 4 | The camera | the session owns the `Vantage`; `Event::Move(Pan | Zoom)`, `Event::Viewport`, `Event::Safe` and `Action::FrameRect` move it; the framing animation runs on the session's clock; `View.vantage` out; the egui `View` is an input reader over the canvas rect | the `settle` probes and the camera tests, moved to the kernel; a kernel test that a pan and a zoom move `View.vantage`; `Vantage ↔ record::Camera` stays in the kernel |
| 5 | In-place editing | the session holds `Editing { id, text, cursor, selection }`; `TextEvent::{Changed, Committed, Cancelled, TabPressed, CaretAt}` are events; the recorder paints the draft, the caret and the selection from the core's layout; egui's `TextEdit` is an invisible input capture; `View.edit_text` is an `EditField` | the block-edit cycle tests and the label goldens unchanged; kernel tests type, commit and cancel through events and find the caret, the selection and the hint in the display list |
| 6 | `App::update` | `App::shell_frame` is one batch, one `kernel` call, one replay: the docked chrome is drawn from the last call's `View` and its acts go into the batch, the glass over the picture is drawn from this call's `View` and its acts are queued for the next; `Session` is named only by the call, the effect doors and the host facts told ahead of it | `grep session src/` outside the kernel call, the doors and the host facts returns only test harnesses; the seven pictures unchanged |

### Acceptance, every phase

- `cargo xtask ci` green, snapshots included.
- No test outcome changes and no golden regenerated unless the phase says
  which and why (phase 5 may re-pin one snapshot if the caret is drawn a
  pixel differently; that diff is reviewed, not silenced).
- The four gates hold: `headless`, `backend`, `shell`, `palette`. The
  backend never learns what a block is; `replay` takes `Paint`s and nothing
  else.
- Comment hygiene as decided on `exit-egui`: no decision tags, no history;
  a comment explains surprising current behavior or is deleted.

## Decisions

- **D1 Output before input.** Replaying `View.paints` and drawing chrome from
  `View` can land while input is still egui's, because the recorder measures
  text through the same `EpaintLayout` the painter used. Doing input first
  would have meant carrying two pointer models at once.
- **D2 The egui `View` shrinks rather than dies.** It keeps the canvas rect,
  the wheel and pinch reading, the `TextEdit` capture and the icon loader,
  and loses the vantage, the framing animation and the paint closure. What
  remains is the egui equivalent of the `<canvas>` element plus the hidden
  input.
- **D3 Chrome types are owned copies, not borrows.** `TopBar<'a>` and the
  others borrow the session today; on `View` they are values the kernel
  builds each call. The cost is a clone of some strings and rows per update;
  the benefit is that a `View` is a plain value a front end (or a test) can
  hold.
- **D4 `Painter` is folded into `replay` in phase 1, not kept.** The plan
  was to let it survive as the replayer's helper until phase 6, but two
  implementations of "a mark onto egui" — the painter's `Renderer` and the
  replayer's `match` — would have had to agree glyph for glyph for five
  phases. So the headless drivers (`measure::headless`, the tools' suites,
  the bench, the tessellation snapshots) record and replay too, and there is
  one path from a `Paint` to a shape.
- **D6 The chrome models are owned and `Clone + PartialEq`, not yet
  `serde`.** `View` cannot be serialized until `EditText` stops carrying an
  `Rc<RefCell<String>>` (phase 5) and `CommandSet` stops carrying `Action`s
  with tools in them (§7g of the interface doc), so deriving `Serialize` on
  the chrome half alone would prove nothing. The models carry only owned
  data and leaf types the store already serializes, so the derives are a
  one-line change when §7g lands. — landed on `ui-ux-split-completion` (E8)
- **D7 Chrome state that is the shell's stays with the widget.** Which
  navigator segment is open, the tree's expand set and filter, the rename
  box's draft, the recent list, the appearance menu, which picker is up,
  the wall clock a row's age is measured against — none of it is the
  editor's, so none of it is on `View`. A widget takes its model and its
  own state side by side (`top_bar::Docked`, `nav_tree::NavScene`,
  `overlay::Overlay`), and the tree's walk runs over the owned `NavTree`
  with the widget's expand set, so the kernel builds the whole tree and the
  widget decides what of it is showing.
- **D8 What a host says about the pointer is raw; what it says about the
  keys is already scoped.** `Raw` is motions, button edges, the pointer's
  leaving, a cancelled gesture and a panning frame, all in screen space;
  resolving them into a click, a drag or a hover is `pointer::Resolver`'s,
  with egui's own thresholds (6 pt, 0.8 s, 0.3 s) so the shell's feel is
  unchanged. The keys travel as `Keys`, scoped by the host's focus rules —
  which widget holds the keyboard is the toolkit's to know, and a front
  end with a hidden text field will scope them the same way. The camera is
  still the shell's until phase 4, so a pan is reported as `Raw::Panning`
  on each frame it holds the pointer, and the resolver makes no tool
  gesture of such a frame — the same gating the egui view did after the
  fact, done before.
- **D9 A pinch is a pan and a zoom.** egui's two-finger gesture yields a
  translation and a zoom about the gesture's centre, and the old view
  applied them one after the other; so the camera's vocabulary is
  `Move::Pan(Vec2)` and `Move::Zoom { factor, anchor }` and a pinch is the
  two in order, not a third event. `Raw::Panning` stays as phase 3 left it:
  whether a gesture owns the pointer this frame is a fact about the egui
  response (the drag-stopped frame, the trailing finger's latch), not about
  the moves it produced, and the resolver still needs it.
- **D10 The framing eases on the session's clock.** The old view eased in
  `begin` by egui's `stable_dt`; the session eases in `ticks` by the
  interval since its last tick, clamped to a tenth of a second, and asks
  for a repaint while a framing is under way. Under the shell the two
  clocks agree — egui's `stable_dt` *is* the unstable interval whenever a
  repaint was requested last frame, which an easing does every frame — so
  the motion is the same; a front end that ticks only while `View.repaint`
  is `Some` gets the same glide.
- **D11 The tool asks, the session holds, the canvas paints.** A tool still
  says what editor it wants through `Canvas::set_edit_text(EditText)` —
  where it sits, what it opens on, its face, wrap, limits and colours —
  and reads the draft back through `Canvas::draft(id)`; the session keeps
  the one `Editing` (following each frame's request: kept, opened or
  closed), and the recorder paints the field, the selection, the draft or
  the hint, and the caret from the `Layout` the same text will be
  committed with. `EditColors` is every colour the editor is painted in,
  resolved by the tool through `Theme::editor_colors(ink, fill)` — five new
  roles, `EditorText`/`Fill`/`Caret`/`Selection`/`Hint`, all base tones.
- **D12 The host's field is invisible and still lays out.** The egui
  `TextEdit` keeps its face, wrap width and char limit but paints nothing
  (transparent ink, no frame, transparent selection and caret, no blink):
  it is where focus, composition and the clipboard land, and because it
  lays its text out with the engine the recorder drew with, its own caret
  and click placement agree with the painted ones. That is why
  `EditField` carries `font` and `wrap_width` where §4.2 said it would not:
  a host whose field lays text out itself needs them; one that does not
  ignores them. What the field says arrives as `TextEvent`s — `Changed`
  when its buffer, caret or selection differ from the session's,
  `Committed` on focus loss, `TabPressed`/`Cancelled` for the keys a cycle
  editor claims — and they land on the next frame's keys and draft, where
  the tools already read them. `CaretAt` is the front end's click for a
  field that does not lay out: the recorder places it by a glyph hit test
  on the next frame and answers `Recorded::caret_placed`. Reversed on
  `ui-ux-split-completion` (E3): the editor is the front end's and only the
  text comes back.
- **D5 The screen is recorded through the context's fonts.** D1 assumed the
  painter measured through `EpaintLayout`; it measured through the context,
  at the display's resolution, and a recorder measuring at one pixel per
  point would have placed anchored text off by the rounding difference on a
  HiDPI screen. `ContextLayout` is the context's fonts behind `TextLayout`,
  so the recorder and the replay lay out on the same engine at the same
  resolution. `EpaintLayout` stays the exporters' engine; which one the
  `kernel` call takes in phase 6 (the document's text extents want one pixel
  per point, the screen wants the display's) is that phase's to decide.

- **D13 The chrome is drawn from the last call's answer; the glass from
  this one's.** An immediate-mode shell over `kernel()` draws before it can
  call: the docked chrome — the bar, the rail, the navigator, the status
  line — is drawn from the `View` the previous call answered, and what it
  raises goes into this frame's batch, so a click acts in the frame it is
  made, exactly as before. The chrome that floats *over* the picture — the
  selection bar, the palette, the notices — is a child of the canvas's own
  layer and must be painted after the replay, so it is drawn from this
  call's `View` and what it raises is queued for the next frame's batch: a
  frame late, which `click_at`'s settle never sees, and no picture moves.
  Giving that chrome a layer of its own would have taken its widgets out of
  egui's hit-testing, which only sees registered areas. The first frame
  has no previous answer, so it primes one with an empty batch. What the
  chrome needs beyond the models — the viewport, the ground colours,
  writability, the selection count, what landed, the document's renaming
  and home — is on `View` now rather than read off the session. Retired as
  a non-issue on `ui-ux-split-completion` (E7).
- **D14 The tool's own action is the call's; the shell's one act is the
  batch's.** The shell still resolves one act a frame — an object paste
  first, then what an open picker reported, then what the docked chrome
  raised, then a keyboard chord — and puts an edit in the batch or performs
  an effect after the call. The action a tool raises on the canvas is
  dispatched inside the call, as a front end's would be; before, a floating
  chrome's act in the same frame overrode it, which no gesture produces.
- **D15 The call's text engine is the screen's.** `kernel()` is given the
  `ContextLayout` — the context's fonts at the display's resolution, what
  the recorder measured through since phase 1 — so the picture is laid out
  where the replay draws it. One call executes as well as paints, so an
  export taken through the shell is laid out by the same engine: at one
  pixel per point it is byte-identical to before; on a HiDPI display it
  matches the screen rather than a 1-ppp export from another machine.
  Committed text extents were already the screen's (the canvas measured
  them through the painter, then the recorder, at the display's resolution
  since before this branch). The resolution-independent `EpaintLayout` stays
  the exporters' own engine and the kernel tests'; text layout in the core
  (§6) is what removes the choice.
- **D16 The theme is the shell's.** `Appearance` owns the `Theme` and tells
  the session whenever it changes — a host fact, like the sheet and the
  identity, stated ahead of the call — so the chrome draws from the
  appearance's theme and no widget reads the session for a colour. Whether
  the theme crosses the seam at all is open (E6,
  `docs/ui-kernel-split-completion-playbook.md`). Superseded by E6: the
  palette crosses as the command `Action::SetPalette`, not as a host fact.

## Follow-ups (not on this branch)

- Text layout in the core (`CoreLayout`, `Paint::Glyphs`) and the wasm
  facade — `docs/react-frontend-interface.md` §6, §7f–h. Not needed for the
  egui shell, which has epaint — deferred (E4).
- `Handoff::Asset` for image bytes, when a front end without the egui loader
  exists — done on `ui-ux-split-completion` (E2).

## Worklog

Filled in as phases land: phase, commit, what moved, what was untangled,
CI time.

**Phase 1 — the replayer** (2026-09-11). `blockworx_egui::replay` draws a
`&[Paint]` onto an `egui::Painter`, registering an asset's bytes through the
`ImageRegistry` the first time a frame paints it; the row-by-row text drawing
with its baseline correction moved there from the painter unchanged. The egui
`Canvas::paint` and `fit_to` take a `TextLayout` and hand the closure a
`Recording` instead of a `Painter`; the recording is finished, its repaint
request passed to the context, its display list replayed under the clip rect,
and its editor rendered as the `TextEdit` it always was. `Painter` and
`ScriptedInput` are deleted: the headless drivers get `measure::headless(ui,
typeface, palette, easing, pointer)`, a `Recording<ContextLayout>` with the
camera at rest and the pointer scripted, and the tools' `painted_shapes`
became `painted_marks` (it counts the display list, which is what it was
looking at). `Recording` gains `take_edit_text` for the driver that reads the
editor mid-frame, and its text marks now carry the font at screen size (the
rest of a `Paint` already was). `Framed.cursor` is gone — the cursor is a
recorded output — and `Canvas::paint` returns `Painted { value, cursor }`.
One behavior change, listed: the pan cursor (Grab while the pan key is held,
Grabbing during a pan) reaches the pointer again; since `7bb93d7` it had been
set on a painter that was dropped on the next line. The app tests call
`App::show_canvas(ui)`, which builds the `ContextLayout` from the
appearance's typeface the way the frame does. No golden regenerated; `cargo xtask ci` green in 77 s.

**Phase 2 — the chrome model** (2026-09-11). `blockworx_kernel::chrome`
holds the owned models — `TopBar { name, scope, steps, lens, liveness }`,
`Reading { tool, selection, zoom, cursor, title }`, `NavTree`/`NavNode`,
`Overlay { selection, accent, pin_dir }`, `Notice`/`Notices` — and
`Session` builds each (`top_bar`, `reading`, `history_rows`, `nav_tree`,
`overlay`, `notices`, `displayed_tool`); `kernel()` puts them on `View`
after dispatch. What moved down: `Liveness::of`, the selection path and
the title block, the history rows (the store's `Row<'a>` became an owned
`Row` with a `Written` stamp, shared with `blockworx log` and the palette),
the block label and the document walk behind the navigator, the accent
swatch's role and the shared pin direction behind the overlay, the standing
file notices, `written_at`, and the failures the library reported
(`Session.failures`; acknowledging one is `Action::AcknowledgeFailure`).
The shell's widgets take the model beside their own state: `top_bar::Docked
{ model, navigator, theme, prefs, recent, document }`, `NavScene { tree,
theme }`, `Overlay { commands, bar, open_picker, safe, camera, right_click
}`; `HistoryScene` keeps its borrowed rows and the wall clock. The session
is told the sheet every frame (`App::state_the_sheet` in `sync`), so the
bar's name comes off the session rather than being overridden by the
shell. `Library::share_bundle` and `save_projection` lost their `self` and
are associated functions. Behavior changes, listed: the shell now reports
`Sighting.pointer` only while the canvas is hovered — what the field said
it was — so a keyboard zoom with the pointer over the navigator steps
about the viewport's centre rather than the pointer; and the pan cursor
override from phase 1 is unchanged. One kernel test reads the chrome off
`View`; the panel tests build the models. No golden regenerated; `cargo xtask ci` green in 74 s.

**Phase 3 — pointer resolution** (2026-09-11). `blockworx_paint` gains the
raw vocabulary — `Raw::{Moved, Down, Up, Gone, Cancelled, Panning}`,
`Button`, `Keys`, `Input` — and the kernel gains `pointer::Resolver`, the
state machine that turns a frame's raw events into the `Interaction` the
tools already consume: a press within 6 pt and 0.8 s released is a click,
a second within 0.3 s and 6 pt of the last is a double-click, a press that
strays or outlives the click duration is a drag from its own origin,
`Cancelled` ends a drag where it stands and the press drags no more, a
panning frame yields neither drag nor press, and a hover is reported while
the pointer is over the canvas and not gone — egui's own rules, read off
`InputState` and `interact` and written down once. `Session::resolve`
runs it against the camera the session was last shown, dated by the
session's clock, and sets the sighting's pointer from the same pass.
`Event::Pointer` carries a `Raw`, `Event::Keys` the frame's keys; the
kernel tests drive drags as down/moved/up. The egui `Canvas::input()`
reads the raw events off the frame's egui events while the canvas is
hovered or holds a press that began on it (`Gone` otherwise, and a
`Moved` at the hover position when the chrome under a still pointer went
away), `Cancelled` on Escape, `Panning` while the view pans, and the keys
scoped as the shim scoped them, with the editor's feedback OR'd in;
`paint` no longer takes an interaction; `interaction.rs` and
`View.last_mouse_down` are gone. The shell sights the camera before it
resolves the pointer (the frame's own pans are in; the undo entry is
still measured after the pass) and — a fix the resolver forced — ticks the
session every frame in `App::sync`, since a click is dated by the
session's clock and it had stood at zero under the shell; `settle`'s undo
timestamp now reads the same clock. Behavior changes, listed: the spotlight
ring's fade is dated from the real clock rather than zero (the 9j finding);
secondary and middle buttons yield no tool events at all, where before they
yielded drag events the pan gating discarded — the same outcome by a
shorter road. No golden regenerated; `cargo xtask ci` green in 74 s.

**Phase 5 — in-place editing** (2026-09-11). `blockworx_paint::edit`
holds `Editing { id, text, cursor, selection, caret_at }`, `TextEvent` and
`EditField`; `Layout` gains `caret`, `selection` and `hit` over its rows
(which now carry their height), tested on a two-row fixture. `EditText`
sheds the `Rc<RefCell<String>>` it shared with egui and carries the text
it opens on; `Canvas::draft(id)` is how a tool reads the draft back, and
the six editing tools keep a `String` instead of a shared cell and commit
what the draft says. The recorder is handed the session's `Editing` in its
`Frame` and paints the editor at `set_edit_text` (D11); `Session` holds
the edit, follows each frame's request, applies `TextEvent`s to the draft
and lands `Committed`/`Cancelled`/`TabPressed` on the next resolve's keys;
`Event::Text` reaches it in `observe`, and `View.edit_text` is the
`EditField` (with the screen rect the front end's field sits under). The
egui `Canvas::paint` takes the edit state and answers the request in
`Painted.edit_text`; `Canvas::capture` runs the invisible field and
returns the `TextEvent`s (D12), replacing `render_pending_edit` and
`EditorFeedback`; the shell's `show_canvas` follows the request, captures
and hands the events to the session. The tools' headless driver keeps an
`Editing` the way the session does and types by setting it. Behavior
deviations, listed: the editor's look is the core's now — a field in
`EditorFill` with a two-point corner, the draft in `EditorText` (or the
label's own ink inside a block body), a `B0D` wash for the selection, a
two-pixel `B07` caret that does not blink — where egui drew its own
widget; no picture pins an open editor, so no snapshot moved. Everything
else — the block-edit cycle, the rename, text-box and route-label tests,
the label goldens — unchanged in outcome. No golden regenerated; `cargo xtask ci` green in 44 s.

**Phase 6 — `App::update`** (2026-09-12). `App::shell_frame` is one batch,
one `kernel` call, one replay. `sync` tells the session the host's facts —
the theme (D16), the sheet, the projection — and the batch opens with the
tick and whatever the last frame's glass and dialogs queued; the docked
chrome is drawn from the last call's `View` (`take_shown`, primed on the
first frame) and its acts go into the batch beside `Event::Safe`; the
canvas is opened (`Event::Viewport`, the moves, the raw pointer, the keys,
the invisible field's `TextEvent`s over the last answer's `EditField`), the
one act is chosen (D14), the call is made, the picture is replayed under
the ground the answer names, and the glass — the selection bar, the
palette, the notices — is drawn from the fresh answer with its acts queued
(D13). `settle` forwards the repaint, says what landed (`View.landed`) and
delivers the hand-off; `poll` runs the dialogs' doors. `View` gains
`viewport`, `ground`, `writable`, `selected`, `landed`, and `TopBar` gains
`renaming` and `home`; `Overlay` gains the accent index its picker shows
beside the swatch; `NavTree` knows what it hangs from (`Hung`) and carries
each block's path name, and resolves a content path itself (`path_of`), so
the palette's `find`, `expand` and `go` run over the tree and the editor's
document parser is gone. What left the shell: `Surface::show_canvas`,
`sync_camera`'s successors, `confirm_what_landed`, `handle_keyboard`'s
session reads (the registry's `Copy` and `View.writable`/`selected`
instead), the pickers' drawing reads (`Overlay.accent`/`pin_dir`),
`Appearance::layout` (D15), and every session read in the widgets. The
egui `View::begin` answers a value `Canvas` that borrows nothing, so the
frame can call between reading the pointer and painting; `show` paints a
`Diagram` and `capture` runs over an `EditField`; `paint`, `measure` and
`Painted` are gone. Tests: `App::show_canvas` is a `cfg(test)` canvas-only
frame, `dispatch_action` a `cfg(test)` call of its own at one pixel per
point, `hand_off(ctx)` delivers what it answered; no test outcome changed.
The proof, run as `grep -rn session src --include=*.rs` with the tests, the
doors (`library.rs`, `exchange.rs`, `dialogs.rs`), `main.rs` and the two
benches filtered out: every hit is a comment, a panel test's own `Session`
harness type, or `src/app/mod.rs` — the field, `new`, the sheet and title
facts (`window_title`, `document_name`, `title_block`, `state_the_sheet`),
`tell_the_theme`, `restore_preferences`, the two `kernel(&mut
self.session, ..)` calls (`take_shown`, `canvas_close`), `perform`'s
doors, `sync`'s projection refresh, `poll`'s doors and `on_exit`.
`src/surface.rs` names it in one comment. Behavior changes, listed: the
floating chrome's acts land a frame later (D13); a tool's canvas action is
no longer dropped when a floating control is clicked in the same frame
(D14); an export taken on a HiDPI display is laid out at the display's
resolution (D15). No golden regenerated; the seven pictures unchanged;
`cargo xtask ci` green in 54 s.

The branch is complete: the egui shell is a consumer of `View`, and
`blockworx_kernel::kernel` has a real consumer. What follows is the
interface doc's §7f–h — text layout in the core (`CoreLayout`,
`Paint::Glyphs`, which also retires D15's choice), `Handoff::Asset` for a
front end without egui's image loader, and `serde` on `Event` and `View`
once `EditText` and `CommandSet` can carry it (D6).

**Phase 4 — the camera** (2026-09-11). `Session.camera` (`camera::Camera`,
crate-private) holds the vantage, the viewport, the safe region the chrome
leaves, the framing in flight and whether the user worked it this frame;
the framing math — `fit_to_rect`, `fit_content`, `focus_on`,
`bring_into_view`, `zoom_step`, the constants and `overshoot` — moved from
the egui view verbatim, with the view's camera tests beside it, and
`Glide` replaces the view's `Framing`. `Sighting`, `Framing`, `sees`,
`take_framings`, `ask_framing` and the kernel's `apply_framing` are gone:
a framing request applies to the session's own camera when it is made,
and `Refit::Owed` is taken by `fit_content` once the frame has measured
the level. `blockworx_paint` gains `Move` (D9) and `Factor`, the zoom
multiplier a wheel notch or a step carries, so `zoomed_about` takes no
bare float. The kernel takes `Event::Move`, `Event::Viewport` (now applied
in `observe`), `Event::Safe`; `View.vantage` comes out; `Effect::Camera`
is `Action::FrameRect`, applied by dispatch. The egui `View` keeps the rect
allocation, the hover, the gesture reading (`Canvas::moves()`), the
`Camera` stand-down for the overlay, the `TextEdit` capture and the icons;
`Canvas::paint` and the new `Canvas::measure` take the vantage from the
session, and the grid draws under it. The shell's `show_canvas` tells the
session the viewport, applies the frame's moves, takes the owed fit,
resolves the pointer and paints, in that order — the order the old view
kept inside `begin`; `sync_camera`, `apply_framings`, `sighting`, the
`Effect::Camera` arm and `dispatch_action`'s context parameter are gone,
and the app tells the session the safe region where it told the view. The
ease runs in `Session::ticks` (D10); the app-test camera helpers read the
session and the one that settles a framing ticks it. No behavior change
found or introduced: the seven pictures, the settle probes and every
camera test are unchanged in outcome. No golden regenerated; `cargo xtask ci` green in 74 s.
