feat: an app icon in colour and symbolic form, validated at build time

Both are drawn to the GNOME HIG: a 128px instrument face that leaves its
margins, carries one 4px front profile for depth and no gradients or shadows,
and a 16px symbolic reduction of the same metaphor in fills only. The colour
icon strokes its sine because it is never recoloured; the symbolic one cannot.

Every file in the manifest now preprocesses through xmllint, because a
malformed SVG is the one icon failure no runtime check can see: librsvg refuses
the file while the name still resolves, so the icon draws nothing and
has_icon() says it is fine.
This commit is contained in:
Martino Ferrari committed 2026-10-04 20:27:07 +02:00
1 parent 69eab02729
commit f3d68cfdd4
4 files changed
+84 -10

No files matched your search

+3 -2
View File
@@ -39,7 +39,7 @@ if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
target_compile_options(udpsclient PRIVATE -Wall -Wextra)
endif()
# --- icons: four symbolic SVGs compiled into the binary -----------------------
# --- icons: six SVGs compiled into the binary --------------------------------
# There is no stock icon for an oscilloscope trigger, a pair of cursors or a measurement
# ruler, so this app draws its own. They go in a GResource rather than beside the binary
# because udpscope is run straight out of the build tree and has no install step: a
@@ -48,7 +48,8 @@ endif()
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
set(UDPSCOPE_ICON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/icons)
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg)
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg
${UDPSCOPE_ICON_DIR}/scalable/apps/*.svg)
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
COMMAND ${GLIB_COMPILE_RESOURCES}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The symbolic form of the app icon: the same metaphor reduced to a screen frame with one
wave inside it, following the UI-icon rules rather than the app-icon ones.
Filled paths only, with no stroke anywhere. GTK's symbolic recolour wraps the file in a
stylesheet that reaches `fill` on `path`, `rect` and `circle` only, so a stroked line keeps
the colour written here and stays dark on a dark theme. The wave is therefore a closed
2px band: a sine along its top edge and the same sine 2px lower along its bottom, which is
how a stroke's look is had from a fill.
`fill-rule="evenodd"` over three subpaths. The frame is the outer rect minus the inner one,
so the screen's interior is two crossings deep and empty; the wave inside it is three, which
is odd, so it fills. Whole-pixel coordinates throughout, because this is drawn at 16px far
more often than not and a half-pixel feature is a grey smear at 1x.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#2e3436" fill-rule="evenodd"
d="M 1 2 H 15 V 14 H 1 Z
M 2 3 H 14 V 13 H 2 Z
M 3 7 C 5 3 6 3 8 7 C 10 11 11 11 13 7 L 13 9 C 11 13 10 13 8 9 C 6 5 5 5 3 9 Z"/>
</svg>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The application icon: an oscilloscope's face, which is the clearest metaphor available and
the kind of physical object the HIG's top-and-front depth language was written for.
Drawn to <https://developer.gnome.org/hig/guidelines/app-icons.html>. The artwork spans
x 12..116 and y 22..106, so it does not fill the 128px canvas, and every feature sits on
the 2px grid. Depth is the 4px front profile along the bottom and nothing else: that is
the HIG's maximum of 2 detail units, and since every surface here is straight, the rest
is flat colour with no gradients and no shadows.
The profile is the bottom 4px of the chassis silhouette, clipped to it. A plain rect from
(12, 102) to (116, 106) would protrude past the radius-8 corners, which reads as a
misdrawn bump at 128px and as a dirty edge at 32px; the clip follows the corners exactly
and keeps every coordinate even, which hand-computing the arc would not.
Two deliberate omissions. There is no graticule: the grid lines a real scope face carries
cannot survive 32px, and the trace carries the metaphor without them. And the trace is
one cycle, a single crest and a single trough, because at 32px the screen is about
22x16 px and anything busier reads as a smudge.
Colours are the GNOME palette: Dark 3 chassis, Dark 2 profile, Dark 4 screen, Green 1 trace.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
<defs>
<clipPath id="chassis-silhouette">
<rect x="12" y="22" width="104" height="84" rx="8"/>
</clipPath>
</defs>
<rect x="12" y="22" width="104" height="84" rx="8" fill="#3d3846"/>
<g clip-path="url(#chassis-silhouette)">
<rect x="12" y="102" width="104" height="4" fill="#5e5c64"/>
</g>
<rect x="20" y="30" width="88" height="64" rx="4" fill="#241f31"/>
<path d="M 24 62 C 37 38 51 38 64 62 C 77 86 91 86 104 62"
fill="none" stroke="#8ff0a4" stroke-width="6" stroke-linecap="round"/>
</svg>
+18 -8
View File
@@ -2,16 +2,26 @@
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The four icons this app had to draw for itself, compiled into the binary so there is
still exactly one file to run. `gtk_icon_theme_add_resource_path()` wants the tree an
icon theme has, so the prefix is followed by `<size>/<context>`; `scalable` is the one
that means "an SVG, use it at any size".
The icons this app had to draw for itself, compiled into the binary so there is still
exactly one file to run. `gtk_icon_theme_add_resource_path()` wants the tree an icon theme
has, so the prefix is followed by `<size>/<context>`; `scalable` is the one that means
"an SVG, use it at any size", and `apps` is where an application icon belongs.
Every file carries `preprocess="xml-stripblanks"`, which makes glib-compile-resources pipe
it through xmllint. That is the only thing that catches a malformed SVG: librsvg refuses
such a file outright, but the icon *name* still resolves, so `gtk_icon_theme_has_icon()`
returns true and the icon simply draws nothing. No runtime GTK call can tell the two apart
(`lookup_icon()` never returns NULL and always reports the size it was asked for), so the
check belongs here, where a double hyphen inside a comment fails the build with a file,
line and column instead.
-->
<gresources>
<gresource prefix="/org/udpscope/icons">
<file>scalable/actions/udpscope-trigger-symbolic.svg</file>
<file>scalable/actions/udpscope-cursors-symbolic.svg</file>
<file>scalable/actions/udpscope-measure-symbolic.svg</file>
<file>scalable/actions/udpscope-math-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-math-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app-symbolic.svg</file>
</gresource>
</gresources>