From c3d588f03d979debb79855f88ddc51e579f3d047 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 12 Oct 2026 19:35:39 +0200 Subject: [PATCH] fix: the math editor's body was a child of the window, not of the box above it Moving the editor's frame into math-editor.ui left `math_body` and `math_actions` as 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 , 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. --- docs/claude/invariants-gtk-and-chrome.md | 11 +++++++++ docs/claude/invariants-math-channels.md | 7 +++++- src/ui/math-editor.ui | 4 ++-- src/ui_probe.cpp | 29 ++++++++++++++++++++++++ 4 files changed, 48 insertions(+), 3 deletions(-) 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 @@ - - horizontal @@ -80,5 +78,7 @@ + + 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;