Files
udpscope/docs/superpowers/specs/2026-10-04-app-icon-and-desktop-entry-design.md
T
Martino FerrariandClaude Opus 4.6 7816f11c81 docs: spec for the app icon, AdwAboutDialog and desktop entry
Records the decisions taken before design — the application ID stays
org.udpscope.app, the icon is the instrument face, and the desktop entry is an
opt-in CMake target rather than an install() rule — along with the HIG
constraints the artwork is built to and the two silent failures the new
verification line has to tell apart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 20:07:04 +02:00

11 KiB
Raw Blame History

App icon, Adwaita About dialog, and desktop entry

Date: 2026-10-04

Goal

Three related pieces of desktop integration udpscope currently lacks:

  1. An application icon drawn to the GNOME HIG, in both full colour and symbolic form.
  2. An About dialog that shows it, built on AdwAboutDialog rather than GtkAboutDialog.
  3. A .desktop entry, so the app can be launched from the shell rather than only from a terminal.

Decisions taken before design

The application ID stays org.udpscope.app. GNOME convention is that the application ID, the icon name and the desktop file's basename are one string, so that string is org.udpscope.app throughout: the icon is named org.udpscope.app, the desktop file is org.udpscope.app.desktop, and gtk_application_new() is left exactly as it is. The alternative (moving to it.ferrarinas.Udpscope, the reverse-DNS of the project's actual forge domain) was offered and declined. Nothing else in the tree depends on the ID — the config directory is ~/.config/udpscope and the app is G_APPLICATION_NON_UNIQUE — so this choice costs nothing beyond the naming itself.

The icon is the instrument face, not a bare waveform and not a colour version of the existing trigger icon. A dark panel carrying one bold trace is the clearest available metaphor, it is the kind of physical object the HIG's top-and-front depth language was written for, and its silhouette survives being scaled down better than either alternative.

The desktop entry is installed by an opt-in CMake target, not by install() rules and not by documented cp commands. The project deliberately has no install step — that is the stated reason its icons are a GResource — and a target that does nothing until asked preserves that while still producing a working entry. install() rules were rejected because they would create a second copy of the app that can diverge from the build tree; manual cp was rejected because Exec would need the binary on PATH, which it is not.

The HIG constraints this is built to

Taken from https://developer.gnome.org/hig/guidelines/app-icons.html:

  • Nominal canvas 128×128, which the artwork should not fill.
  • A 2px grid for all detail.
  • Depth comes from combining the top and front of an object: a front profile along the bottom, no taller than 2 detail units (4 nominal px).
  • Flat colour on straight surfaces; gradients reserved for curved surfaces.
  • Shadows avoided where possible.
  • The standard GNOME palette.
  • A symbolic version is required, reusing the same metaphor, following the UI-icon (single colour) rules.
  • The icon must still read at 64px and 32px.

Component 1 — the artwork

Two new source files, both added to the existing src/icons/udpscope.gresource.xml under the scalable/apps/ tree (the icons already there are scalable/actions/).

src/icons/scalable/apps/org.udpscope.app.svg

Full colour, 128×128, every coordinate on the 2px grid:

element geometry palette colour
chassis rounded rect (12, 22)–(116, 106), radius 8 Dark 3 #3d3846
front profile the bottom band of that same silhouette, y 102–106 Dark 2 #5e5c64
screen rounded rect (20, 30)–(108, 94), radius 4 Dark 4 #241f31
trace one sine cycle, centred y = 62, amplitude 18, over x 24–104, 6 px thick Green 1 #8ff0a4

The chassis is one rounded rect and the front profile is the bottom 4px of that silhouette, drawn as a path that follows its bottom corners — not a separate full-width rectangle underneath it. A plain rect from (12, 102) to (116, 106) would protrude past the radius-8 corners, which at 128px reads as a misdrawn bump and at 32px as a dirty edge.

That band is exactly the HIG's 4px maximum and is the only depth treatment: no gradients (every surface here is straight, so flat colour is what the HIG asks for) and no shadows.

The artwork occupies x 12–116 and y 22–106, leaving 12px side margins and 22px top and bottom, so it does not fill the canvas. Every coordinate above is even.

Two deliberate omissions. There is no graticule — the grid lines a real scope face carries cannot survive 32px, and the trace already carries the metaphor without them. And the trace is one cycle, a single trough and a single crest, rather than several: at 32px the screen is about 22×16 px, and anything busier reads as a smudge.

src/icons/scalable/apps/org.udpscope.app-symbolic.svg

16×16, the same metaphor reduced to a screen frame with a wave inside it. Unlike the colour icon this is filled paths only, with an evenodd hole for the screen's interior, because GTK's symbolic recolour only reaches fill on path, rect and circle — a stroke keeps the colour written in the file and stays dark on a dark theme. Whole-pixel coordinates throughout.

This asymmetry is the trap worth documenting: CLAUDE.md's existing icon-authoring rules say "everything is a filled path" without qualifying it, because every icon in the tree so far has been symbolic. A 128px colour icon is never recoloured, so it may stroke freely — and stroking is the sane way to draw a smooth sine. The rule becomes "fills only in a symbolic icon".

Component 2 — the About dialog

