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.
This commit is contained in:
Martino Ferrari committed 2026-10-04 20:32:52 +02:00
1 parent f3d68cfdd4
commit 6deb53c458
1 file changed
+85 -9
+85 -9
View File
@@ -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 `<svg>` 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<Found*>(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<gssize>(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<const char*>(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.