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.
This commit is contained in:
Martino Ferrari committed 2026-10-14 17:38:25 +02:00
1 parent d8a79b96ba
commit bb62b8178c
5 files changed
+94 -12

No files matched your search

+30
View File
@@ -5646,9 +5646,35 @@ void collect_spins(GtkWidget* w, std::vector<double>* 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<const char*>(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.
+32 -11
View File
@@ -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<int>(std::ceil(card_bounds.size.width));
ch = static_cast<int>(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));