# The core as seen from a React-style front end

What the boundary between the Rust core and a new UI looks like if the UI is a
React-style tree: components own the view, the core owns the model, and the two
meet at one function. Written against the interfaces as they stand on
`exit-egui` (`docs/exit-egui-playbook.md`, "State at the end of the branch");
§7 lists what has to change in the core before a front end can be built on it.

## 1. The shape in one sentence

```text
view = kernel(&mut session, events, &layout)
```

React holds no document state. The `Session` (in `blockworx-kernel`) is the
single source of truth; the front end sends it events and renders the `View`
it gets back. In React terms the core is the reducer *and* the selectors:
`dispatch(event)` runs the kernel, and the `View` is the derived props tree
for every component on screen. Nothing in the UI computes anything about the
diagram.

Three rules shape everything below:

- **Commands in, nothing asked back.** The core executes what it is sent and
  never asks the front end a question. A flow with substates — a file dialog,
  an image pick, an import — is the front end's from the moment it opens to
  the moment it ends, and the core hears only what it ended in.
- **Results out through one list.** The front end never queries the core. It
  sends a command ("export to PDF"); when the core has the result it puts it
  on `View.handoffs`, and the front end drains it there. One list of one
  enum, so a future kernel can produce a result on a thread without the front
  end changing.
- **The core is called when something happens, not every frame.**
  `View.repaint` says whether another call is owed and when. An idle diagram
  costs nothing.

## 2. Where the boundary sits

The facade is a typed Rust API, in a crate of its own (`blockworx-editor-api`
or the kernel itself), and it is the same API on every platform:

```rust
pub struct Editor { session: Session, layout: CoreLayout }

impl Editor {
    pub fn open(bytes: &[u8]) -> Result<Editor, OpenError>;   // a projection
    pub fn update(&mut self, events: Vec<Event>) -> View;
}
```

That is the whole facade: a constructor and `update`. It is `update`, not
`frame`, because it is called when the front end has something to say or
`View.repaint` has come due — not on a clock. Everything the front end says
is an event, time and the viewport included: there is no clock argument and
no size argument, because a tick and a resize are things that happen.
`Event` and `View` are the typed values the kernel already uses; the facade adds no
vocabulary of its own except `OpenError`, because the front end can hand
`open` any bytes at all — a file that is not a projection, a
projection from a newer build, a truncated download — and each of those is a
reported refusal, never a panic. The same is true of every command: a
command the document cannot carry out is refused and reported, and the
facade stays alive.

Language boundaries are glue around that API, not part of it. For a browser
front end the core compiles to wasm (it already does for the egui web build;
nothing below `src/` names a toolkit or a filesystem except the native-only
store modules) and a `wasm-bindgen` wrapper in `blockworx-web` (de)serializes
at the edge:

```rust
#[wasm_bindgen]
pub struct WebEditor(Editor);

#[wasm_bindgen]
impl WebEditor {
    pub fn open(bytes: &[u8]) -> Result<WebEditor, JsError> {   // a bad file is a JS exception, not a panic
        Ok(WebEditor(Editor::open(bytes)?))
    }
    pub fn update(&mut self, events: JsValue) -> JsValue {
        let events: Vec<Event> = from_value(events)?;          // serde at the edge
        to_value(&self.0.update(events))
    }
}
```

`Event` and `View` cross as plain data (serde; JSON while iterating, a binary
codec once the shape settles). The front end never holds a reference into the
core, and the core never calls into the front end.

The facade is deferred (E4, `docs/ui-kernel-split-completion-playbook.md`):
web support stays with the egui build until the split is done, and the
target front end is a Rust one (dioxus or similar), which needs no
serialization edge. The sketch above is what a JavaScript front end would
need, kept for when one is wanted.

On the desktop the same `Editor` sits behind a webview (Tauri or equivalent)
with the native store behind it, or behind any Rust-native UI directly, with
no glue at all; the API is identical.

## 3. Events: what the front end may say

