diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md
index 7f2d12b..61d74cf 100644
--- a/docs/claude/invariants-gtk-and-chrome.md
+++ b/docs/claude/invariants-gtk-and-chrome.md
@@ -335,3 +335,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
reads the *reverse* of the file: `header-bar.ui` declares menu, sidebar, math, measures, cursors and the
screen shows cursors, measures, math, sidebar, menu. The three comments this replaced each had it wrong
once, which is why the order is the file's now and a byte-for-byte diff of the probe is the check.
+
+- **A widget can end up in the wrong container, and only a *structural* read notices.** `math_body` was
+ written as a `` of the *window* instead of the box above it when the math editor's frame moved into a
+ `.ui`. A `GtkWindow` takes exactly one default ``, so nothing failed: the body -- with the canvas and
+ the parameter form inside it -- was left parentless, `visible` but never `mapped`, size `0x0`, drawing
+ nothing. The symptom was "the editor opens and the canvas is blank", with the window sized around its
+ remaining children. `check_ui` passes such a file; `xml-stripblanks` passes it; so does every probe that
+ reads *values* off the widgets inside. What catches it is a reading that walks *up* from the widget
+ (`in_dialog=1`) or asks whether it was ever given an allocation (`mapped=1`, `canvas=420x836`). When a frame
+ moves into a file, the probe that proves it has to read the frame's *structure*, not only the numbers it
+ holds.
diff --git a/docs/claude/invariants-math-channels.md b/docs/claude/invariants-math-channels.md
index 24d61c3..453346e 100644
--- a/docs/claude/invariants-math-channels.md
+++ b/docs/claude/invariants-math-channels.md
@@ -41,7 +41,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
anywhere in 78--101 ns/slot is one reading of the same thing.
The probe also asserts the new editor mechanics. `dialog build:` reads the identity row back as well --
`name='math3' unit=''` -- which is the pair the `.ui` holds the widgets for and the code fills from the
-channel being edited, so a row that stopped being the one the code writes shows there. Each line carries
+channel being edited, so a row that stopped being the one the code writes shows there.
+`editor frame:` is the same idea one level down: it reads the canvas's allocation, whether that canvas is
+*mapped*, and whether the canvas and the name entry are inside the dialog at all -- delayed by 500 ms, because
+an allocation only exists after a layout. A canvas that is visible but parentless never maps and stays `0x0`,
+and reading values off the widgets *inside* the editor cannot see that. It is the reading that catches
+`math_body` written as a child of the window instead of the box above it. Each line carries
`(want ...)` and the reading
taken from a real run is the reference:
* `[math-probe] hit: inside node=0 which=-1, past-edge node=0 which=-1 (want 0 -1 0 -1)` — the
diff --git a/src/ui/math-editor.ui b/src/ui/math-editor.ui
index a74db9d..bf6b82f 100644
--- a/src/ui/math-editor.ui
+++ b/src/ui/math-editor.ui
@@ -46,8 +46,6 @@
-
-
+
+
diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp
index 4a52180..2da9e38 100644
--- a/src/ui_probe.cpp
+++ b/src/ui_probe.cpp
@@ -386,6 +386,35 @@ gboolean math_probe(gpointer user) {
if (ed->gain != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->gain), 2.0);
if (ed->offset != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->offset), 0.0);
on_math_param_changed(nullptr, ed);
+ // The frame, structurally: the canvas being *in* the dialog and given an allocation. A `.ui`
+ // mistake this has already paid for once -- `math_body` nested under the window instead of the
+ // outer box -- leaves the canvas visible but parentless: it never maps, stays 0x0 and draws
+ // nothing, which a user reads as "the editor opens and the canvas is blank". The values below
+ // cannot see that, and the reading is delayed because an allocation only exists after a layout.
+ g_timeout_add(500, +[](gpointer u) -> gboolean {
+ App* app = static_cast(u);
+ if (app->math_dialog == nullptr) {
+ std::fprintf(stderr, "[math-probe] editor frame: (dialog closed before the reading)\n");
+ return G_SOURCE_REMOVE;
+ }
+ MathDialog* m = static_cast(
+ g_object_get_data(G_OBJECT(app->math_dialog), "udpscope-math-dialog"));
+ if (m == nullptr) return G_SOURCE_REMOVE;
+ const auto in_dialog = [app](GtkWidget* w) {
+ for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) {
+ if (p == app->math_dialog) return 1;
+ }
+ return 0;
+ };
+ std::fprintf(stderr,
+ "[math-probe] editor frame: canvas=%dx%d mapped=%d in_dialog=%d "
+ "name_in_dialog=%d\n",
+ m->canvas != nullptr ? gtk_widget_get_width(m->canvas) : -1,
+ m->canvas != nullptr ? gtk_widget_get_height(m->canvas) : -1,
+ m->canvas != nullptr ? gtk_widget_get_mapped(m->canvas) : -1,
+ in_dialog(m->canvas), in_dialog(m->name));
+ return G_SOURCE_REMOVE;
+ }, a);
const std::size_t built = ed->nodes.size();
const double gain_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().gain;
const double offset_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().offset;