ui: the tabs are icons, and the pills keep their names

Each sidebar page gets its own glyph and no visible text. The Measure tab reuses the ruler
(`udpscope-measure-symbolic`, the icon its deleted toolbar toggle used and the Plot preferences page
still carries); the Signals tab gets a new `udpscope-signals-symbolic` — three rows of a colour chip
and a name, which is what that tab mostly is — rather than a second copy of the trigger glyph, since
one shape meaning two things in one window is worse than a new one. Its bars are all one length: a
set of unequal ones reads as a bar chart, and a chart is not a list.

`AdwViewSwitcherPolicy.narrow` alone does **not** give an icon-only bar. Measured on libadwaita 1.10
by walking the switcher's tree: with `narrow` and a title, each pill holds a `GtkImage` *and* a
`GtkLabel` — 48px of "Measure" inside a 54px pill. So the pages' `title` is **empty** (their `name` is
what identifies them to `select_side_tab()` and to the session file, and both are unchanged) and the
words are re-attached in code: `name_side_tabs()` runs on the first tick — the pills are
`AdwViewSwitcher`'s own and exist only once it is mapped — and gives each pill a **tooltip and an
accessible name** from the page's name, then hides the empty label that would otherwise leave the
icon off-centre. An icon-only pill is still a named control for a screen reader, and the page icon
that does not resolve leaves a *blank* pill, which is why it is read like the toolbar's icons are.

Two readings, because the claim is two claims:

    switcher: policy=narrow pills=2 labels=0 names=2
    page icons: measure=udpscope-measure-symbolic(ok) signals=udpscope-signals-symbolic(ok)

`labels` counts *mapped labels with text* — a plain mapped count reads 2 either way, because the
narrow pill keeps an empty label in its layout — and `pills=2` is the guard that makes it mean
something: with nothing mapped the rest of the line is void rather than a pass.