The kernel's input today is `Event::{Pointer(Interaction), Action(Action),
Command(CommandId), Tick(Tick), Viewport(Rect)}`. A React front end needs the
same families plus two that the egui shell still handles for the core:

| Family | Today | React front end |
|---|---|---|
| Pointer | `Interaction { event: Hover/DragStarted/Dragging/DragStopped/Clicked/DoubleClicked, press, shift, enter/tab/escape/delete_pressed, lost_focus }`, positions in **world** space | Same struct, positions in **screen** space; the core converts through its own `Vantage`. The front end reports raw pointer state and drag/click resolution moves into the core (§7a). |
| Command | `CommandId` (`Arm(tool)`, `Undo`, `Redo`, `Copy`, `Cut`, `Delete`, `ZoomIn/Out`, `FitView`, `Lock`, `AddImage`, `AddIcon`, `Export(fmt)`, `Import`, `Save`, …) | Unchanged. Toolbar, palette and menus send these; keyboard chords map to them through the binding table in `View.commands`. The registry answers *permission and target* only (§5). |
| Action | `Action` — the things the core executes, holding no `Tool`: `Arm(ToolName)`, `ArmAddRouteLabel(RouteId)`, `OpenEditor(EditTarget)`, `Delete`, `Copy`/`Cut`/`Paste`, `SetRole`, `Nudge`, `GoToPath`, `NavSelect`, `Undo`/`Redo`, `ViewRev`/`ViewHead`, `TagRev`, `SetIcon`, `PlaceImage`, `Export`, `SetPalette(Palette)`, … | Unchanged. Panels and overlays send the typed action for what was clicked; the ends of the front end's own flows are actions too (§5). The front end holds the theme and sends `SetPalette` when its appearance changes and ahead of its first picture; its chrome is its own. |
| Tick | `Tick(time)` — the clock, as an event | Sent from `requestAnimationFrame` only while `View.repaint` is `Some`. A batch with no tick means time has not advanced; a tick earlier in a batch dates the events after it. |
| Viewport | `Viewport(Rect)` — a resize, as an event | Sent when the canvas element's size changes. |
| Camera | owned by the egui `View` | New: `Pan(Vec2)`, `ZoomAbout { factor, anchor }`, `Pinch { … }`. The core owns the vantage (§7b). |
| Text edit | an `Rc<RefCell<String>>` shared with egui's widget | `Text(TextEvent)`: `Committed { id, text }`, `Cancelled { id }`, `TabPressed { id, text }` — from the front end's own visible editor, which keeps the draft, caret and selection; the core hears only how the edit ended (§4.2). |

Events are batched per call: everything that happened since the last
`update()` goes in one `Vec<Event>` in the order it happened.

## 4. View: what the front end must show

`View` is what the egui shell draws from today (`blockworx_kernel::View`);
the sketch, with the fields the shell needed beyond the models:

```rust
pub struct View {
    // the canvas
    pub draw_list: DrawList,            // §4.1
    pub cursor: Option<Cursor>,         // -> CSS cursor
    pub edit_text: Option<EditField>,   // §4.2: the editor the front end runs, and where
    pub selection_bounds: Option<Rect>, // screen rect the overlay anchors on
    pub vantage: Vantage,               // zoom + translation, for scrollbars, a minimap
    pub viewport: Rect,                 // the screen rect the canvas was laid out in
    pub ground: Ground,                 // the canvas background and grid colours

    // the chrome model (blockworx_kernel::chrome)
    pub commands: CommandSet,           // every command: enabled, precedence, and its Act (§5)
    pub writable: Writability,          // what a front end's own chords (paste, nudge) gate on
    pub selected: usize,
    pub tool: ToolName,                 // which rail button is lit
    pub top_bar: TopBar,                // scope path, lens (head / past rev), document name + liveness, undo/redo labels
    pub status: Reading,                // the status line's fields
    pub history: Vec<Row>,              // rows for the history panel and the palette
    pub nav_tree: NavTree,              // the navigator's tree, which also resolves a content path
    pub overlay: Option<Overlay>,       // the selection bar's contents and its anchor
    pub notices: Vec<Notice>,
    pub landed: Option<String>,         // what this call wrote, for the status line's confirmation
    pub title: String,                  // window title

