ui: the layout editor's buttons keep their look in one place, closing that plan row

The row asked for these buttons in a .ui file and that is not possible: there is one per
row and one more than that per boundary, and a GtkBuilder instantiates a file's objects
once, so the file can hold neither the count nor a prototype for the code to copy. The
plan's note ("only the buttons are template") is wrong for the same reason, and the plan
document now says so.

What was real in the row was the duplication: the look was written out three times -- the
icon, the two CSS classes that make a small round on-screen-display control, the size,
the overlay append and the bookkeeping -- and is now one function,
`layout_overlay_button()`. Each loop keeps only what makes it different: its alignment,
its sensitivity, the data it carries and, for the resize handle, its drag gesture.

The layout probe is the check and its readings are byte-identical before and after, all
seven lines: `buttons=5 done=1 done_box=[680,4,72x42] close=[4] add=[0,832] resize=[(1:-)]`
and the row geometry through add-above, remove, add-at-end and Editor off.
This commit is contained in:
Martino Ferrari committed 2026-10-12 16:30:00 +02:00
1 parent 148d1168d6
commit c94bc4316c
3 files changed
+45 -25

No files matched your search

+8
View File
@@ -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, 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 which is why no other `.ui` in `src/ui/` contains a double hyphen. It has cost two slices a debugging
round for one character. 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.
@@ -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 | | `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 | | `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 **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 (`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 (`90a17eb`) and the math editor's window, identity row and canvas. What is left of that row is the palette
+31 -25
View File
@@ -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 /// Builds one `×` per row and one `+` per boundary, or takes them all off again when the
/// editor is switched off. /// 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) { void rebuild_layout_editor(App& app) {
clear_layout_editor(app); clear_layout_editor(app);
if (!app.layout_editing || app.area_overlay == nullptr) return; if (!app.layout_editing || app.area_overlay == nullptr) return;
@@ -706,48 +728,34 @@ void rebuild_layout_editor(App& app) {
app.layout_buttons_rows = rows; app.layout_buttons_rows = rows;
for (int i = 0; i < rows; ++i) { for (int i = 0; i < rows; ++i) {
GtkWidget* b = gtk_button_new_from_icon_name("window-close-symbolic"); GtkWidget* b = layout_overlay_button(app, "window-close-symbolic", "remove this row");
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);
gtk_widget_set_halign(b, GTK_ALIGN_START); gtk_widget_set_halign(b, GTK_ALIGN_START);
gtk_widget_set_valign(b, GTK_ALIGN_START); gtk_widget_set_valign(b, GTK_ALIGN_START);
gtk_widget_set_sensitive(b, rows > 1); gtk_widget_set_sensitive(b, rows > 1);
set_row_key(b, i); set_row_key(b, i);
g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_remove), &app); 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) { for (int b_i = 0; b_i <= rows; ++b_i) {
GtkWidget* b = gtk_button_new_from_icon_name("list-add-symbolic"); GtkWidget* b = layout_overlay_button(app, "list-add-symbolic",
gtk_widget_set_tooltip_text(b, b_i == 0 ? "add a row above" b_i == 0 ? "add a row above"
: b_i == rows ? "add a row below" : b_i == rows ? "add a row below"
: "add a row here"); : "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);
gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_halign(b, GTK_ALIGN_CENTER);
gtk_widget_set_valign(b, GTK_ALIGN_START); gtk_widget_set_valign(b, GTK_ALIGN_START);
gtk_widget_set_sensitive(b, rows < RowSet::kMaxRows); gtk_widget_set_sensitive(b, rows < RowSet::kMaxRows);
set_row_key(b, b_i); set_row_key(b, b_i);
g_object_set_data(G_OBJECT(b), kLayoutKindKey, b); g_object_set_data(G_OBJECT(b), kLayoutKindKey, b);
g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_insert), &app); 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 // ... 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 // 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 // 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. // (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) { for (int b_i = 0; b_i <= rows; ++b_i) {
GtkWidget* b = gtk_button_new_from_icon_name("udpscope-resize-rows-symbolic"); GtkWidget* b = layout_overlay_button(app, "udpscope-resize-rows-symbolic",
gtk_widget_set_tooltip_text(b, b_i == 0 ? "drag to resize the first row" b_i == 0 ? "drag to resize the first row"
: b_i == rows ? "drag to resize the last row" : b_i == rows ? "drag to resize the last row"
: "drag to resize the two rows here"); : "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);
gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_halign(b, GTK_ALIGN_CENTER);
gtk_widget_set_valign(b, GTK_ALIGN_START); 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 // 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-update", G_CALLBACK(on_layout_drag_update), &app);
g_signal_connect(drag, "drag-end", G_CALLBACK(on_layout_drag_end), &app); g_signal_connect(drag, "drag-end", G_CALLBACK(on_layout_drag_end), &app);
gtk_widget_add_controller(b, GTK_EVENT_CONTROLLER(drag)); 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 // The editor's way out, in the area's top-right corner: the toolbar's toggle is in the