ui: bolder tab glyphs, and the trigger shares the Signals pill's picture

The previous pair were detailed miniatures — 1px ticks, 2px dashes, a 13-column wave — and at 32x16 that
reads as *texture* rather than as a shape, which is what "not clear" looks like. All three glyphs are
redrawn bold: one thick band, two thick dashes, few features.

- **Measure** is two cursors beside the ruler they read against. Each cursor is a cap over two bold
  dashes (4px), where three 2px dashes read as a dotted column; the ruler is two bands with three 2x4
  marks between them and **no end caps**, because a closed frame reads as a rectangle and costs a fifth
  of the ink on two lines that say nothing. This is the layout whose first attempt looked like two icons
  in a box — the difference is entirely the weight and the open ends.
- **Signals** is a signal rising above its level beside two bold channel rows. Half a period instead of
  a full one: at this size a full period is a zigzag and a hump is unmistakably a wave, and the level's
  marks survive on both sides of it instead of being swallowed by the crest.
- **The trigger's own icon** is now the same picture as the Signals pill, so the pill and the button that
  turns the trigger on are recognisably about the same thing.

Each was rendered and read back as an ink map at 1:1 and at 2x before it was installed, and three
candidates were compared for the Measure glyph (markers *on* a scale, cursors *over* a ruler, and this):
the first two turned the marks and the shafts into blobs at the crossings. Ink is 18%, 30% and 29%,
inside the range the icons above them set, and everything stays inset two pixels.