    // results
    pub handoffs: Vec<Handoff>,         // §5c
    pub repaint: Option<Duration>,      // call again after this long (None: only on input)
}
```

Every field is owned data. The chrome model types live in
`blockworx_kernel::chrome` (`TopBar`, `Reading`, `NavTree`, `Overlay`,
`Notice`; the history rows are the store's own `Row`), built by the session
and consumed by the egui shell's widgets beside their own chrome state. They
derive `serde` with the rest of `View` (§7g, E8).

### 4.1 The display list

`DrawOp` is the recorder's vocabulary, already toolkit-neutral, and a `DrawList` is
the frame's `Vec<DrawOp>`:

```text
Rect { rect, rounding, fill, stroke }
LineSegment { points: [Pos2; 2], stroke }
Line { points: Vec<Pos2>, stroke }
Circle { center, radius, fill, stroke }
ConvexPolygon { points, fill, stroke }
Text { rect, anchor, text, font, color }              -> becomes Glyphs (§6)
RotatedText { pos, anchor, text, font, color, angle } -> becomes Glyphs
TextWrapped { rect, anchor, text, font, color, max_width } -> becomes Glyphs
Image { rect, asset }                                 -> asset is a content hash (§4.3)
```

Coordinates are **screen space** (the recorder applies the vantage) and a
text mark's `font` is at the size it is drawn, so the front end's canvas
component is a straight replay — `blockworx_egui::replay` is that component
for the egui shell, and the shell's pictures are the proof it is enough:

```ts
for (const op of view.draw_list) switch (op.kind) {
  case "Rect":    ctx.roundRect(...); ctx.fill(); ctx.stroke(); break;
  case "Line":    ctx.beginPath(); ...; ctx.stroke(); break;
  case "Glyphs":  for (const g of op.glyphs) ctx.fillText(g.ch, g.x, g.y); break;
  case "Image":   ctx.drawImage(bitmaps.get(op.hash), ...); break;
}
```

A 2D canvas is enough for the diagram's scale. The same list feeds an SVG
renderer for tests, and a WebGL path later if profiling asks for it.

### 4.2 The in-place editor: the front end's

Reversed on `ui-ux-split-completion` (E3). This section used to make the
editor invisible and have the core draw the draft, caret and selection from
its own layout, so that nothing the browser wrapped was ever shown. The
price was the core owning a cursor, a selection, a glyph hit test and every
keystroke, for a guarantee that turned out not to be worth it: the core
rotates and wraps text when it commits anyway, so an editor that lays text
out a little differently only differs for as long as the edit is open. That
disparity is accepted.

So the editor is the front end's, and it is visible. The core asks for one
in `View.edit_text`:

```rust
pub struct EditField {
    pub id: EditId,
    pub rect: Rect,               // screen space
    pub angle: Angle,             // about the rect's centre
    pub font: Font,               // world size; scale by the vantage
    pub align: Align2,
    pub wrap_width: Option<WorldPx>,
    pub text: String,             // the text as it stands
    pub multiline: bool,
    pub char_limit: Option<usize>,
    pub tab_cycle: bool,          // Tab and Escape belong to the editor
    pub select_all_on_focus: bool,
    pub hint: Option<&'static str>,
    pub colors: EditColors,       // resolved: text, background, caret, selection, hint
}
```

The front end runs its own editor over that rect, with its own caret, blink,
IME, clipboard and accessibility, and says only how the edit ended:

```rust
Event::Text(TextEvent::Committed { id, text })   // Enter (single-line) or blur
Event::Text(TextEvent::Cancelled { id })         // Escape
Event::Text(TextEvent::TabPressed { id, text })  // commit, then step the block-edit cycle
```

The core holds no draft. The outcome lands on the next frame's
`Interaction.text` as a `TextOutcome` for the tool that opened the field,
which commits through the same op builders it always did. The core keeps
painting the committed text, and the editor covers it. A front end that
cannot rotate a field edits a rotated label upright. The egui one does
this, since a `TextEdit` does not rotate. A text box wraps where the editor
wraps until it commits, and then where the core wraps.

### 4.3 Images

`DrawOp::Image` names an asset by its content hash. Assets are immutable by
construction, so the front end keeps a `Map<hash, ImageBitmap>` and the core
delivers bytes the same way it delivers every other result: the first frame
that paints a hash the session has not yet sent, the `Asset` rides in
`View.handoffs` as `Handoff::Asset { hash, asset }` (§5c), decoded once and
cached forever. The `Asset` enum is the format (`Svg` or `Png`), which both
sides must know — the core to measure and export, the front end to decode.
The session keeps the set of hashes it has sent, so a hash crosses once; a
new session (a document opened) starts with an empty set and sends again,
and registration on the far side is idempotent. No query.

## 5. Commands in, the front end's own effects, results out

### 5a. Commands in

Everything the front end has to say is an event (§3), and the end of one of
its own flows is no different from a toolbar click:

```rust
Action::SetIcon { block: BlockId, asset: Asset }
Action::PlaceImage { asset: Asset }          // the core chooses where (§5b)
Action::Import { name: String, bytes: Vec<u8> }
Action::Export { format, selection }         // the result comes back in the slot (§5c)
```

Each one carries everything needed to execute it. A cancel is not one of
these: it is a substate of the front end's dialog, and what it means is the
front end's to decide — leave the tool where it stood, or arm another one,
which is an `Arm` like any other. An asset the document will not carry
(over the limit, not an image) is refused by the core the way any command can
fail, which is a report, not a question.

### 5b. The front end's own effects

What a command is, and what it is offered on, is the registry's — and what
invoking it does is one of two things:

```rust
pub enum Act {
    Edit(Action),     // the core executes it
    Effect(Effect),   // the front end performs it
}

