diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index 9fc4048..2c3451d 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -193,13 +193,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear and this dialog binds none, so a key printed here would be documentation that does nothing. *Documentation* calls `show_help()` straight from the chooser (the manual is another process and needs nothing of the app); *Enable contextual help* writes the settings file, which is what the app - reads when it starts a moment later; *Guided tour* cannot start anything — the tour is drawn on the - app's own window — so it sets **`Config::tour_pending`**, the one value `main()` owns and hands to + reads when it starts a moment later, and **re-syncs this dialog's own "(i)" rows** through + `chooser_help_sync()`; *Guided tour* starts a tour **of this window** -- the chooser's own walk -- + and that walk's last stop sets **`Config::tour_pending`**, the one value `main()` owns and hands to both `choose_source()` and `run_ui()` (`open_waveform` and the preferences' relative time cross the same way). `on_activate()` schedules `tour_start_requested()` 1.5 s later, the same delay the first-run auto-start uses, and **that callback is separate from `tour_autostart()` on purpose**: it sets `tour_requested`, not `tour_auto_started`, so `help layer:` says which of the two started the - tour instead of conflating a user's request with a first run. + window's half instead of conflating a user's request with a first run. The chooser's half has its own + entry points in `source_dialog.cpp` (`chooser_tour_begin`, `chooser_tour_autostart`), and *that* walk + is what a first run meets first -- see the tour bullets in the help-affordance section below. - **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.** Putting the events pane in a paned is what makes its width the user's to drag -- but with the position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column @@ -537,27 +540,69 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps stack, the visible child is the view, and `gtk_popover_present()` re-measures a popover that is already up. `UDPSCOPE_PROBE_MENUS_MS` walks the *visible page*, which is the one line the stack cost the probe. -- **A "?" joins its surface's heading row as a sibling, not as an overlay child.** An overlay child - sits *on* whatever is under it -- and every panel heading already has a switch or an [edit] button in - a corner. `src/ui_context_help.cpp` is a table of (id, title, body, shortcut, host) where the host is - the *heading box*, so a new surface is a row and nothing else. The one exception is the plot, which - has no heading: its "?" is an overlay child of the overlay the layout editor already uses, and its - corner is free. The icon is `dialog-question-symbolic` -- `help-about-symbolic` is *already* the - status bar's information button, and two "help" glyphs meaning different things on the same line is - the trap the seven app icons exist to avoid; the dump reads the name back through the theme for the - same reason the pane [edit] buttons do. +- **A "(i)" joins its surface's row as a sibling, not as an overlay child.** An overlay child + sits *on* whatever is under it -- and every panel heading already has a switch or a pencil in a + corner. `src/ui_context_help.cpp` is a table of (id, title, body, shortcut, host) where the host is + the *row* -- a panel's heading usually, and the row of a specific control (the "record events" + switch, the pre-trigger share) where that is the thing being explained. The one exception is the + plot, which has no heading: its "(i)" is an overlay child of the overlay the layout editor already + uses, and its corner is free. The icon is **`dialog-information-symbolic`** -- `help-about-symbolic` + is *already* the status bar's information button, and two "help" glyphs meaning different things on + the same line is the trap the seven app icons exist to avoid; the dump reads the name back through + the theme for the same reason the pane [edit] buttons do. `math_head` exists only while the math + editor is open, so that row is skipped when it is null. +- **A `GtkMenuButton` ignores both `min-width` and a size request; its subnodes are where a small + one is made.** The "(i)" is meant to be a *mark*, not a button, and the default is 34x34 (a 24 px + floor plus 5 px of padding from Adwaita's `button` rule). Measured on libadwaita 1.10: setting + `min-width`/`min-height` on the widget changed nothing, and neither did `gtk_widget_set_size_request()` + -- the menu button's own measure wins. Three selectors are needed, all in `install_css()`: + `menubutton.udpscope-hint` (the outer node), `menubutton.udpscope-hint > button` (its subnode) and + `... > button > image` (the glyph), with `min-width: 0; min-height: 0; padding: 0` -- and the result + is **16x16**, the glyph's own size, because a `min-width` is a floor and not a cap. (A class on the + outer node alone leaves the subnode at 34; a class on the subnode alone shrank the width to 16 and + left the height at 26.) The dump's `hint=` reading is that number, and it is measured with + `gtk_widget_compute_bounds()` on the first *visible* hint -- a hint inside a collapsed `GtkExpander` + is not in the widget-parent chain at all and reads 0x0, which would look like a mark shrunk to + nothing. - **Off is gone, not greyed, and the buttons are found by a data key rather than kept in a list.** The list is what the math editor cannot survive: its heading lives in a builder that is dropped and rebuilt on every opening, so a stored pointer into it would go stale one dialog later. A key on the button (`udpscope-help-hint`, carrying the surface's id) is both the mark that says "this is a hint" and the name the probe and the dump report it by, and removing a hint is `gtk_widget_unparent()`, - which means the same thing for a box and for the plot's overlay. + which means the same thing for a box and for the plot's overlay. The button itself is + `ui_impl::hint_button()` reached either by the table's `context_help_sync()` or by + `context_help_button()` for a window with no `App` (the source chooser) -- one button, two callers, + so the two windows cannot drift into two affordances. - **The guided tour takes no grab, and that is the whole reason it is an overlay.** Four dim rectangles around the hole are ordinary widgets that swallow the clicks that land on them and nothing else; `Skip`, `Finish` and `Escape` hide them, and there is no popover, no gesture and no grab to leak. The probe asserts it as a reading -- `controllers=` against the count taken before the tour opened -- rather than as a claim, because "no lingering grab" is exactly the sort of thing a later change can add back without noticing. +- **The tour is one implementation over two windows.** `ui_impl::TourView` holds the overlay children + and the walk's place in it (`tour_build`/`tour_begin`/`tour_advance`/`tour_end`/`tour_update`), and + what differs between the windows is only the *stops*: a `TourStop` names a widget the owner resolves, + so each window keeps its own table -- the main window's against `App` fields, + `source_dialog.cpp`'s `kChooserStops` against the `Chooser`'s. A walk that passes its last stop calls + the view's `on_done`; the window's passes null, the chooser's sets `Config::tour_pending`, which is + how the two halves are one tour. `App` carries its own `TourView` (`app.tour`), so the machinery is + compiled once and the probe reads either window's state by the same fields. +- **The chooser's tour needs an overlay over its *whole* window, and `AdwWindow`'s content is + replaced, not wrapped.** Two of the chooser's stops name header-bar buttons, and an overlay inside + the `AdwToolbarView`'s `content` would give them negative coordinates and clamp the hole to the top + edge. So the overlay *becomes* the window's content and adopts the toolbar view -- and the order + matters: `gtk_overlay_set_child()` refuses a child that still has a parent, so + `adw_window_set_content(win, overlay)` goes **first** (that call is what unparents the toolbar view) + and `gtk_overlay_set_child(overlay, content)` second. The builder's object list still owns the view, + so nothing is finalized in between. Getting the order wrong is a `Gtk-CRITICAL` and an empty + chooser, which is exactly what the first version of this did. +- **The chooser's hints are its own table because it runs before `App`.** An `AdwEntryRow` is not a + box and its editor is internal, so a hint cannot join the host/port fields as a sibling -- appending + it to the `AdwPreferencesGroup` would make it a row under the field rather than a mark beside it, so + no hint is invented for them and the group's own `description` carries that text. The two rows the + dialog *does* have -- the heading above the saved-source list and the waveforms page's action row -- + carry `chooser_sources` and `chooser_waveform`, added with the same `context_help_button()` the + window uses and re-synced by its Help menu's toggle. - **The hole is kept from the last frame that could measure the target.** `gtk_widget_compute_bounds()` answers nothing on a frame that lands between two layout passes, and letting the hole empty there made the spotlight and the card jump to the corner and back; a *target that is not visible* is the @@ -567,8 +612,11 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps - **The first-run tour is a `settings.toml` key (`tour_seen`), written when the tour *starts*.** Not "the settings file did not exist": a user who has already changed the theme has a file and has still never seen the tour. Written on the way *in*, because a user who skips it in the first second has - still seen it. The auto-start is a 1.5 s timer from `on_activate()`, so the window is up and laid out - before anything points at it. + still seen it. It starts **in the source chooser** when one is shown (the chooser is the surface a + first run meets first, and the window's own auto-start is unreachable until the chooser has returned + with a source) and in the window otherwise; either way it is a 1.5 s timer, so the window is up and + laid out before anything points at it. The chooser's walk writes the key itself and hands the second + half over by `Config::tour_pending`, so a first run is *one* tour that crosses the two windows. - **The `help figures:` line is captured on the tick, like every other size the dump prints.** It names ten surfaces' window-coordinate rectangles for the help's annotated figures, and the dump runs after the window is destroyed -- a `gtk_widget_compute_bounds()` there is the "no `gtk_widget_*` call in the diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 3ed5e0c..e536f6d 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -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=` 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/_.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 '