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>
This commit is contained in:
1 parent
8a37e9d412
commit
7816f11c81
1 file changed
+227
@@ -0,0 +1,227 @@
|
|||||||
|
# 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 `AdwDialog`s 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 `AdwDialog`s (About,
|
||||||
|
shortcuts), and both its doc comment and the CLAUDE.md invariant that says "two of the three are
|
||||||
|
`GtkWindow`s" 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 `rm`s.
|
||||||
|
|
||||||
|
## 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 `AdwDialog`s; 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.
|
||||||
Reference in new issue
Block a user