pub enum Effect {
    AddIcon(BlockId),         // pick artwork for this block's icon
    AddImage,                 // pick artwork to place as an image
    Accent(RoleTarget),       // open the accent picker on this target
    PinType(Vec<PinId>),      // open the I/O picker over these pins
    Import,                   // pick a PNG or SVG to import
    SaveProjection,
    Camera(Rect),
    NewDocument, RenameDocument(String), PickFile(FileRequest), OpenRecent(PathBuf),
}
```

The registry decides *permission* — whether Add-icon is offered, and for
which block — and hands over the target and nothing else. Which dialog opens,
how many steps it takes, whether the user backs out, and what a cancel means
are the front end's alone and invisible from below. When a flow ends in
something the document should carry, that arrives as an ordinary command
(§5a). Where a picked image *goes* is the core's, not the picker's: it lands
centred on the point a paste lands on, at the artwork's own aspect, with the
resize selection armed so the user sizes it next.

The image drag-box tool was retired for this reason: it was the one case in
which a canvas gesture decided a placement and the core had to tell the front
end "an image is wanted here". Pick-then-place needs no channel out of the
core at all.

### 5c. Results out: one list

The front end never queries the core. A command that produces something the
front end must hand to the platform — bytes to download, text for the system
clipboard, an image to decode — is executed like any other, and its result is
put on the one pass-back list:

```rust
pub enum Handoff {
    Export { content: ExportContent, name: String },   // SVG / PNG / PDF bytes to save or download
    Clipboard(String),                                 // JSON to put on the system clipboard
    Asset { hash: AssetHash, asset: Asset },           // artwork the display list named, with its format (§4.3)
}
// on View:
pub handoffs: Vec<Handoff>,
```

The front end drains the list every frame and acts on each entry in order.
A list rather than a slot because one frame can owe several results — a
document that opens on five images hands all five out at once. A kernel that
computes an export on a thread adds it when the work lands, and the front
end's poll is unchanged. Nothing about this is asynchronous *to the front
end*: it is a list that is sometimes non-empty.

## 6. Text: the one hard part

Retired on `ui-ux-split-completion` (E3). With the editor the front end's
(§4.2), nothing needs glyph positions from the core. The core measures
committed text (label hit tests, text-box extents, route-label placement)
through the `TextLayout` the front end supplies, and a Rust front end
supplies one over its own text engine. Exports go through `EpaintLayout`, so
an export matches itself on every platform. D15 stands, and `CoreLayout` and
`DrawOp::Glyphs` are not planned.

## 7. What the core must change first, in order

Each item is small and independently testable against the existing kernel
suite; together they are the next branch. The list is the "Kernel: what still
lives in the shell" list of the playbook, resolved into changes.

- **a. Pointer resolution into the core.** `compute_interaction` (the egui
  shim that turns a pointer response into hover/drag/click) moves into the
  kernel as a small state machine over raw `PointerMoved/Down/Up` in screen
  space, with the drag threshold and double-click window as constants. The
  front end sends raw pointer state; the core produces the `Interaction` it
  already consumes.
- **b. Camera into the session.** `Vantage` is already paint's and the
  session already mirrors it (`Sighting`) and asks for moves (`Framing`).
  Invert that: the session owns the vantage, `Pan`/`ZoomAbout`/`Pinch`/
  `Viewport` are events, framing animations run on the core's easing table,
  and the recorder reads the vantage from the session. `Effect::Camera` then
  becomes an `Action`. The egui `View` consumes `View.vantage` during the
  transition.
- **c. Text editing into the core** (§4.2): reversed on
  `ui-ux-split-completion` (E3). The editor is the front end's, and only the
  text comes back.
- **d. The chrome model into `View`.** Make `TopBar`, `Reading`,
  `HistoryScene`, `NavScene`, `Overlay`, `Notice` owned and `serde`; build
  them in the kernel from the session (the code that builds them is in the
  shell's `Surface`/`Library` today and moves down). The egui shell then reads
  them from `View`, which is the point at which it becomes swappable.
- **e. Done on this branch.** The registry answers permission and target
  (`Act::Edit` vs `Act::Effect`); the shell performs its effects behind a
  `Dialogs` seam that tests script; the ends of flows are commands; export is
  a command whose bytes come back in `View.handoffs`; `Session::dispatch`
  returns nothing. `Handoff::Asset` (§4.3) landed on `ui-ux-split-completion`
  (E2).
- **f. Text layout in the core** (§6): retired on `ui-ux-split-completion`
  (E3).
- **g. `serde` on `Event` and `View`**, and the `blockworx-web` facade with
  `wasm-bindgen`. The serde half landed on `ui-ux-split-completion`: `Action`
  holds no `Tool` (E1) — a tool the front end asks for is named, by
  `Arm(ToolName)`, `ArmAddRouteLabel(RouteId)` or `OpenEditor(EditTarget)`,
  and built at dispatch, while a tool's own hand-off to the next seeded tool
  is a `Transition` that never leaves the call — and `Event`, `View` and
  everything they carry derive `Serialize`/`Deserialize`, with round-trip
  tests over a real session (E8). The wasm facade half stays deferred (E4).
- **h. Persistence on the web.** `Doc::Scratch` runs in the browser today
  with nothing durable behind it. The store's row types and rev encoding are
  target-independent; an OPFS-backed container is a `Store` impl, not a
  format change.

g's facade and h are deferred (E4): web support stays with the egui build,
and a Rust front end (dioxus or similar) calls `kernel()` directly with no
`wasm-bindgen` edge; the seam serializes regardless (E8).

a–e are done, on the `exit-egui` and `shell-on-kernel` branches: the egui
shell consumes `View` through one `kernel()` call a frame
(`docs/shell-on-kernel-playbook.md`), and the kernel is the only thing that
reads `Session` beside the shell's own file doors. After f–h a React front
end can be written against the facade with no further core change. The two shells can coexist through the
transition, since both are consumers of the same `View`.

## 8. The React side, sketched

```tsx
function Editor({ editor }: { editor: WasmEditor }) {
  const [view, dispatch] = useKernel(editor);   // batches events (Tick and Viewport included), calls update(), schedules the next Tick from view.repaint
  usePerform(view.commands, dispatch);          // performs Act::Effect: file inputs, pickers, then dispatches the resulting command
  useHandoffs(view.handoffs);                   // downloads, clipboard, image cache
  return (
    <Shell title={view.title}>
      <TopBar model={view.top_bar} onCommand={c => dispatch({ Command: c })} />
      <ToolRail commands={view.commands} tool={view.tool} onCommand=… />
      <Canvas drawList={view.draw_list} cursor={view.cursor}
              onPointer={p => dispatch({ Pointer: p })}
              onWheel={w => dispatch({ Camera: … })} />
      {view.edit_text && <HiddenInput edit={view.edit_text} onChange=… />}   // captures keys/IME; the text itself is in view.draw_list
      {view.overlay && <SelectionOverlay model={view.overlay} onAction=… />}
      <Navigator tree={view.nav_tree} onAction=… />
      <HistoryPanel rows={view.history} onAction=… />
      <StatusLine reading={view.status} />
      <Notices items={view.notices} />
    </Shell>
  );
}
```

`useKernel` is the whole integration: an event queue, one `update()` call
*when there is something to say* (or when `view.repaint` comes due), and `setView` with the
result. `usePerform` is the front end's dialog state: when a command whose
`Act` is an `Effect` is invoked, it opens the input or picker, holds every
substate itself, and on completion dispatches the resulting `Action` (or
nothing, on cancel). `useHandoffs` drains the list. Components are pure
functions of `view`. Keyboard handling is a `keydown` listener that looks the
chord up in `view.commands` and dispatches the `CommandId`, skipping when a
text field has focus.

## 9. What does not change

The document model, the store's on-disk format, the router, the op emitters,
the `Drawing` waist, the tools, the undo stack, the export: none of them know
this document exists. The kernel tests that drive a tool through
`Event`s and read the result off `View.draw_list` and `View.handoffs` are the
tests a React shell is built against, and they run with no browser.