adw_about_dialog_new() returns an AdwDialog*. An AdwDialog is not a GtkWindow, which is the trap CLAUDE.md already documents for the shortcuts overview, so in src/help_window.cpp:

  • g_about changes from GtkWidget* to AdwDialog*.
  • It is cleared on ::closed, not ::close-request (AdwDialog has no such signal).
  • It closes with adw_dialog_close(), not gtk_window_close().
  • It is presented with adw_dialog_present().
  • Both AdwDialogs can share one ::closed handler keyed by a pointer-to-pointer, leaving on_window_close to the help window alone.

close_help_windows() therefore becomes one GtkWindow (help) plus two AdwDialogs (About, shortcuts), and both its doc comment and the CLAUDE.md invariant that says "two of the three are GtkWindows" have to be revised to "one of the three".

Field mapping:

GtkAboutDialog AdwAboutDialog
program_name application_name
version version
comments comments
license_type license_type
system_information debug_info
— application_icon = "org.udpscope.app"
— developer_name, website

Two gains are taken while mapping. debug_info renders as a Troubleshooting page with a copy button, where system_information was inert text. And the string behind it is built as a std::string rather than the char details[1024] a previous review flagged for silent truncation on a long settings path — removing the ceiling rather than widening it, in code already being rewritten.

Credits, release notes and an issue URL are deliberately not added; nothing asked for them.

One ordering detail: gtk_icon_theme_add_resource_path() currently runs at the top of build_toolbar(). The main window's own icon is now set from a name in the same resource, so the registration moves up into run_ui(), before both gtk_window_set_icon_name() and the build_toolbar() call — otherwise whichever of the two runs first resolves its name to nothing.

Component 3 — the desktop entry

data/org.udpscope.app.desktop.in

[Desktop Entry]
Type=Application
Name=udpscope
GenericName=Oscilloscope
Comment=An oscilloscope for UDPStreamer sources
Exec=@UDPSCOPE_EXEC@
Icon=org.udpscope.app
Terminal=false
Categories=Development;Science;Electronics;
Keywords=oscilloscope;UDP;signal;waveform;trigger;
StartupNotify=true

@UDPSCOPE_EXEC@ is configured to the built binary's absolute path in the build tree. The consequence to accept knowingly: moving or deleting the build directory breaks the entry. That is the price of having no install step, and it is the right price here because the entry then always launches exactly the binary that was last built.

No StartupWMClass is needed. GTK takes the WM class from the application ID, and the desktop file's basename already matches it, which is what associates a running window with this entry.

The desktop-entry target

A custom target driving cmake/install_desktop_entry.cmake, which writes three files under $XDG_DATA_HOME (defaulting to ~/.local/share):

  • applications/org.udpscope.app.desktop
  • icons/hicolor/scalable/apps/org.udpscope.app.svg
  • icons/hicolor/symbolic/apps/org.udpscope.app-symbolic.svg

The two SVGs are copied from src/icons/scalable/apps/ in the source tree, not extracted from the GResource: the same two files serve both the in-binary resource and the installed icon theme, so there is one copy to edit.

It then refreshes update-desktop-database and gtk4-update-icon-cache if either is on PATH, runs desktop-file-validate if it is available, and prints every path it wrote.

cmake --build build is unaffected: the target runs only when named. No uninstall target is provided — the three paths are printed, so undoing it is three rms.

Verification

Two silent failures are possible, and they fail differently.

A GResource path that was never registered draws image-missing while every other line of the dump passes; the existing toolbar icons: line already guards that class with gtk_icon_theme_has_icon(). But a malformed SVG is worse: librsvg refuses the whole file, has_icon() still returns true because the name resolves, and the icon draws nothing. has_icon cannot see it — and these SVGs have already been bitten once by librsvg, which rejects a file for a -- inside an XML comment.

So the new assertion distinguishes named from renderable:

app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)

It looks each icon up and queries the resulting paintable's intrinsic size. Both entries must read ok with a non-zero size; a parse failure reports 0x0 with the name still resolving, which is precisely the case the existing check misses.

Alongside it:

  • The [help] probe gains about icon=org.udpscope.app, read back through adw_about_dialog_get_application_icon(), so the dialog is asserted to carry the icon rather than merely to open.
  • That probe's existing closed ok becomes a stronger crash test, because About is now an AdwDialog closed by a different call than the help window beside it. It must still produce no Gtk-CRITICAL beyond the two already baselined.
  • desktop-file-validate runs inside the desktop-entry target, so a malformed entry fails at the moment it is asked for rather than being silently ignored by the shell later.

Documentation

  • CLAUDE.md: the app icons: dump line as a documented assertion; the windows invariant revised to one GtkWindow and two AdwDialogs; the desktop-entry target and the build-path caveat; and the icon-authoring rules amended so "filled paths only" is scoped to symbolic icons.
  • README.md: the feature narrative — the icon's HIG constraints and the two omissions (graticule, multiple cycles) with the 32px reasoning, the move to AdwAboutDialog and what it gained, and how to get the desktop entry.

Out of scope

  • An AppStream metainfo.xml. Nothing asked for it, and it is only required for store submission.
  • Redrawing the four existing action icons.
  • A nightly/devel icon variant.
  • An uninstall target for the desktop entry.