diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index 19109e6..81ad761 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -316,3 +316,11 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear the bare `FAIL` line is that mistake and nothing else. The house style for a dash in prose is the em dash, which is why no other `.ui` in `src/ui/` contains a double hyphen. It has cost two slices a debugging round for one character. + +- **A `.ui` file instantiates its objects once, so anything whose *count* is data cannot live in one.** The + layout editor's overlay buttons are one per row, plus one per boundary between them: a `GtkBuilder` would + hand back the first and nothing for the rest, and there is no "prototype the code copies" in the format. + They are therefore code, and what they share -- the icon, the two CSS classes that make a small round + on-screen-display control, and the size -- is one function (`layout_overlay_button()`) rather than three + copies. The same reason keeps the math editor's palette and parameter rows in code: their count follows the + model, not the file. `.ui` files are for the frame, and the frame is what has no count. diff --git a/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md b/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md index bf4b5e9..65646b2 100644 --- a/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md +++ b/docs/superpowers/plans/2026-10-11-ui-files-and-gtk4-practice.md @@ -74,6 +74,12 @@ needs a probe that proves it *loads* and that its contents match whatever drives | `src/ui/ui_layout.cpp` (~20 kB) | the layout editor's overlay buttons, which are positioned by code | only the buttons are template | | `build_toolbar` (`src/ui.cpp`, ~212 lines) | the header bar's layout, with every button still wired in code | delicate: the `[udpscope] header:` probe reads its geometry | +**The layout editor's buttons are not a `.ui` row, and the note above is wrong about them.** Their count *is* +the row count (one per row, one per boundary), and `GtkBuilder` instantiates a file's objects once, so a file +can hold neither the number nor a prototype for the code to copy. What was real in that row was the +duplication: the shared look is now one function (`layout_overlay_button()`), and the layout probe reads +byte-identical before and after. See `invariants-gtk-and-chrome.md`. + **Done since, one row per commit:** the source chooser's window (`0a64e9e`), the measurements panel (`d344361`, `07fcf61`), the trigger panel (`1ccc70c`), the signals panel (`c37fbdd`), the math panel (`90a17eb`) and the math editor's window, identity row and canvas. What is left of that row is the palette diff --git a/src/ui.cpp b/src/ui.cpp index 763c9de..20e4b4d 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -699,6 +699,28 @@ void on_layout_drag_end(GtkGestureDrag*, double, double, gpointer user) { /// Builds one `×` per row and one `+` per boundary, or takes them all off again when the /// editor is switched off. +/// One of the layout editor's overlay buttons: the look they all share is *here*, once. It was written out +/// three times -- the icon, the two CSS classes that make a small round on-screen-display control, and the +/// size -- and the fourth copy would have been a button added next to them. +/// +/// These buttons cannot be a `.ui` file, and the reason is worth stating because the plan expected them to +/// be: their *number* is the row count, one per row and one more than that for the boundaries, and a +/// `GtkBuilder` instantiates a file's objects once. This function reappears whenever a `.ui` is asked to hold +/// something whose count is data. +/// +/// What each button does with the look is the caller's: they differ in alignment, sensitivity, the data they +/// carry and the gesture they take. +GtkWidget* layout_overlay_button(App& app, const char* icon, const char* tooltip) { + GtkWidget* b = gtk_button_new_from_icon_name(icon); + gtk_widget_set_tooltip_text(b, tooltip); + gtk_widget_add_css_class(b, "circular"); + gtk_widget_add_css_class(b, "osd"); + gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx); + gtk_overlay_add_overlay(GTK_OVERLAY(app.area_overlay), b); + app.layout_buttons.push_back(b); + return b; +} + void rebuild_layout_editor(App& app) { clear_layout_editor(app); if (!app.layout_editing || app.area_overlay == nullptr) return; @@ -706,48 +728,34 @@ void rebuild_layout_editor(App& app) { app.layout_buttons_rows = rows; for (int i = 0; i < rows; ++i) { - GtkWidget* b = gtk_button_new_from_icon_name("window-close-symbolic"); - gtk_widget_set_tooltip_text(b, "remove this row"); - gtk_widget_add_css_class(b, "circular"); - gtk_widget_add_css_class(b, "osd"); - gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx); + GtkWidget* b = layout_overlay_button(app, "window-close-symbolic", "remove this row"); gtk_widget_set_halign(b, GTK_ALIGN_START); gtk_widget_set_valign(b, GTK_ALIGN_START); gtk_widget_set_sensitive(b, rows > 1); set_row_key(b, i); g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_remove), &app); - gtk_overlay_add_overlay(GTK_OVERLAY(app.area_overlay), b); - app.layout_buttons.push_back(b); } for (int b_i = 0; b_i <= rows; ++b_i) { - GtkWidget* b = gtk_button_new_from_icon_name("list-add-symbolic"); - gtk_widget_set_tooltip_text(b, b_i == 0 ? "add a row above" - : b_i == rows ? "add a row below" - : "add a row here"); - gtk_widget_add_css_class(b, "circular"); - gtk_widget_add_css_class(b, "osd"); - gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx); + GtkWidget* b = layout_overlay_button(app, "list-add-symbolic", + b_i == 0 ? "add a row above" + : b_i == rows ? "add a row below" + : "add a row here"); gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_valign(b, GTK_ALIGN_START); gtk_widget_set_sensitive(b, rows < RowSet::kMaxRows); set_row_key(b, b_i); g_object_set_data(G_OBJECT(b), kLayoutKindKey, b); g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_insert), &app); - gtk_overlay_add_overlay(GTK_OVERLAY(app.area_overlay), b); - app.layout_buttons.push_back(b); } // ... and one arrow per boundary whose *only* job is resizing the two rows it sits between. A widget // of its own rather than a second gesture on the `+`, so each handle does one thing -- and a handle // that cannot be clicked cannot be clicked by accident. The figure's own edges are not boundaries // (there is no pair of rows to share between them), so the first and last are inert. for (int b_i = 0; b_i <= rows; ++b_i) { - GtkWidget* b = gtk_button_new_from_icon_name("udpscope-resize-rows-symbolic"); - gtk_widget_set_tooltip_text(b, b_i == 0 ? "drag to resize the first row" - : b_i == rows ? "drag to resize the last row" - : "drag to resize the two rows here"); - gtk_widget_add_css_class(b, "circular"); - gtk_widget_add_css_class(b, "osd"); - gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx); + GtkWidget* b = layout_overlay_button(app, "udpscope-resize-rows-symbolic", + b_i == 0 ? "drag to resize the first row" + : b_i == rows ? "drag to resize the last row" + : "drag to resize the two rows here"); gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_valign(b, GTK_ALIGN_START); // Hidden, not disabled, where it has no pair to share between: the figure's own edges. A greyed @@ -766,8 +774,6 @@ void rebuild_layout_editor(App& app) { g_signal_connect(drag, "drag-update", G_CALLBACK(on_layout_drag_update), &app); g_signal_connect(drag, "drag-end", G_CALLBACK(on_layout_drag_end), &app); gtk_widget_add_controller(b, GTK_EVENT_CONTROLLER(drag)); - gtk_overlay_add_overlay(GTK_OVERLAY(app.area_overlay), b); - app.layout_buttons.push_back(b); } // The editor's way out, in the area's top-right corner: the toolbar's toggle is in the