ui: the tab glyphs are 32x16 pairs, drawn twice as wide
Each tab's icon is now two motifs side by side instead of one in a 16px square: the Measure pill is the two cursors beside the ruler, the Signals pill the trigger beside three channel rows. A 16px cell carries one motif — squeezing a pair into it is what turned both into a smear — and the pill has the width for a pair in a 300px column with two pills in it. Two measurements decided the size, both of which I had wrong from the outside: - **GDK paints an icon into a square box whatever the document's aspect.** `gtk_icon_theme_lookup_icon()` at 32 reports a `32x32` paintable for a 32x16 document, and librsvg fits it: rendered with aspect kept, the artwork comes out `32x16` inside a 32 request. So the pair is letterboxed, not stretched, and `pixel_size` 32 draws it at exactly the size it is written — 1:1, so the 2px bars stay crisp. A fractional scale (24 or 48) is what would blur them, which is why nothing here is drawn at one. - **The bar does not get taller for it.** `switcher: … icon=32x32 size=308x36`: 36px is what the switcher measured with 16px icons too, because the pill's fixed height takes a 32px box without growing. Both glyphs were drawn and then read back as ink maps rather than trusted: the first drafts came out at 39% and 44% ink (a blot beside the house 25–36%), and the tab bar is the last place a shape can be checked, since no screenshot works under this X setup. The finished two measure 23% and 33%. Verified: `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36`; `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)`; `[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' GResource blob is 11661 bytes, still over the C99 limit, so the clean-build warning count stays four.
This commit is contained in:
1 parent
e615465366
commit
dbe6377415
9 files changed
+114
-47
No files matched your search
@@ -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
|
||||
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
|
||||
cursors and the ruler for Measure, the trigger and the channel list for Signals — with the words
|
||||
kept as each pill's tooltip and
|
||||
cursors and the ruler for Measure, the trigger and the channel list for Signals, drawn as a
|
||||
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
|
||||
`icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon,
|
||||
measured), so the pages carry an empty title and the pills are named in code. With the math
|
||||
|
||||
@@ -65,11 +65,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
`udpscope-tab-measure-symbolic` is the cursors and the ruler they read against, and
|
||||
`udpscope-tab-signals-symbolic` is the trigger's edge above the channel list it acts on. They are not
|
||||
reuses because a glyph the toolbar already carries means two things the moment a tab shows it too. Both
|
||||
are 16px composites with four features each, which is about the limit of this size: the Signals list is
|
||||
two rows and not three, so the edge and the list get half the cell each, and the bars in it are all one
|
||||
length because unequal ones read as a bar chart, and a chart is not a list. Their ink fractions (32% and
|
||||
36%) sit inside the range the icons above set, which is the check that a composite has not turned into a
|
||||
blot. Beside them in
|
||||
are **32x16** composites, one motif per half: the Measure pill is the two cursors beside the ruler, the
|
||||
Signals pill the trigger beside the channel list. Wider than tall on purpose — a 16px square cell carries
|
||||
one motif, and squeezing a pair into it is what turns both into a smear — and the pill has the width for
|
||||
it, in a 300px column whose switcher has two pills in it. **GDK paints an icon into a square box whatever
|
||||
the document's aspect** (measured: `gtk_icon_theme_lookup_icon()` at 32 reports `32x32`, and librsvg
|
||||
fits it to `32x16`), so the pair is letterboxed, not stretched — and the pill's image is given
|
||||
`pixel_size` 32 in `name_side_tabs()`, which draws the artwork at exactly the size it is written, so its
|
||||
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
|
||||
|
||||
@@ -357,13 +357,22 @@ Two lines of the self-check output are assertions in disguise:
|
||||
substitutes a missing-image placeholder, the button still works, and every other line of the
|
||||
dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this
|
||||
is the only check that it names something that exists.
|
||||
- `switcher: policy=narrow pills=2 labels=0 names=2` (in the exit dump, beside `panels:`) — the two
|
||||
- `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36` (in the exit dump, beside
|
||||
`panels:`) — the two
|
||||
sidebar tabs as the switcher actually draws them. **`pills=2 labels=0`** is the claim: two pills are on
|
||||
screen and **no text is drawn** (`labels` counts *mapped labels with text*, because `narrow` keeps an
|
||||
empty label in the pill's layout and a plain mapped count would read 2 either way). `names=2` is the
|
||||
other half — both pills carry a tooltip and an accessible name set by `name_side_tabs()`, so an
|
||||
icon-only pill is not an anonymous button — and `pills=0` means nothing is mapped, which makes the
|
||||
rest of the line meaningless rather than a pass. `policy=narrow` is read rather than assumed.
|
||||
rest of the line meaningless rather than a pass. `policy=narrow` is read rather than assumed. `icon=`
|
||||
is the box the pills' `GtkImage` is *rendered* in, which proves the 32px override stuck: the tab
|
||||
glyphs are 32x16 documents, and GDK paints an icon into a square box whatever the document's aspect,
|
||||
so the artwork is drawn 32x16 at 1:1 inside that 32x32 box — letterboxed, not stretched, which is what
|
||||
keeps its 2px bars crisp. It reads `0x0` on the first tick, where nothing has been allocated yet, and
|
||||
is therefore read by the exit probe with the rest of the sidebar's geometry. `size=` is the bar's own
|
||||
size, and it is the answer to "what did the wider glyphs cost": `36` is what it was with 16px icons,
|
||||
because the pill's fixed height takes a 32px box without growing — the glyph gets twice the width for
|
||||
no vertical room at all.
|
||||
- `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)` (in the exit
|
||||
dump, beside `toolbar icons:`) — each tab's own glyph and `gtk_icon_theme_has_icon()`'s verdict, read
|
||||
on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are
|
||||
|
||||
@@ -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
|
||||
`-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
|
||||
**11309** against C99's 4095-byte string-literal limit with the seven action icons and the two app icons
|
||||
**11661** 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
|
||||
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`
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- The Measure tab: the cursors and the ruler they read against, in one glyph. Two markers, each a
|
||||
cap over a dashed shaft, stand across a ruler: the ruler is two bars with three hanging ticks.
|
||||
The ruler's ends are open on purpose. A closed frame puts four more pixels of ink at the left
|
||||
and right edges, which at this size is what turns a ruler into a rectangle. The shafts stop at
|
||||
the bars rather than crossing them, the way a marker on a plot sits over the scale. Whole
|
||||
pixels, filled `path`s wound the same way, and an ink fraction of 36%, in line with the ruler
|
||||
and the cursors it is made of. -->
|
||||
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M3 1 H7 V3 H3 Z
|
||||
M9 1 H13 V3 H9 Z
|
||||
M4 3 H6 V6 H4 Z
|
||||
M10 3 H12 V6 H10 Z
|
||||
M4 14 H6 V16 H4 Z
|
||||
M10 14 H12 V16 H10 Z
|
||||
M2 6 H14 V8 H2 Z
|
||||
M2 12 H14 V14 H2 Z
|
||||
M4 8 H5 V11 H4 Z
|
||||
M8 8 H9 V10 H8 Z
|
||||
M12 8 H13 V11 H12 Z"/>
|
||||
<!-- The Measure tab, as two motifs side by side in a 32x16 cell: the two cursors on the left (a cap
|
||||
over a dashed shaft, the shape the toolbar's cursors button has) and the ruler they read against
|
||||
on the right. The ruler is a closed frame with its ticks hanging inside it, which needs
|
||||
`fill-rule="evenodd"`: the ticks sit inside the frame's hole, three crossings deep, so they fill
|
||||
rather than punching a second hole. The cursors do not overlap the ruler, so one evenodd path
|
||||
carries both without either cancelling. Rendered at 32, i.e. 1:1, so the 2px bars stay crisp,
|
||||
and every coordinate is a whole pixel of the cell. As with the Signals glyph, GDK's box is
|
||||
square whatever the document's aspect: this pair is letterboxed, not stretched. -->
|
||||
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" fill-rule="evenodd" d="M1 1 H7 V4 H1 Z
|
||||
M3 5 H6 V8 H3 Z
|
||||
M3 9 H6 V12 H3 Z
|
||||
M3 13 H6 V16 H3 Z
|
||||
M9 1 H15 V4 H9 Z
|
||||
M11 5 H14 V8 H11 Z
|
||||
M11 9 H14 V12 H11 Z
|
||||
M11 13 H14 V16 H11 Z
|
||||
M17 4 H31 V12 H17 Z
|
||||
M19 6 H29 V10 H19 Z
|
||||
M21 6 H22 V9 H21 Z
|
||||
M25 6 H26 V9 H25 Z
|
||||
M28 6 H29 V8 H28 Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,20 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- The Signals tab: the trigger *and* the channel list it acts on, in one glyph. A rising edge
|
||||
crossing a dashed level is the trigger as the toolbar already draws it; below it, two rows of a
|
||||
colour chip and a name are the channel list, which is what the page mostly is. Two rows and not
|
||||
three, so the edge and the list each get half the cell: at 16px a third row costs the edge its
|
||||
clearance and the whole thing turns into a grid. Whole pixels, filled `path`s wound the same
|
||||
way, and the 2px bars match the house stroke weight. -->
|
||||
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M6 1 H8 V6 H6 Z
|
||||
M6 1 H15 V3 H6 Z
|
||||
M0 5 H2 V7 H0 Z
|
||||
M3 5 H5 V7 H3 Z
|
||||
M12 5 H14 V7 H12 Z
|
||||
M1 9 H4 V12 H1 Z
|
||||
M6 10 H14 V12 H6 Z
|
||||
M1 13 H4 V15 H1 Z
|
||||
M6 13 H14 V15 H6 Z"/>
|
||||
<!-- The Signals tab, as two motifs side by side in a 32x16 cell: the trigger on the left (a rising
|
||||
edge crossing a dashed level, the shape the toolbar's trigger button has) and three rows of a
|
||||
colour chip and a name on the right, which is the channel list the tab mostly is. Wider than
|
||||
tall because it has to hold *two* motifs: a 16px square cell can carry one, and squeezing the
|
||||
pair into it is what turns them into a smear. The pill renders this at 32, i.e. 1:1, so the 2px
|
||||
bars stay crisp; a fractional scale would blur them, which is why every coordinate is a whole
|
||||
pixel. GDK paints an icon into a *square* box whatever the document's aspect, so this pair is
|
||||
letterboxed in a 32x32 box rather than stretched (measured: a 32x16 render inside a 32 request).
|
||||
All subpaths are filled `path`s wound the same way. -->
|
||||
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M4 2 H6 V12 H4 Z
|
||||
M6 2 H14 V4 H6 Z
|
||||
M0 7 H2 V9 H0 Z
|
||||
M8 7 H10 V9 H8 Z
|
||||
M12 7 H14 V9 H12 Z
|
||||
M18 2 H21 V5 H18 Z
|
||||
M23 3 H30 V5 H23 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>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.5 KiB |
+16
-2
@@ -2903,6 +2903,17 @@ void name_side_tabs(App& app) {
|
||||
}
|
||||
};
|
||||
hide_labels(pill);
|
||||
// The tabs' glyphs are 32x16 documents, and the pill has the width for them: drawn at 32 they are
|
||||
// 1:1, so both motifs sit side by side at the size they were authored at and the 2px bars stay
|
||||
// crisp. A 16px box would scale the pair down to a smear, and a fractional scale would blur it.
|
||||
const std::function<void(GtkWidget*)> size_images = [&](GtkWidget* w) {
|
||||
if (GTK_IS_IMAGE(w)) gtk_image_set_pixel_size(GTK_IMAGE(w), 32);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
size_images(c);
|
||||
}
|
||||
};
|
||||
size_images(pill);
|
||||
app.side_tabs_named = true;
|
||||
}
|
||||
}
|
||||
@@ -5674,9 +5685,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
||||
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
||||
app.window_limit_s);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d\n",
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d "
|
||||
"size=%dx%d\n",
|
||||
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_names);
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_names,
|
||||
app.dbg_switcher_icon_w, app.dbg_switcher_icon_h, app.dbg_switcher_w,
|
||||
app.dbg_switcher_h);
|
||||
std::printf("[udpscope] mode with trigger on: %s\n", app.dbg_mode_triggered.c_str());
|
||||
std::printf("[udpscope] trigger marker x=%.3f, view x_max=%.3f; model trigger_time=%.3f "
|
||||
"valid=%d\n",
|
||||
|
||||
@@ -219,6 +219,14 @@ struct App {
|
||||
int dbg_switcher_pills = -1;
|
||||
int dbg_switcher_labels = -1;
|
||||
int dbg_switcher_names = -1;
|
||||
/// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what
|
||||
/// keeps its 2px bars crisp.
|
||||
int dbg_switcher_icon_w = -1;
|
||||
int dbg_switcher_icon_h = -1;
|
||||
/// The switcher's own size, beside it: the pills carry 32px icon boxes now, so the tab bar's height
|
||||
/// is what it costs.
|
||||
int dbg_switcher_w = -1;
|
||||
int dbg_switcher_h = -1;
|
||||
int dbg_switcher_policy = -1;
|
||||
/// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped.
|
||||
bool side_tabs_named = false;
|
||||
|
||||
@@ -78,6 +78,28 @@ gboolean exit_probe_step3(gpointer user) {
|
||||
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
|
||||
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
|
||||
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
|
||||
// The switcher's icon as it is *rendered*: a 32x16 glyph in a 32px box draws 1:1, which is what
|
||||
// keeps its 2px bars crisp rather than blurred by a fractional scale. Read here and not on the
|
||||
// first tick, where the window has not been allocated yet and every widget still measures 0x0.
|
||||
if (a->side_switcher != nullptr) {
|
||||
a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher);
|
||||
a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher);
|
||||
}
|
||||
for (GtkWidget* pill = a->side_switcher != nullptr ? gtk_widget_get_first_child(a->side_switcher)
|
||||
: nullptr;
|
||||
pill != nullptr; pill = gtk_widget_get_next_sibling(pill)) {
|
||||
const std::function<void(GtkWidget*)> measure_icon = [&](GtkWidget* node) {
|
||||
if (GTK_IS_IMAGE(node) && gtk_widget_get_mapped(node)) {
|
||||
a->dbg_switcher_icon_w = std::max(a->dbg_switcher_icon_w, gtk_widget_get_width(node));
|
||||
a->dbg_switcher_icon_h = std::max(a->dbg_switcher_icon_h, gtk_widget_get_height(node));
|
||||
}
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(node); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
measure_icon(c);
|
||||
}
|
||||
};
|
||||
measure_icon(pill);
|
||||
}
|
||||
// The height of one signal row, which is the thing "the channel toggle is too tall"
|
||||
// was about. The panel's own height cannot stand in for it: it also covers the
|
||||
// heading, the hint and the box margins.
|
||||
|
||||
Reference in new issue
Block a user