From 6deb53c458b1a650298c2c2fd939d8a3d21ac988 Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Sun, 4 Oct 2026 20:32:52 +0200 Subject: [PATCH] feat: show the app icon on the window, and assert it is really there The resource-path registration moves from build_toolbar() up into run_ui(), because the window's icon is now a second consumer of it and this is the one place ahead of both. Whichever consumer ran first would otherwise resolve its name to nothing, with no error -- GTK draws a missing-image placeholder. The new 'app icons:' dump line is the guard, computed on the first tick beside 'toolbar icons:'. It reports named-and-present at the size the file declares, and deliberately does not claim the file renders: no runtime GTK call can see a malformed SVG, which is why the manifest validates at build time instead. --- src/ui.cpp | 94 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 9 deletions(-) diff --git a/src/ui.cpp b/src/ui.cpp index f879e9a..5b3dbfe 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -148,6 +148,7 @@ struct App { /// registered is *silent* -- GTK substitutes a missing-image placeholder, the button still /// works, and every other line of the dump still passes. std::string dbg_toolbar_icons; + std::string dbg_app_icons; /// The header bar, so the dump can report the order the controls are in: a header bar is a /// *titlebar*, not a child of the window, so walking down from the window does not reach it. GtkWidget* toolbar = nullptr; @@ -4386,6 +4387,61 @@ void rebuild_signal_panels(App& app) { app.signal_generation = app.model->generation(); } +/// The width and height the root `` element of `data` declares, or 0x0 when it has none. +/// GMarkup rather than a string scan because the attributes may be in any order and on any +/// line; it is *not* a validity check -- GMarkup is a deliberately lenient subset parser and +/// accepts a double hyphen inside a comment, which is exactly the file librsvg refuses. That +/// check is `xml-stripblanks` in the GResource manifest, at build time. +void svg_declared_size(const char* data, gsize size, int* w, int* h) { + struct Found { int w = 0; int h = 0; }; + Found found; + GMarkupParser parser{}; + parser.start_element = [](GMarkupParseContext*, const char* element, const char** names, + const char** values, gpointer user, GError**) { + if (std::strcmp(element, "svg") != 0) return; + Found* f = static_cast(user); + for (int i = 0; names[i] != nullptr; ++i) { + if (std::strcmp(names[i], "width") == 0) f->w = std::atoi(values[i]); + if (std::strcmp(names[i], "height") == 0) f->h = std::atoi(values[i]); + } + }; + GMarkupParseContext* ctx = g_markup_parse_context_new(&parser, GMarkupParseFlags(0), &found, + nullptr); + if (g_markup_parse_context_parse(ctx, data, static_cast(size), nullptr)) { + g_markup_parse_context_end_parse(ctx, nullptr); + } + g_markup_parse_context_free(ctx); + *w = found.w; + *h = found.h; +} + +/// One entry of the `app icons:` line: the icon is *named* (the theme resolved it, so the +/// resource path was registered) and *present* (the bytes the name resolved to are really in +/// the binary), at the size its own root element declares. +/// +/// It deliberately does not claim the file renders. `gtk_icon_theme_lookup_icon()` never +/// returns NULL, its paintable's intrinsic size is the size that was asked for, and a valid +/// icon, a malformed one and a name that does not exist at all all snapshot to the same +/// texture node -- GTK substitutes `image-missing` and there is nothing left to compare. The +/// malformed case is a build error instead; see the GResource manifest. +std::string icon_report(GtkIconTheme* theme, const char* name, const char* resource) { + const bool named = gtk_icon_theme_has_icon(theme, name) != FALSE; + GBytes* bytes = g_resources_lookup_data(resource, G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr); + const bool present = bytes != nullptr; + int w = 0; + int h = 0; + if (present) { + gsize size = 0; + const char* data = static_cast(g_bytes_get_data(bytes, &size)); + svg_declared_size(data, size, &w, &h); + g_bytes_unref(bytes); + } + std::string out = std::string(name) + "("; + out += (named && present) ? "ok " : "MISSING "; + out += std::to_string(w) + "x" + std::to_string(h) + ")"; + return out; +} + /// Where the panel controls live, as their parents say: the three pane toggles are toolbar buttons /// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the /// window is alive, neither of which is true when the dump runs -- so the dump can print a fact @@ -4440,9 +4496,11 @@ void note_panel_controls(App& app) { // ... and the icon each of them carries, asked of the theme rather than of our own // string. Four of these are drawn for this app and live in a GResource inside the - // binary, which the theme only looks in because `build_toolbar()` adds the resource - // path -- and a path that was never added fails *silently*: the button keeps working, - // the layout is unchanged, and GTK draws a missing-image placeholder no probe can see. + // binary, which the theme only looks in because `run_ui()` adds the resource path + // before building anything -- and a path that was never added fails *silently*: the + // button keeps working, the layout is unchanged, and GTK draws a missing-image + // placeholder no probe can see. The two app icons below come out of the same + // registration, which is why a MISSING on either line is one defect. GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); for (const auto& [widget, name] : named) { const char* icon = nullptr; @@ -4455,6 +4513,15 @@ void note_panel_controls(App& app) { app.dbg_toolbar_icons += std::string(app.dbg_toolbar_icons.empty() ? "" : " ") + name + "=" + icon + (gtk_icon_theme_has_icon(theme, icon) ? "(ok)" : "(MISSING)"); } + // The application icon, the same two ways and for the same reason: a resource path that was + // never registered is not an error, it is a missing-image placeholder. This one is read on + // the first tick like the line above, because the dump runs after the window is destroyed. + app.dbg_app_icons = + icon_report(theme, "org.udpscope.app", + "/org/udpscope/icons/scalable/apps/org.udpscope.app.svg") + + " " + + icon_report(theme, "org.udpscope.app-symbolic", + "/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg"); } /// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window @@ -5582,12 +5649,6 @@ GtkWidget* build_toolbar(App& app) { GtkWidget* bar = gtk_header_bar_new(); app.toolbar = bar; - // The app's own icons, before the first button asks for one. They are a GResource - // compiled into the binary (see CMakeLists), and this is what makes the theme look - // there: an unregistered path is not an error, it is a missing-image placeholder. - gtk_icon_theme_add_resource_path(gtk_icon_theme_get_for_display(gdk_display_get_default()), - "/org/udpscope/icons"); - gtk_header_bar_pack_start(GTK_HEADER_BAR(bar), labeled_box_fixed("window", build_window_control(app))); refresh_window_limits(app); @@ -5764,6 +5825,10 @@ void on_activate(GtkApplication* application, gpointer user) { }), app); gtk_window_set_title(GTK_WINDOW(app->window), "udpscope"); + // The shell takes a window's icon from this name, and GTK takes the WM class from the + // application ID -- which is the same string, which is what associates this window with the + // desktop entry. + gtk_window_set_icon_name(GTK_WINDOW(app->window), "org.udpscope.app"); gtk_window_set_default_size(GTK_WINDOW(app->window), 1280, 800); gtk_window_set_titlebar(GTK_WINDOW(app->window), build_toolbar(*app)); @@ -7525,6 +7590,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit // 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(); + // The app's own icons, before anything asks for one. They are a GResource compiled into the + // binary (see CMakeLists), and this is what makes the theme look there: an unregistered path + // is not an error, it is a missing-image placeholder. + // + // It is here rather than in `build_toolbar()` because there are now two consumers -- the + // window's own icon and the toolbar's buttons -- and this is the one place that is ahead of + // both. `adw_init()` calls `gtk_init()`, which opens the default display, so the line above + // is what makes `gdk_display_get_default()` valid here. + gtk_icon_theme_add_resource_path(gtk_icon_theme_get_for_display(gdk_display_get_default()), + "/org/udpscope/icons"); App app; app.model = &model; app.acq = &acq; @@ -7799,6 +7874,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit // critical (which is how the first version of this line earned four of them). std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str()); std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str()); + std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str()); // The measurements pane, with the numbers the engine computed: a probe adds measurements and // this is where their values are read, so a wrong threshold or a window that moved under a // stale result is visible in the run rather than only on screen.