Docs: the icon census (six action icons, eight bundled, and what each of the two new users is), the
switcher bullet in `invariants-gtk-and-chrome.md` with the measurement above, the two new dump lines
in `probe-readings.md`, the re-measured GResource figures in `verification.md` (icons 8613 -> 9842
bytes, help 514660 after this session's help rewrite; the count stays four), and the README recording
that the design spec had the switcher showing titles and neither page an icon.

Verified: `make test-ui` 14 files; `[side-tab-probe] … advanced=4` (tab switching by name still
works with empty titles); `signals panel: … divider=1`; `[math-probe] 3 math channels`; zero
`Gtk-CRITICAL` and no missing icon.
This commit is contained in:
Martino Ferrari committed 2026-10-14 13:29:11 +02:00
1 parent f9cea4885b
commit 0b146f1d53
9 files changed
+201 -16

No files matched your search

+6 -2
View File
@@ -2393,8 +2393,12 @@ now — they are listed so they are not mistaken for anything else:
signal list — in one scrolling column; with four of them open in a 320 px sidebar every signal list — in one scrolling column; with four of them open in a 320 px sidebar every
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), with the math list folded into the signal list under a tab (the trigger and the channels), **each a pill with its own glyph and no text** — the ruler
`MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in for Measure, a channel list for Signals — 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
list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in
the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no
toggle hides a pane any more: trigger-off and cursors-off **disable** their pane and leave toggle hides a pane any more: trigger-off and cursors-off **disable** their pane and leave
it in place, and the measurements' and math panes' toolbar toggles are gone with the panes. it in place, and the measurements' and math panes' toolbar toggles are gone with the panes.
+18 -4
View File
@@ -55,10 +55,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else measurement ruler, and the stock names those buttons used to carry said something else
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors, entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows}-symbolic.svg` measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows,signals}-symbolic.svg`
are the five action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last are the six action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last
user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it -- user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it --
an icon with a live user is not an orphan, whatever the toggle that introduced it did. Beside them in an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is
*also* the Measure tab's icon. `udpscope-signals-symbolic` was drawn for the Signals tab: three rows of
a colour chip and a name, which is what that tab mostly is, and deliberately *not* a second copy of the
trigger glyph (one shape with two meanings in one window is worse than a new one). Its bars are all one
length because unequal ones read as a bar chart, and a chart is not a list. Beside them in
`src/icons/scalable/apps/` are the two `src/icons/scalable/apps/` are the two
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and **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 `org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an
@@ -67,7 +71,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed 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 copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell
cannot read a GResource compiled into the binary). cannot read a GResource compiled into the binary).
All seven are bundled by `src/icons/udpscope.gresource.xml` (CMake runs All eight are bundled by `src/icons/udpscope.gresource.xml` (CMake runs
`glib-compile-resources`) and reached through **one** `glib-compile-resources`) and reached through **one**
`gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top `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, of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon,
@@ -195,6 +199,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not* allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
current -- a change in what the reading means, not a defect, and no functional code may depend on current -- a change in what the reading means, not a defect, and no functional code may depend on
those numbers. those numbers.
- **`AdwViewSwitcherPolicy.narrow` still draws a label; an *empty title* is what makes a pill an icon.**
Measured on libadwaita 1.10: with `narrow` and a title, the pill holds a `GtkImage` **and** a
`GtkLabel` — 48px of "Measure" inside a 54px pill — so the policy alone does not give an icon-only tab
bar. The sidebar's pages therefore have **no title** (their `name` is what identifies them to
`select_side_tab()` and the session file) and the pills are *named in code* instead: the first tick
calls `name_side_tabs()`, which walks the switcher's children — the pills are there in page order, and
`AdwViewSwitcherButton` is not a public type, so there is no type check to make — and sets each pill's
**tooltip and accessible name** from the page's name, then hides the empty label that would otherwise
leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and
`switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once.
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to - **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
+12
View File
@@ -357,6 +357,18 @@ 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 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 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. 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
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.
- `page icons: measure=udpscope-measure-symbolic(ok) signals=udpscope-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
empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks
exactly like a page with nothing in it.
- `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump, - `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump,
beside `toolbar icons:`) — the application icon, named and present at the size its own root beside `toolbar icons:`) — the application icon, named and present at the size its own root
`<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is `<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is
+5 -4
View File
@@ -15,11 +15,12 @@ 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' (**514532** bytes) and the icons', whose blob is generated GResource `.c` files — the help pages' (**514660** bytes) and the icons', whose blob is
**8613** against C99's 4095-byte string-literal limit with the five action icons and the two app icons **9842** against C99's 4095-byte string-literal limit with the six 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
`udpscope-math-symbolic` was deleted, which did not move the count; re-measure by touching the two the tab work changed the set (that icon left, `udpscope-signals-symbolic` arrived, and this file's
`*.gresource.xml` files and rebuilding. The fifth warning that used to appear here was an unused help page was rewritten), which did not move the count; re-measure by touching each `*.gresource.xml`
under `src/` and rebuilding. The fifth warning that used to appear here was an unused
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the `output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
`output_after_add` already printed on the same line — and that deletion is what dropped the baseline `output_after_add` already printed on the same line — and that deletion is what dropped the baseline
from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!-- The Signals tab's own glyph: the channel list, drawn as a list. Three rows of a
colour chip and a name, the shape the signal panel's own rows have — which is
what the tab mostly *is*, and which is why it is not the trigger icon: the
toolbar's trigger button already carries that one, and one glyph with two
meanings in one window is worse than a new glyph. The bars are all one length
on purpose: unequal ones read as a bar chart, and a chart is not a list. Every
coordinate is a whole pixel of the 16px cell, every subpath is a filled `path`
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="M1 1 H4 V4 H1 Z
M6 2 H14 V4 H6 Z
M1 6 H4 V9 H1 Z
M6 7 H14 V9 H6 Z
M1 11 H4 V14 H1 Z
M6 12 H14 V14 H6 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+1
View File
@@ -20,6 +20,7 @@
<file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-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-cursors-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-signals-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file> <file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file>
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file> <file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
+109 -2
View File
@@ -2865,6 +2865,48 @@ std::string icon_report(GtkIconTheme* theme, const char* name, const char* resou
/// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the /// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the
/// window is alive, neither of which is true when the dump runs -- so the dump can print a fact /// window is alive, neither of which is true when the dump runs -- so the dump can print a fact
/// instead of asking GTK about a destroyed widget. /// instead of asking GTK about a destroyed widget.
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
static const char* side_tab_title(const char* name) {
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
return "";
}
/// Names the switcher's pills and takes their empty labels out of the layout: an empty `GtkLabel`
/// still asks for its own width, which leaves the icon off-centre in a pill meant to be one glyph.
/// Called from the first tick, because the pills are `AdwViewSwitcher`'s own and exist only once the
/// widget has been mapped.
void name_side_tabs(App& app) {
if (app.side_tabs_named || app.side_switcher == nullptr) return;
GListModel* pages = app.side_stack != nullptr
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
: nullptr;
// The switcher's children *are* its pills, in page order, and `AdwViewSwitcherButton` is not a
// public type (there is no `ADW_IS_VIEW_SWITCHER_BUTTON`), so the two lists are walked together by
// index rather than checked by type.
GtkWidget* pill = gtk_widget_get_first_child(app.side_switcher);
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
pill = gtk_widget_get_next_sibling(pill), ++i) {
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
const char* title = side_tab_title(adw_view_stack_page_get_name(page));
g_object_unref(page);
if (title[0] == '\0') continue;
gtk_widget_set_tooltip_text(pill, title);
gtk_accessible_update_property(GTK_ACCESSIBLE(pill), GTK_ACCESSIBLE_PROPERTY_LABEL, title, -1);
const std::function<void(GtkWidget*)> hide_labels = [&](GtkWidget* w) {
if (GTK_IS_LABEL(w)) gtk_widget_set_visible(w, FALSE);
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
hide_labels(c);
}
};
hide_labels(pill);
app.side_tabs_named = true;
}
}
void note_panel_controls(App& app) { void note_panel_controls(App& app) {
if (!app.dbg_panel_controls.empty()) return; if (!app.dbg_panel_controls.empty()) return;
const auto in_bar = [](GtkWidget* w) { const auto in_bar = [](GtkWidget* w) {
@@ -2941,6 +2983,62 @@ void note_panel_controls(App& app) {
" " + " " +
icon_report(theme, "org.udpscope.app-symbolic", icon_report(theme, "org.udpscope.app-symbolic",
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg"); "/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
// The two sidebar pages' own icons, named and resolved the same way, and what the switcher is
// drawing for them. The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped,
// so this reads on the first tick: `pills` is the guard (0 pills would make a label count of 0
// meaningless) and `labels` must be 0, because the switcher's policy is `narrow` -- the pages are
// icons, not words, in a 300px column.
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`
// (only its `child` is a widget), so `gtk_widget_get_first_child()` on the stack yields the pages'
// scrolled windows and this line read empty the first time it was written.
GListModel* pages = app.side_stack != nullptr
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
: nullptr;
for (guint i = 0; pages != nullptr && i < g_list_model_get_n_items(pages); ++i) {
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
const char* page_name = adw_view_stack_page_get_name(page);
const char* icon = adw_view_stack_page_get_icon_name(page);
app.dbg_page_icons += std::string(app.dbg_page_icons.empty() ? "" : " ") +
(page_name != nullptr ? page_name : "?") + "=" +
(icon != nullptr ? icon : "-") +
(icon != nullptr && gtk_icon_theme_has_icon(theme, icon) ? "(ok)"
: "(MISSING)");
g_object_unref(page);
}
// `mapped`, not `visible`: visibility is per widget, so a label whose *parent* is hidden still
// reads visible, and in the narrow policy that is exactly the case. `mapped` requires every
// ancestor, which is what "on screen" means -- and it is why the pill count is read the same way:
// if nothing is mapped the pill count reads 0 and the label count means nothing, rather than
// passing vacuously.
int pills = 0;
int drawn_labels = 0;
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
// A *mapped* label with text: libadwaita's narrow pill keeps an empty label in the layout, so
// "mapped" alone would count the empty ones and read 2 whether the pills were icons or words.
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
++drawn_labels;
}
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
count_drawn(c);
}
};
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
app.dbg_switcher_pills = pills;
app.dbg_switcher_labels = drawn_labels;
int named_pills = 0;
for (GtkWidget* w = app.side_switcher != nullptr ? gtk_widget_get_first_child(app.side_switcher)
: nullptr;
w != nullptr; w = gtk_widget_get_next_sibling(w)) {
const char* tip = gtk_widget_get_tooltip_text(w);
if (tip != nullptr && tip[0] != '\0') ++named_pills;
}
app.dbg_switcher_names = named_pills;
if (app.side_switcher != nullptr) {
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
}
} }
/// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window /// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window
@@ -3128,6 +3226,11 @@ void update_capture_row(App& app) {
gboolean tick(gpointer user) { gboolean tick(gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
if (app->window == nullptr) return G_SOURCE_REMOVE; if (app->window == nullptr) return G_SOURCE_REMOVE;
// Before the reading, and not after it: the pills are `AdwViewSwitcher`'s own widgets and exist
// only once the switcher is mapped, so this is the first frame that can name them -- and their
// page titles are deliberately empty, so a frame that read before this ran would read pills with
// no text and no name either.
name_side_tabs(*app);
note_panel_controls(*app); note_panel_controls(*app);
ScopeModel& model = *app->model; ScopeModel& model = *app->model;
const gint64 tick_start_us = g_get_monotonic_time(); const gint64 tick_start_us = g_get_monotonic_time();
@@ -5563,14 +5666,17 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
inside(app.events_list, app.events_revealer)); inside(app.events_list, app.events_revealer));
} }
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d " std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d cursors=%d window=%.3fs " "(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d "
"window_cap=%.1fs\n", "cursors=%d window=%.3fs window_cap=%.1fs\n",
app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w, app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w,
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current, app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current,
app.dbg_trigger_w, app.dbg_trigger_h, app.dbg_trigger_w, app.dbg_trigger_h,
app.dbg_trigger_sensitive, app.dbg_switcher_visible, app.dbg_trigger_sensitive, app.dbg_switcher_visible,
rows.cursors_visible() ? 1 : 0, rows.time_window(), rows.cursors_visible() ? 1 : 0, rows.time_window(),
app.window_limit_s); app.window_limit_s);
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%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);
std::printf("[udpscope] mode with trigger on: %s\n", app.dbg_mode_triggered.c_str()); 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 " std::printf("[udpscope] trigger marker x=%.3f, view x_max=%.3f; model trigger_time=%.3f "
"valid=%d\n", "valid=%d\n",
@@ -5692,6 +5798,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
// critical (which is how the first version of this line earned four of them). // critical (which is how the first version of this line earned four of them).
std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str()); std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str());
std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str()); std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str());
std::printf("[udpscope] page icons: %s\n", app.dbg_page_icons.c_str());
std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str()); std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str());
// The measurements pane, with the numbers the engine computed: a probe adds measurements and // The measurements pane, with the numbers the engine computed: a probe adds measurements and
// this is where their values are read, so a wrong threshold or a window that moved under a // this is where their values are read, so a wrong threshold or a window that moved under a
+19 -4
View File
@@ -52,14 +52,25 @@
heights, so one shared scroller around the stack would leave a tab switch scrolled heights, so one shared scroller around the stack would leave a tab switch scrolled
to somewhere the new page never asked for. The switcher is the sibling above, not a to somewhere the new page never asked for. The switcher is the sibling above, not a
child of either scroller: the one control that chooses what you are looking at must child of either scroller: the one control that chooses what you are looking at must
not be the first thing to scroll away. --> not be the first thing to scroll away.
The policy is `narrow` and the **titles are empty**: `narrow` still draws a label
beside an icon (measured: a 48px "Measure" inside a 54px pill), so an empty title is
what makes a pill an icon. The pages' *names* are what identify them to
`select_side_tab()` and the session file, and the words are re-attached to the pills in
code — tooltip and accessible name — by `name_side_tabs()` on the first tick, so an
icon-only pill is still a named thing rather than an anonymous button.
Each page's icon is its own glyph: `udpscope-measure-symbolic` is the ruler the
measurements used as a toggle, and `udpscope-signals-symbolic` is the channel list,
drawn for this tab rather than reusing the toolbar's trigger icon — one glyph
meaning two things in one window is worse than a new one. -->
<object class="GtkBox" id="side_column"> <object class="GtkBox" id="side_column">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="width-request">300</property> <property name="width-request">300</property>
<child> <child>
<object class="AdwViewSwitcher" id="side_switcher"> <object class="AdwViewSwitcher" id="side_switcher">
<property name="stack">side_stack</property> <property name="stack">side_stack</property>
<property name="policy">wide</property> <property name="policy">narrow</property>
<property name="margin-top">4</property> <property name="margin-top">4</property>
<property name="margin-bottom">4</property> <property name="margin-bottom">4</property>
<property name="margin-start">6</property> <property name="margin-start">6</property>
@@ -72,7 +83,9 @@
<child> <child>
<object class="AdwViewStackPage"> <object class="AdwViewStackPage">
<property name="name">measure</property> <property name="name">measure</property>
<property name="title" translatable="yes">Measure</property> <property name="title"></property>
<!-- The ruler: what the pane measures the waveform with. -->
<property name="icon-name">udpscope-measure-symbolic</property>
<property name="child"> <property name="child">
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property> <property name="hscrollbar-policy">never</property>
@@ -89,7 +102,9 @@
<child> <child>
<object class="AdwViewStackPage"> <object class="AdwViewStackPage">
<property name="name">signals</property> <property name="name">signals</property>
<property name="title" translatable="yes">Signals</property> <property name="title"></property>
<!-- The channel list this page mostly is. -->
<property name="icon-name">udpscope-signals-symbolic</property>
<property name="child"> <property name="child">
<object class="GtkScrolledWindow"> <object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property> <property name="hscrollbar-policy">never</property>
+12
View File
@@ -210,6 +210,18 @@ struct App {
/// works, and every other line of the dump still passes. /// works, and every other line of the dump still passes.
std::string dbg_toolbar_icons; std::string dbg_toolbar_icons;
std::string dbg_app_icons; std::string dbg_app_icons;
/// The two sidebar pages' own icons and whether the theme resolved them, read on the first tick
/// like the two lines above.
std::string dbg_page_icons;
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The label
/// count is the one that says the pages are icons rather than words: `narrow` hides them, and the
/// titles stay set because they are the pages' names for a screen reader and for the tooltip.
int dbg_switcher_pills = -1;
int dbg_switcher_labels = -1;
int dbg_switcher_names = -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;
/// The header bar, so the dump can report the order the controls are in: a header bar is a /// The header bar, so the dump can report the order the controls are in: a header bar is a
/// *titlebar*, not a child of the window, so walking down from the window does not reach it. /// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr; GtkWidget* toolbar = nullptr;