help: a tinier (i), hints beside their controls, and the tour starting in the chooser

The contextual help becomes the affordance the ask names: a plain (i)
(`dialog-information-symbolic`) at **16x16**, and one that sits beside the
control it explains rather than only in a pane's heading.

- The mark: `GtkMenuButton` ignores `min-width` on the widget and a size
  request, so the size is made in `install_css()` on all three nodes (outer
  `menubutton`, its `button` subnode, the glyph). Measured 16x16 against the
  default 34x34; the dump prints it as `hint=WxH`, measured with
  `gtk_widget_compute_bounds()` on the first *visible* hint.
- More sites: `events_record` (the "record events" switch in the trigger
  panel), `trigger_advanced` (the pre-trigger share row), `signals_config`
  (the signals pane's hint line, wrapped in a box so a hint can join it).
  Hints join as siblings, so none can cover a switch, pencil or dropdown.
- The source chooser gets its own layer -- `chooser_sources` (a new heading
  above the saved-source list) and `chooser_waveform` (the waveforms action
  row) -- built with the one `context_help_button()` both windows use and
  re-synced by its Help menu's toggle. The form's `AdwEntryRow`s cannot host a
  sibling hint, so none is invented for the host/port fields.
- The tour is one implementation over two windows: `ui_impl::TourView` plus a
  list of `TourStop`s; each window keeps its own table. A walk that passes its
  last stop calls `on_done`, and the chooser's sets `Config::tour_pending`, so
  a first run is one tour that crosses from the chooser into the app. The
  chooser's tour auto-starts from `settings.tour_seen` exactly as the window's
  does, and its "Guided tour" row now starts *it* instead of only requesting
  the window's.
- Probes: `UDPSCOPE_PROBE_CHOOSER_TOUR=1` / `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`,
  the hint size in the `help layer:`/`[helpui] contextual on:` lines, and the
  chooser's hints in its `chooser ui:` line. Docs updated in
  invariants-gtk-and-chrome.md, probe-readings.md and verification.md.
This commit is contained in:
Martino Ferrari committed 2026-10-14 22:42:38 +02:00
1 parent 395c9caec3
commit 829176248a
14 files changed
+911 -303

No files matched your search

+77 -18
View File
@@ -395,21 +395,32 @@ Two lines of the self-check output are assertions in disguise:
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.
- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1 requested=0` (in the exit
- `help layer: contextual=on hints=11 ids={events plot status cursors measures trigger trigger_advanced events_record signals signals_config window} icon=dialog-information-symbolic(ok) hint=16x16; tour=idle steps=5 step=0 auto=1 requested=0` (in the exit
dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal
the number of ids**, and the ids are the surfaces the contextual help is on: the eight of the window
the number of ids**, and the ids are the surfaces the contextual help is on: ten of the window
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt
on every opening, so it is not one of the window's own). The layer is **off by default**
on every opening, so it is not one of the window's own). Three of the ten are not a pane's *heading*
but the row of a specific control — `events_record` (the "record events" switch in the trigger
panel), `trigger_advanced` (the pre-trigger share row) and `signals_config` (the signals pane's hint
line, whose box exists so a hint can join it) — which is the rule the layer follows: a hint is a
sibling of the thing it explains and never an overlay on it. The layer is **off by default**
(`contextual_help` in `settings.toml` is false for a missing file), so a fresh-config run reads
`contextual=off hints=0` and that line above is the state *after* the Help row was switched on;
`contextual=off` beside a non-zero count is the toggle not having removed its buttons — **off is
*gone*, not greyed**. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a
*blank* control that still works, the failure the pane [edit] buttons already paid for once. `tour=`
is `running`/`idle` with the plan's `steps=` and the current `step=`; `auto=1` is the first-run
auto-start having fired *in this process* — the flag is `tour_seen` in `settings.toml`, so a second run
against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour"
(`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not
the auto-start.
*gone*, not greyed**. **`icon=` must read `(ok)`**: an "(i)" whose name Adwaita lacks is a
*blank* control that still works, the failure the pane [edit] buttons already paid for once. The name
is `dialog-information-symbolic`, the plain information mark; `help-about-symbolic` is the status
bar's ⓘ and a second meaning for it is what the app's own icons exist to avoid. `hint=WxH` is the
size the **first visible** hint was allocated, measured the way every other rectangle here is
(`gtk_widget_compute_bounds()`), and **16x16 is the reading a "much tinier" mark must keep**: the
class zeroes the outer `menubutton` node, its `button` subnode and the glyph, which is what a
`GtkMenuButton` needs (its measure ignores both `min-width` on the widget and a size request) — the
default 34x34 is the number to compare against. Hints inside a collapsed expander are skipped, so the
number is never a hidden widget's 0x0. `tour=` is `running`/`idle` with the plan's `steps=` and the
current `step=`; `auto=1` is the first-run auto-start having fired *in this process* — the flag is
`tour_seen` in `settings.toml`, so a second run against the same config directory reads `auto=0` —
and `requested=1` is the **chooser's** tour having handed over (`Config::tour_pending`), which is
`auto=0` because the request came from the other window rather than from this process's first run.
- `gtk warnings: N` (in the exit dump, beside `help layer:`) — **GTK's own log at warning or above,
counted for the whole run, and it must read `0`.** It is counted through a log *writer* function
because GTK is built with structured logging: its messages go through the writer and never through
@@ -472,6 +483,50 @@ Two lines of the self-check output are assertions in disguise:
connect_sensitive=1` then a `picked` line, with no SIGSEGV, which is as much a crash test of a
dialog built and destroyed as it is a state check. An empty list must print `picked nothing:
the list is empty` rather than dereferencing a null row.
**The command that reaches the chooser at all is `UDPSCOPE_PROBE_PICK_MS`, and this is worth
saying because three earlier attempts found no chooser and no output:** with no `--server` and no
probe, the chooser runs its own nested `GMainLoop` and *blocks* — there is nothing to time it out
and no dump runs, so a bare `./build/udpscope` (bare or under a pty) sits there silently. The
working form, against the mock:
```sh
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=6000 UDPSCOPE_PROBE_PICK_MS=900 \
./build/udpscope
```
(with `XDG_CONFIG_HOME` pointing at a directory holding at least one saved source, or the pick
finds an empty list and the run prints `picked nothing`). It is the same command the docs give
elsewhere with `--server` removed and `UDPSCOPE_PROBE_PICK_MS` added.
- `[sources] chooser ui: … hints=[N {ids}] hint=WxH` (stderr, `UDPSCOPE_PROBE_PICK_MS`) — the
chooser's frame *and* its help layer in one line. `hints=[2 {chooser_sources chooser_waveform}]`
is the layer **on** (`contextual_help` in the file the run reads) and `hints=[0 {}] hint=0x0` is
it **off**, which is the same "off is gone" rule the window's line reads, in the window that runs
before `App` exists. The two ids are the surfaces this dialog has a row for — the heading above
the saved-source list and the waveforms page's action row; the form's `AdwEntryRow`s are *not*
hosts (a row is not a box and its editor is internal, so a hint cannot join it as a sibling) and
the connection fields are explained by their group's `description` instead. `hint=WxH` is the
same 16x16 the window's line reads: the button is `ui_impl::context_help_button()`, the one
`udpscope-hint` affordance both windows draw.
- `[sources] tour: first run -- auto_started=1 in the chooser` then
`[sources] chooser tour: visible=1 steps=5 step=0` (stderr) — **a first run's tour starts in the
chooser**, on the chooser's own widgets, from the same `settings.tour_seen` the window's
auto-start reads and with the same 1.5 s delay. `steps=5` is the chooser's plan (the source list,
the two tabs, New host, the help menu and the preferences). Driven by the walk itself:
- `UDPSCOPE_PROBE_CHOOSER_TOUR=1` emits `::clicked` on the help menu's **Guided tour** row, which
now starts *this* walk rather than only requesting the window's: `[sources] chooser help: tour
started in the chooser`, then `[sources] chooser tour: visible=1 steps=5 step=0`, then a walking
`[sources] chooser tour: next -> step=N/5` per press, then `[sources] chooser tour: finished 5
stops -- the window continues it (tour_pending=1)`. **`tour_pending=1` and `visible=0` is the
handoff assertion** — the walk is over here and the request rides on the one `Config` value
`main()` hands to both windows.
- `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` schedules the same walk on a timer, for reading a tour
**the run itself started** (the first-run auto-start) rather than one the probe started; it must
be later than 1500 ms. Paired with `UDPSCOPE_PROBE_PICK_MS` a few hundred ms later, the run then
shows the other half: the application's `help layer:` reads `tour=running … auto=0 requested=1`,
the window's tour having been started by `tour_start_requested()` rather than by the first-run
timer.
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
with a drop and a window change, then again with no interaction: the second run's dump
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
@@ -685,12 +740,16 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
touches anything: `auto_started=1 visible=1` is the tour that started itself, and the parenthetical is
`settings.tour_seen` as the process found it — `tour_seen already set` with `auto=0` is the *second*
run against the same config directory, which is the assertion that the flag persisted. Then:
- `[helpui] contextual on: hints=8 ids={events plot status cursors measures trigger signals window}` —
the eight "?" affordances, by id. The layer's ninth surface, `math`, is not here: its host exists
only while the math editor window is open. **On a fresh config this reads `hints=0 ids={}`**, because
the layer is off by default; the example is the state after the row below was switched on, and a
fresh run's `hints=0` beside a `settings.toml` with no `contextual_help` key is the default being
read rather than assumed.
- `[helpui] contextual on: hints=11 ids={events plot status cursors measures trigger trigger_advanced events_record signals signals_config window} hint=16x16` —
the eleven "(i)" affordances, by id, and the size the first visible one was allocated. Ten are the
window's own surfaces (three of them a specific control's row rather than a heading — see the
`help layer:` entry) and the eleventh is `math`, which is present only while the math editor
window is open; it reads 10 with the editor shut. **On a fresh config this reads `hints=0 ids={}`
and `hint=0x0`**, because the layer is off by default; the example is the state after the row below
was switched on, and a fresh run's `hints=0` beside a `settings.toml` with no `contextual_help`
key is the default being read rather than assumed. **`hint=16x16` is the "much tinier" assertion**
— the default menu-button mark is 34x34 (see `help layer:`), and anything near that is the class
not reaching the outer `menubutton` node.
- `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '<label>' accel='<key>' order=O`
per row of the **Help view** — `entry 0 'Back'` with no accelerator and then the four the submenu
promises: `entry 1 'Keyboard shortcuts' accel='Ctrl+/'`, `entry 2 'Documentation' accel='F1'`,
@@ -701,10 +760,10 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
`toggle=` being the contextual-help row's check, which *is* `settings.contextual_help`. It reads `0`
on a fresh config (the default) and `1` on one where the setting is on; the probe's next step drives
it both ways, which is why the example here is the `1` side.
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=8 ids={…}; setting=on` — the toggle's effect
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=11 ids={…}; setting=on` — the toggle's effect
read both ways through the row itself. **`hints=0` with `off` is the assertion**: off means the
affordances are *gone*, not greyed, so anything else is a row that hid its buttons instead of
removing them. The count comes back to the same eight, which is the other half — a toggle that
removing them. The count comes back to the same eleven, which is the other half — a toggle that
removed and could not restore is a one-way switch.
- `[helpui] tour started from the probe: visible=1 steps=5 step=0` — the tour started the way
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose target was on screen