Verified: `toolbar icons: trigger=udpscope-trigger-symbolic(ok)`;
`page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`;
`switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `[side-tab-probe] … advanced=4`;
the Preferences dialog opened in the same run with no "not present"; `make test-ui` 14 files; zero
`Gtk-CRITICAL`. The icons' blob is 11774 bytes, still over the C99 limit, so the clean-build warning
count stays four.
This commit is contained in:
Martino Ferrari committed 2026-10-14 13:58:36 +02:00
1 parent 574f47f769
commit 8c9c6ff7fd
6 files changed
+76 -140

No files matched your search

+2 -2
View File
@@ -2394,8 +2394,8 @@ now — they are listed so they are not mistaken for anything else:
section was short of room, and finding one meant scrolling past the others. The built section was short of room, and finding one meant scrolling past the others. The built
sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals** sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals**
tab (the trigger and the channels), **each a pill with its own glyph and no text** — the tab (the trigger and the channels), **each a pill with its own glyph and no text** — the
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list for cursors and the ruler for Measure, a signal rising above its trigger level and the channel list
Signals (the same picture the toolbar's trigger button carries), drawn as a for Signals (the same picture the toolbar's trigger button carries), drawn as a
32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and 32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and
accessible name. The design spec had the switcher showing *titles* and neither page an accessible name. The design spec had the switcher showing *titles* and neither page an
`icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon, `icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon,
+18 -68
View File
@@ -61,74 +61,24 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is
the Preferences window's icon, not the tab's. the Preferences window's icon, not the tab's.
The two **tabs** got glyphs of their own, and each is a *pair* rather than one of the icons above: The two **tabs** got glyphs of their own, and each is a *pair* rather than one of the icons above.
`udpscope-tab-measure-symbolic` is the cursors and the ruler they read against, and `udpscope-tab-measure-symbolic` is two cursors beside the ruler they read against — each a cap over two
`udpscope-tab-signals-symbolic` is the trigger's edge above the channel list it acts on. They are not bold dashes, beside a ruler of two bands with three marks between them and **no end caps** (a closed
reuses because a glyph the toolbar already carries means two things the moment a tab shows it too. Both frame reads as a rectangle, and costs a fifth of the ink on two lines that say nothing) — and
are **32x16** composites: the Measure pill is two cursors *standing on* the scale they read against — the `udpscope-tab-signals-symbolic` is a signal rising above its level, beside two bold channel rows. They
one band with alternating marks, each marker centred on one of them, which is one instrument rather are not reuses, because a glyph the toolbar already carries means two things the moment a tab shows it
than two icons in a box — and the Signals pill a sine with a dashed level through it beside the too; instead the *trigger's* own icon (`udpscope-trigger-symbolic`) was redrawn to the Signals pill's
channel list. That picture is shared on picture, so the pill and the button that turns the trigger on are recognisably about the same thing.
purpose: the trigger's *own* toolbar icon (`udpscope-trigger-symbolic`) is the same sine and the same Both are **32x16** composites drawn **bold** — one thick band, two thick dashes, few features — because
level, one period of one-pixel stairs, so the pill and the button that turns the trigger on are at this size a thin wave or a 1px tick reads as *texture* rather than as a shape, which is what "not
recognisably about the same thing. The trigger glyph used to be a rising edge, and one edge names only clear" looks like. Wider than tall on purpose: a 16px square cell carries one motif, not a pair. And
one of the four shape triggers the panel offers — a wave crossing a level names the trigger's job. **GDK paints an icon into a square box whatever the document's aspect** (measured:
Wider than tall on purpose — a 16px square cell carries `gtk_icon_theme_lookup_icon()` at 32 reports `32x32`, and librsvg fits it to `32x16`), so the pair is
one motif, and squeezing a pair into it is what turns both into a smear — and the pill has the width for letterboxed rather than stretched — and the pill's image is given `pixel_size` 32 in `name_side_tabs()`,
it, in a 300px column whose switcher has two pills in it. **GDK paints an icon into a square box whatever which draws the artwork at the size it is written, so its bars stay crisp. Every glyph here is
the document's aspect** (measured: `gtk_icon_theme_lookup_icon()` at 32 reports `32x32`, and librsvg **inset two pixels**, because art flush with the cell edge is what a button's padding or a crop takes a
fits it to `32x16`), so the pair is letterboxed, not stretched — and the pill's image is given pixel off, and a fractional scale is what would blur it, which is why nothing is drawn at 24 or 48. The
`pixel_size` 32 in `name_side_tabs()`, which draws the artwork at exactly the size it is written, so its two tab glyphs measure 29% and 30% ink, inside the range the icons above them set.
2px bars stay crisp. A fractional scale is what blurs them, which is why nothing here is drawn at 24 or
48. The bars in the Signals list are all one length, because unequal ones read as a bar chart and a chart
is not a list; their ink fractions (23% and 33%) sit inside the range the icons above set, which is the
check that a composite has not turned into a blot. Beside them in
`src/icons/scalable/apps/` are the two
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and
`org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an
application's own icon and is where the **in-process** window icon resolves from; it is also
the layout that `cmake/install_desktop_entry.cmake` mirrors when it copies the same two files
into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed
copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell
cannot read a GResource compiled into the binary).
All nine are bundled by `src/icons/udpscope.gresource.xml` (CMake runs
`glib-compile-resources`) and reached through **one**
`gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top
of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon,
set before the toolbar is built, and the toolbar's buttons) and `run_ui()` is the one place
ahead of both. `adw_init()` calls `gtk_init()`, which is what makes
`gdk_display_get_default()` valid there.
A resource rather than files beside the binary because udpscope is run straight out of the
build tree and has no install step. Five authoring rules, each of which fails quietly:
* **GTK's symbolic recolour only reaches `fill` on `path`, `rect` and `circle`.** It wraps the
SVG in a stylesheet for those selectors, so a `stroke`, a `line` or a `polyline` keeps the
colour written in the file and stays dark on a dark theme. **In a symbolic icon everything is
therefore a filled `path`** — including anything that wants to look stroked, which is drawn as
a closed band (two offset curves joined) instead. The rule is scoped to symbolic icons on
purpose: `org.udpscope.app.svg` is 128px colour artwork that is never recoloured, so it strokes
its sine freely, which is the sane way to draw a smooth one. Do not "fix" one to match the
other.
* **Whole-pixel coordinates**, because these are drawn at 16 px far more often than not: the
first draft placed features on half-pixels and the whole icon was a grey smear at 1x.
* `fill-rule="evenodd"` is how the ruler's body is a hole *and* its ticks still fill -- a tick
inside the hole is three crossings deep, which is odd. Nonzero would fill the hole back in.
* Overlapping subpaths under the default nonzero rule must be **wound the same way** or they
cancel instead of uniting. All of them run clockwise.
* **A malformed SVG is a build error, and that is the only place it can be caught.** Every
`<file>` in `src/icons/udpscope.gresource.xml` carries `preprocess="xml-stripblanks"`, which
makes `glib-compile-resources` pipe it through `xmllint` — so `--` inside an XML comment, which
is illegal and which librsvg refuses the whole file for, fails the build with a file, a line
and a caret under the offending character (`…app-symbolic.svg:5: parser error : Double hyphen
within comment`). It has to be here because **no runtime check can see it**: librsvg refuses
the file but the icon *name* still resolves, so `gtk_icon_theme_has_icon()` returns true and
the icon draws nothing. `gtk_icon_theme_lookup_icon()` cannot tell either (see the `app icons:` line under
Verification), `GMarkup` accepts the double hyphen because it is a lenient subset parser, and
`gdk_pixbuf` goes through glycin here, which sandboxes a subprocess per load and refuses this
project's own icons outright. The cost is that the `xmllint` binary is a build dependency; it
ships with libxml2, which GTK already requires.
`gtk_icon_theme_has_icon()` is checked for every toolbar button and reported by the dump (see
`toolbar icons:` in [probe-readings.md](probe-readings.md)): a resource path that was never added does not raise an
error, it draws `image-missing`.
- **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has - **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has
none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them
with `gtk_accelerator_parse()` — so they are `"space"`, `"<Control>e"`, `"<Control>slash"`, with `gtk_accelerator_parse()` — so they are `"space"`, `"<Control>e"`, `"<Control>slash"`,
+1 -1
View File
@@ -16,7 +16,7 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
Two baselines every run is read against. A clean build emits **four** warnings and no more: two Two baselines every run is read against. A clean build emits **four** warnings and no more: two
`-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both** `-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both**
generated GResource `.c` files — the help pages' (**514660** bytes) and the icons', whose blob is generated GResource `.c` files — the help pages' (**514660** bytes) and the icons', whose blob is
**12624** against C99's 4095-byte string-literal limit with the seven action icons and the two app icons **11774** against C99's 4095-byte string-literal limit with the seven action icons and the two app icons
that remain (a note about a generated file, which GCC itself supports) -- re-measured when that remain (a note about a generated file, which GCC itself supports) -- re-measured when
the tab work changed the set (that icon left, the two tab glyphs arrived, and this file's the tab work changed the set (that icon left, the two tab glyphs arrived, and this file's
help page was rewritten), which did not move the count; re-measure by touching each `*.gresource.xml` help page was rewritten), which did not move the count; re-measure by touching each `*.gresource.xml`
@@ -1,26 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The Measure tab: two cursors standing on the scale they read against. The scale is one band with <!-- The Measure tab: two cursors beside the ruler they read against. Each cursor is a cap over two
alternating marks rather than the closed ruler body `udpscope-measure-symbolic` draws for the bold dashes — the mark the toolbar's cursors button carries, at a weight that survives this size.
Preferences window: at 32x16 a frame costs a fifth of the ink on two lines that say nothing, and The ruler is two bands with three marks between them and *no end caps*: a closed frame reads as a
the cursors are what this tab is *for*. Each marker is centred on one of the marks — a cap over a rectangle, and it costs a fifth of the ink on two lines that say nothing. Both motifs are centred
dashed shaft that meets the scale instead of hovering over it — so the two read as one instrument on the same line and share one weight, and everything is inset two pixels. -->
rather than as two icons sharing a box. Both use the 2px weight of every other icon here, and
everything is inset two pixels, because art flush with the cell edge is what a button or a crop
clips. -->
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg"> <svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" d="M2 12 H30 V14 H2 Z <path fill="#2e3436" d="M2 2 H7 V5 H2 Z
M5 9 H7 V12 H5 Z M3 6 H6 V10 H3 Z
M9 10 H11 V12 H9 Z M3 11 H6 V14 H3 Z
M13 9 H15 V12 H13 Z M9 2 H14 V5 H9 Z
M17 10 H19 V12 H17 Z M10 6 H13 V10 H10 Z
M21 9 H23 V12 H21 Z M10 11 H13 V14 H10 Z
M25 10 H27 V12 H25 Z M17 4 H30 V6 H17 Z
M7 3 H12 V6 H7 Z M17 10 H30 V12 H17 Z
M8 7 H11 V10 H8 Z M18 6 H20 V10 H18 Z
M8 10 H11 V12 H8 Z M22 6 H24 V10 H22 Z
M19 3 H24 V6 H19 Z M26 6 H28 V10 H26 Z"/>
M20 7 H23 V10 H20 Z
M20 10 H23 V12 H20 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,32 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The Signals tab: the toolbar's trigger picture — a sine with a dashed level through it — beside <!-- The Signals tab: the toolbar's trigger picture — a signal above its level — beside the channel
the channel list. Sharing that picture is the point: the pill and the toolbar button that turns list. Sharing that picture is the point: the pill and the button that turns the trigger on should
the trigger on should be recognisably about the same thing. Three channel rows on the right, on be recognisably about the same thing. Two channel rows on the right, drawn bold for the same
the left half's centre line, and everything inset two pixels, because art flush with the cell reason the wave is, and everything inset two pixels. -->
edge is what a button or a crop clips. -->
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg"> <svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" d="M2 6 H3 V10 H2 Z <path fill="#2e3436" d="M3 7 H4 V12 H3 Z
M3 4 H4 V8 H3 Z M4 6 H5 V11 H4 Z
M4 3 H5 V7 H4 Z M5 4 H6 V9 H5 Z
M5 2 H6 V6 H5 Z M6 3 H7 V8 H6 Z
M6 3 H7 V7 H6 Z M7 2 H8 V7 H7 Z
M7 4 H8 V8 H7 Z M8 2 H9 V7 H8 Z
M8 6 H9 V10 H8 Z M9 2 H10 V7 H9 Z
M9 8 H10 V12 H9 Z M10 2 H11 V7 H10 Z
M10 9 H11 V13 H10 Z M11 3 H12 V8 H11 Z
M11 10 H12 V14 H11 Z M12 4 H13 V9 H12 Z
M12 9 H13 V13 H12 Z M13 6 H14 V11 H13 Z
M13 8 H14 V12 H13 Z M14 7 H15 V12 H14 Z
M14 6 H15 V10 H14 Z M2 8 H3 V10 H2 Z
M2 7 H5 V9 H2 Z M15 8 H17 V10 H15 Z
M7 7 H10 V9 H7 Z M18 3 H23 V8 H18 Z
M12 7 H15 V9 H12 Z M24 4 H30 V7 H24 Z
M18 2 H21 V5 H18 Z M18 10 H23 V15 H18 Z
M23 3 H30 V5 H23 Z M24 11 H30 V14 H24 Z"/>
M18 7 H21 V10 H18 Z
M23 8 H30 V10 H23 Z
M18 12 H21 V15 H18 Z
M23 13 H30 V15 H23 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,26 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT --> <!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari --> <!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The trigger: a sine and the dashed level it is looked for at, which is what a trigger *is* on a <!-- The trigger: a signal rising above the level it is looked for at, with the level showing on both
waveform that is not a step. It used to be a rising edge, and one edge names only one of the four sides of it. One event, which is what the trigger is for. Bold on purpose: at this size a
shape triggers the panel offers — a wave crossing a level names the trigger's job instead. One thinner wave reads as texture, and the level has to survive as two clear marks rather than as a
period, one-pixel stairs, the way the plot rasterises its own traces, with the level through the dotted line. Inset two pixels like the tabs' glyphs, so no art sits flush with an edge a button
centre so the crest and the trough stand clear and the crossings fall on the steep parts. Inset might clip. Whole-pixel stairs, the way the plot rasterises its own traces. -->
two pixels, like the tabs' glyphs, so no art is flush with an edge a button might clip. -->
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"> <svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path fill="#2e3436" d="M2 7 H3 V10 H2 Z <path fill="#2e3436" d="M3 7 H4 V11 H3 Z
M3 5 H4 V8 H3 Z M4 5 H5 V9 H4 Z
M4 3 H5 V6 H4 Z M5 4 H6 V8 H5 Z
M5 3 H6 V6 H5 Z M6 3 H7 V7 H6 Z
M6 4 H7 V7 H6 Z M7 2 H8 V6 H7 Z
M7 6 H8 V9 H7 Z M8 2 H9 V6 H8 Z
M8 8 H9 V11 H8 Z M9 3 H10 V7 H9 Z
M9 10 H10 V13 H9 Z M10 4 H11 V8 H10 Z
M10 11 H11 V14 H10 Z M11 5 H12 V9 H11 Z
M11 11 H12 V14 H11 Z M12 7 H13 V11 H12 Z
M12 9 H13 V12 H12 Z M2 8 H3 V10 H2 Z
M13 7 H14 V10 H13 Z M13 8 H15 V10 H13 Z"/>
M2 7 H5 V9 H2 Z
M7 7 H10 V9 H7 Z
M11 7 H14 V9 H11 Z"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB