Moving the editor's frame into math-editor.ui left `math_body` and `math_actions` as
<child> elements of the *window* rather than of `math_outer`, because those edits were
anchored on the file's tail and closed the outer box two elements early. A GtkWindow
takes exactly one default <child>, so nothing failed loudly: the body -- with the canvas
and the parameter form inside it -- was left parentless, visible but never mapped, 0x0,
drawing nothing. Two symptoms, one cause: the canvas is blank, and the window is sized
around the head row and the buttons alone.
Measured against the pre-move revision in a worktree, same probe, same environment:
before this fix `canvas=0x0 mapped=0 window=756x940`; after it `canvas=420x836 mapped=1
window=938x940`, which is the old revision's own reading exactly.
It survived because that slice's probe read the editor's *values* (nodes, gain, output,
name, unit) and nothing about its structure. Every other panel moved that day has a
structural reading -- `controls_inside=13/13`, `nested=1`, `labels=2 list=1` -- and none
of them was affected. `[math-probe] editor frame:` is that reading now: the canvas's
allocation, its mapped state, and whether the canvas and the name entry are inside the
dialog at all, delayed 500 ms because an allocation only exists after a layout. In the
broken build it read `0x0 mapped=0 in_dialog=0`.
check_ui passes both files: the XML was valid and every property type-checked.
The second half of this plan row, and the half of that dialog which is template.
src/ui/math-editor.ui holds the window (its size and modality), the box around it,
the identity row -- the name and the unit -- and the body with the canvas in it.
What stays in code is the drawing: the canvas is a GtkDrawingArea the file *declares*
and the code draws, with the gestures that make it a graph editor, and the parameter
form follows which node is selected. The palette and the buttons go next.
The builder is held on `App` and cleared on the *next* opening rather than with the
window: the window belongs to the builder's object list, so freeing the builder under a
live window frees the window with it. That is the source form's pattern, and the probe's
close-then-reopen path is what exercises it -- `reopened: dialog=ok, nodes=2`.
The identity row is read back too, since a `.ui` can hold the widget but not the value:
`dialog build: ... name='math3' unit='' ...`, the entries having been filled from the
channel being edited. check_ui is at 12 ok, 0 failed.
`math_dialog_sync_form()` wrote the label and then moved a node whose source
names an unacquired channel -- a mutation that changes what
`math_dialog_problem()` reports. With the sync re-entry now guarded, the nested
call that used to repair the label is suppressed, so the line kept describing a
source that had just been fixed and left `Apply` insensitive until the next
mutation. The reconcile moves above the write and the dropdown reuses its list.
A review of the whole node editor. The hit test measured the distance to the
24 curve samples rather than to the segments between them, so a wire spanning
several rows was unpickable between two samples -- the probe now places its
boxes 280 units apart and prints the nearest sample's own 8.4 against a 3.0
tolerance, which the point test would have missed. A refused connection said
nothing: the message is now carried in `MathDialog::refusal` and shown once by
the single writer of the label, and a self-wire is refused through the real
drag-end path instead of only through `math_connect()`.
Two defects were found only by the new assertions. `math_dialog_sync()`
re-entered itself -- `gtk_drop_down_set_selected()` emits `notify::selected`
for a programmatic set too, and the handler answers by syncing again, which
consumed the refusal before it could be drawn -- so the body is now guarded
one level deep. And a deleted node left `selected_wire` naming an index that
afterwards belongs to a different wire, so `Delete` cleared an input on the
wrong node.
A node is placed in the first free grid cell rather than at the end of the
list's own cell, so a node added after a delete no longer lands on top of a
survivor; the label ellipsizes with a tooltip rather than wrapping, because a
wrapped line makes the dialog's height depend on the text in it.
Each invariant records what went wrong rather than only the rule: a port hit
that lost its node, a hit test at the identity view that asserts nothing, a
`Delete` that must not fall through to the selected node, and a column pitch
too narrow for a wire's own tread. The warning baseline drops to four.
The file was 1967 lines and is auto-loaded into every context, so every
session paid for the whole of it to reach one invariant. It now carries the
project's goal, the three facts that are always relevant, and a table of
contents; the body is fourteen files under docs/claude/ reached by link
rather than by import, so nothing is loaded until it is asked for.
The content is moved verbatim -- the only edits are each file's own header
and the cross-references that became cross-file ("see the invariant below"
is now a link to the file that owns it).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>