From bb62b8178c288d11957888456ab5d975b317c8e0 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 17:38:25 +0200 Subject: [PATCH] fix: place the guided tour's card from its allocation, and count GTK warnings The tour measured its card with a height-for-width request whose for_size came from the natural width minus the margins the tour sets for positioning. A measure reports margins *included*, so that for_size landed a margin short of the minimum the box answers with and GTK warned once per frame: Trying to measure GtkBox 0x... for width of N, but it needs at least M (about 9/s for as long as the tour was up). The card now takes its size from gtk_widget_compute_bounds() once a frame has run -- its allocation rectangle, not gtk_widget_get_width(), which is only the content box -- and falls back to a measure with no for_size at all only on a frame with nothing allocated. GTK is built with structured logging, so its messages reach the log writer and never the handler API; no existing probe can read one. The exit dump now counts them (`gtk warnings:`, must read 0) through a writer function chained to g_log_writer_default(), which leaves stderr unchanged. --- docs/claude/invariants-gtk-and-chrome.md | 17 ++++++++++ docs/claude/probe-readings.md | 11 ++++++ docs/claude/verification.md | 5 ++- src/ui.cpp | 30 +++++++++++++++++ src/ui_tour.cpp | 43 ++++++++++++++++++------ 5 files changed, 94 insertions(+), 12 deletions(-) diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index b4c7ce3..9514051 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -511,6 +511,23 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps request did not help -- the margin alone was the loop). Take the margins off a measure that feeds a position back. `hdl`/`bd`-style CSS padding is *not* part of it, and neither is the other axis's margin, so it is `margin-start + margin-end` on the horizontal and top/bottom on the vertical. +- **A widget's allocation is not `gtk_widget_get_width()`, and a measure is never a position.** GTK's + `gtk_widget_get_width()`/`get_height()` return the widget's **content** box: `size_allocate` + subtracts the CSS border and padding before storing them (measured on the tour card, which carries + `border 1px` + `padding 12px`: `get_width()` reads 254 where the rectangle on screen is 280), so a + card *placed* from it overhangs its own size and covers the target it is meant to sit beside. + `gtk_widget_compute_bounds()` is that rectangle -- border and padding in, margins out -- and is what + the probe's geometry and the help's figures already read. And `gtk_widget_measure()` cannot stand in + for it: its size includes the widget's margins (the bullet above), and asking for a **height with a + width** (`for_size`) on a widget whose margins were then subtracted from that width makes GTK warn + `Trying to measure GtkBox ... for width of N, but it needs at least M` **once per frame**, because + the for_size lands one margin short of the minimum the widget answers with. The guided tour's card + therefore takes its size from `compute_bounds()` once a frame has run and falls back to a measure + **with no for_size at all** (`-1`, the form GTK measures without that check) only on a frame with + nothing allocated yet. The warning reaches no probe -- GTK is built with **structured logging**, so + it goes through the log *writer* and never `g_log_set_handler()`/`g_log_set_default_handler()` -- + which is why the exit dump counts GTK's own warnings through a writer function (`gtk warnings:`, + which must read 0) as the one reading that can assert the class away. - **The overflow menu's `Help` submenu is a two-page `GtkStack`, and neither of the obvious alternatives works.** Rebuilding the rows -- the plot menu's own shape -- destroys `app.layout_toggle`, which points into the main view and which `sync_layout_editor()` and the `L` diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 7ad77dc..3ed5e0c 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -410,6 +410,17 @@ Two lines of the self-check output are assertions in disguise: against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour" (`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not the auto-start. +- `gtk warnings: N` (in the exit dump, beside `help layer:`) — **GTK's own log at warning or above, + counted for the whole run, and it must read `0`.** It is counted through a log *writer* function + because GTK is built with structured logging: its messages go through the writer and never through + `g_log_set_handler()`/`g_log_set_default_handler()`, which is exactly why no probe can read one — + the message is written by the toolkit from inside a call of ours and is stored in no widget. The + first class to need it was `Trying to measure GtkBox … for width of N, but it needs at least M` + (once per frame, from a card measured for a height with a width a margin short of its own minimum; + see the allocation bullet in + [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to + `g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented + baselines are read the same way. - `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_switcher=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 4593258..9643333 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -24,7 +24,10 @@ touching each `*.gresource.xml` under `src/` and rebuilding; a third `-Wmissing- or any other warning is a finding. (The fifth warning that used to appear here was an unused `output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the `output_after_add` already printed on the same line — which dropped the baseline from five to -four.) And an exit-dump run emits **no** `Gtk-CRITICAL` at all. +four.) And an exit-dump run emits **no** `Gtk-CRITICAL` at all, nor any `Gtk-WARNING`: the dump's +`gtk warnings:` line counts GTK's own log (through a writer function — GTK is built with structured +logging, so the handler API never sees its messages) and must read `0`; see +[probe-readings.md](probe-readings.md). An *incremental* build reads **one** of those three: the generated GResource `.c` is not recompiled when only `src/` changed, so its `-Woverlength-strings` does not reappear. diff --git a/src/ui.cpp b/src/ui.cpp index 0080141..5f56ce4 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -5646,9 +5646,35 @@ void collect_spins(GtkWidget* w, std::vector* out) { /// dialogs built and destroyed in one run. +/// GTK writes its own log messages -- `Trying to measure GtkBox \u2026 for width of N, but it needs at +/// least M` is one -- from inside a call of ours, so they reach stderr and never a widget: no probe +/// can read one, and the class is otherwise invisible to the self-check. The exit dump therefore +/// carries a count of them, which is the one reading that can assert the class away (`gtk warnings: +/// 0`). GTK is built with structured logging, so its messages go through the log *writer* and not +/// through the handler API (`g_log_set_handler()` never sees them); the writer is chained to rather +/// than replaced, so every message still reaches stderr exactly as before and the documented +/// baselines are read the same way they always were. +int g_gtk_warning_count = 0; +GLogWriterOutput count_gtk_log(GLogLevelFlags level, const GLogField* fields, gsize n_fields, + gpointer user_data) { + for (gsize i = 0; i < n_fields; ++i) { + if (g_strcmp0(fields[i].key, "GLIB_DOMAIN") != 0) continue; + if (g_strcmp0(static_cast(fields[i].value), "Gtk") == 0 && + (level & (G_LOG_LEVEL_WARNING | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_ERROR)) != 0) { + ++g_gtk_warning_count; + } + break; + } + return g_log_writer_default(level, fields, n_fields, user_data); +} + int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq, RowSet& rows) { (void)argc; + // Count GTK's own log messages for the run (see `count_gtk_log`). Installed before anything can + // log -- and it must be: a log writer function may only be installed once, and GTK does not install + // one, so this is the whole of the run's logging rather than a replacement. + g_log_set_writer_func(count_gtk_log, nullptr, nullptr); // Installs libadwaita's stylesheet, which the shortcut overview's `AdwShortcutsDialog` // needs to be drawn at all. Idempotent, and `choose_source()` may already have run it. adw_init(); @@ -6040,6 +6066,10 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit // guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`), // so neither asks a destroyed widget anything. std::printf("[udpscope] help layer: %s\n", app.dbg_context_help.c_str()); + // GTK's own log at warning or above, counted for the whole run (see `count_gtk_log`). A probe + // cannot read one of these -- they are written by the toolkit, not stored in a widget -- so this + // count is the reading that catches the class, and the run must print `0`. + std::printf("[udpscope] gtk warnings: %d\n", g_gtk_warning_count); // The bounds the help's annotated figures draw their crop boxes from: window coordinates, or // -1,-1,0,0 for a widget that is not on screen (`is_visible()`, not `get_visible()` -- a widget // under a hidden parent keeps its own flag true). Measured on the tick, after a frame. diff --git a/src/ui_tour.cpp b/src/ui_tour.cpp index a80c687..d6d0679 100644 --- a/src/ui_tour.cpp +++ b/src/ui_tour.cpp @@ -296,19 +296,40 @@ void update_tour(App& app) { // whichever column is beside it -- which is, on this window, the side panel for the plot's step and // the plot for every step in the side panel. // - // Its size is measured, never requested -- but `gtk_widget_measure()` reports the size *including - // the widget's own margins* (measured: a 280 px box with `margin-start` 453 measures 733). Ours are - // the card's position, so reading the measure without taking them off is a feedback loop: the width - // it was placed at becomes the width it reports, and the card walks to the far corner and back - // between frames. + // The card's size is the *rectangle it was allocated*, read the same way the probe reads it and + // the help's figures are drawn from: `gtk_widget_compute_bounds()`. Not `gtk_widget_get_width()`, + // which is the widget's *content* size -- a border and a padding short of the rectangle on screen + // (measured here: 254 against the card's real 280), so a card placed from it overhangs its own size + // into the target; that call is read only to tell a frame that has been laid out from one that is + // too early. And, once a frame has run, not a measure either: + // * `gtk_widget_measure()` reports the size *including the widget's own margins* (a 280 px box + // with `margin-start` 453 measures 733), and ours are the card's position -- reading one back + // without taking them off is a feedback loop. + // * asking for a **height** with a width (`for_size`) on a widget whose margins are then left out + // of that width is a question GTK refuses: it reports `Trying to measure GtkBox ... for width + // of N, but it needs at least M` once per frame, because the for_size derived from the natural + // width minus the margins lands one margin short of the minimum the box answers with. The + // allocation the widget was actually given cannot be short of it. + // A measure survives only as the fallback for a frame with nothing allocated yet (the first frame + // the card is shown, before any layout pass), and that fallback is taken with **no for_size at + // all**: `for_size == -1` is the form GTK measures without the check that warns. int cw = 0; int ch = 0; - gtk_widget_measure(app.tour_card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr, nullptr); - cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(app.tour_card) - - gtk_widget_get_margin_end(app.tour_card)); - gtk_widget_measure(app.tour_card, GTK_ORIENTATION_VERTICAL, cw, nullptr, &ch, nullptr, nullptr); - ch = std::max(0, ch - gtk_widget_get_margin_top(app.tour_card) - - gtk_widget_get_margin_bottom(app.tour_card)); + graphene_rect_t card_bounds = GRAPHENE_RECT_INIT(0, 0, 0, 0); + if (gtk_widget_is_visible(app.tour_card) && gtk_widget_get_width(app.tour_card) > 0 && + gtk_widget_compute_bounds(app.tour_card, app.tour_overlay, &card_bounds)) { + cw = static_cast(std::ceil(card_bounds.size.width)); + ch = static_cast(std::ceil(card_bounds.size.height)); + } + if (cw <= 0 || ch <= 0) { + gtk_widget_measure(app.tour_card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr, + nullptr); + cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(app.tour_card) - + gtk_widget_get_margin_end(app.tour_card)); + gtk_widget_measure(app.tour_card, GTK_ORIENTATION_VERTICAL, -1, nullptr, &ch, nullptr, nullptr); + ch = std::max(0, ch - gtk_widget_get_margin_top(app.tour_card) - + gtk_widget_get_margin_bottom(app.tour_card)); + } int cx = 0, cy = 0; if (h - (hy + hh) >= ch + 2 * kGap) { cx = std::clamp(hx, kGap, std::max(kGap, w - cw - kGap));