help: the search block's (i) on its header, a 12 px mark, and a real search page

This commit is contained in:
Martino Ferrari committed 2026-10-14 23:29:00 +02:00
1 parent d549223889
commit fe499bfe41
10 files changed
+178 -53

No files matched your search

+21 -13
View File
@@ -544,26 +544,34 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
sits *on* whatever is under it -- and every panel heading already has a switch or a pencil in a 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 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" 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 switch, the pre-trigger share, the search block's expander *header*) 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 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` 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 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 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 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. 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 - **A `GtkMenuButton` ignores both `min-width` and a size request, and the icon is a node *below*
one is made.** The "(i)" is meant to be a *mark*, not a button, and the default is 34x34 (a 24 px its `button` subnode.** The "(i)" is meant to be a *mark*, not a button, and the default is 34x34 (a
floor plus 5 px of padding from Adwaita's `button` rule). Measured on libadwaita 1.10: setting 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()` `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()`: -- the menu button's own measure wins. The node tree is `menubutton > button > box > image` (read off
`menubutton.udpscope-hint` (the outer node), `menubutton.udpscope-hint > button` (its subnode) and the CSS node dump), so `install_css()` reaches the glyph as
`... > button > image` (the glyph), with `min-width: 0; min-height: 0; padding: 0` -- and the result `menubutton.udpscope-hint > button > box > image`. The direct-grandchild form `> button > image`
is **16x16**, the glyph's own size, because a `min-width` is a floor and not a cap. (A class on the matches **nothing** and is silent about it -- the glyph keeps its natural 16 px, which is why an
outer node alone leaves the subnode at 34; a class on the subnode alone shrank the width to 16 and earlier version shipped a 16x16 mark and took that for the floor it was not. Zeroing the outer node
left the height at 26.) The dump's `hint=` reading is that number, and it is measured with and the `button` subnode and asking the glyph for **12 px** (`min-width`/`min-height` *and*
`gtk_widget_compute_bounds()` on the first *visible* hint -- a hint inside a collapsed `GtkExpander` `-gtk-icon-size`, the latter what actually *scales* the drawing rather than clamping its box) gives
is not in the widget-parent chain at all and reads 0x0, which would look like a mark shrunk to **12x12**. (A class on the outer node alone leaves the subnode at 34; a class on the subnode alone
nothing. shrank the width to 16 and left the height at 26.) There is no lower floor: the symbolic glyph renders
at any request (measured down to 4 px), so 12 px is a legibility choice and not a limit. The dump's
`hint=` reading is that number, measured with `gtk_widget_compute_bounds()` on the first *visible*
hint -- a hint inside a collapsed `GtkExpander`'s *child* is not in the widget-parent chain at all and
reads 0x0, which would look like a mark shrunk to nothing. That is why the search block's hint rides
the expander's **header** (a `label-widget` box, `search_head` in `src/ui/trigger-panel.ui`, which
*is* in the tree) rather than its child, and why the `[helpui] collapsed search:` reading asserts the
header hint is mapped and 12x12 while the block is collapsed.
- **Off is gone, not greyed, and the buttons are found by a data key rather than kept in a list.** The - **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 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 rebuilt on every opening, so a stored pointer into it would go stale one dialog later. A key on the
+36 -21
View File
@@ -395,15 +395,17 @@ 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 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 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. exactly like a page with nothing in it.
- `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 - `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; 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 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: ten of the window the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt 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). Three of the ten are not a pane's *heading* on every opening, so it is not one of the window's own). Four of the eleven are not a pane's *heading*
but the row of a specific control — `events_record` (the "record events" switch in the trigger 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 panel), `trigger_advanced` (the pre-trigger share row), `trigger_search` (the *header* of the search
line, whose box exists so a hint can join it) — which is the rule the layer follows: a hint is a block's expander, which is what lets it be on screen while the block is collapsed) and
sibling of the thing it explains and never an overlay on it. The layer is **off by default** `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_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 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 `contextual=off` beside a non-zero count is the toggle not having removed its buttons — **off is
@@ -412,11 +414,17 @@ Two lines of the self-check output are assertions in disguise:
is `dialog-information-symbolic`, the plain information mark; `help-about-symbolic` is the status 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 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 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 (`gtk_widget_compute_bounds()`), and **12x12 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 class zeroes the outer `menubutton` node and its `button` subnode (a `GtkMenuButton`'s measure
`GtkMenuButton` needs (its measure ignores both `min-width` on the widget and a size request) — the ignores both `min-width` on the widget and a size request), and the glyph is a third node *below*
default 34x34 is the number to compare against. Hints inside a collapsed expander are skipped, so the the subnode — `menubutton > button > box > image`, read off the CSS node dump. The direct-grandchild
number is never a hidden widget's 0x0. `tour=` is `running`/`idle` with the plan's `steps=` and the form `> button > image` matches **nothing** and silently leaves the glyph at its natural 16 px,
which is the 16x16 an earlier version shipped by mistake; and there is no lower floor to find
(the symbolic glyph renders at any request, measured down to 4 px), so 12 px is a legibility
choice. The default 34x34 is the number to compare against. A hint inside a collapsed expander's
*child* is skipped, so the number is never a hidden widget's 0x0 — but a hint on the expander's
*header* is in the widget tree and reads its real size while the block is collapsed (see the
`collapsed search:` entry below). `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 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` — `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 and `requested=1` is the **chooser's** tour having handed over (`Config::tour_pending`), which is
@@ -506,7 +514,7 @@ Two lines of the self-check output are assertions in disguise:
the saved-source list and the waveforms page's action row; the form's `AdwEntryRow`s are *not* 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 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 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 same 12x12 the window's line reads: the button is `ui_impl::context_help_button()`, the one
`udpscope-hint` affordance both windows draw. `udpscope-hint` affordance both windows draw.
- `[sources] tour: first run -- auto_started=1 in the chooser` then - `[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 `[sources] chooser tour: visible=1 steps=5 step=0` (stderr) — **a first run's tour starts in the
@@ -751,16 +759,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 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* `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: run against the same config directory, which is the assertion that the flag persisted. Then:
- `[helpui] contextual on: hints=11 ids={events plot status cursors measures trigger trigger_advanced events_record signals signals_config window} hint=16x16` — - `[helpui] contextual on: hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} hint=12x12` —
the eleven "(i)" affordances, by id, and the size the first visible one was allocated. Ten are the the twelve "(i)" affordances, by id, and the size the first visible one was allocated. Eleven are
window's own surfaces (three of them a specific control's row rather than a heading — see the the window's own surfaces (four 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 `help layer:` entry) and the twelfth 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={}` window is open; it reads 11 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 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` 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** key is the default being read rather than assumed. **`hint=12x12` is the "much tinier" assertion**
— the default menu-button mark is 34x34 (see `help layer:`), and anything near that is the class — the default menu-button mark is 34x34 (see `help layer:`), and anything near that is the class
not reaching the outer `menubutton` node. not reaching the glyph's own node.
- `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '<label>' accel='<key>' order=O` - `[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 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'`, promises: `entry 1 'Keyboard shortcuts' accel='Ctrl+/'`, `entry 2 'Documentation' accel='F1'`,
@@ -771,10 +779,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` `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 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. it both ways, which is why the example here is the `1` side.
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=11 ids={…}; setting=on` — the toggle's effect - `[helpui] toggle: off -> hints=0 ids={}; on -> hints=12 ids={…}; setting=on` — the toggle's effect
read both ways through the row itself. **`hints=0` with `off` is the assertion**: off means the 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 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 eleven, which is the other half — a toggle that removing them. The count comes back to the same twelve, which is the other half — a toggle that
removed and could not restore is a one-way switch. 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 - `[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 surface is on screen or `Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose surface is on screen or
@@ -804,6 +812,13 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
rather than leaving them to find the mode. `trigger_panel`'s height differs between them (508 px in rather than leaving them to find the mode. `trigger_panel`'s height differs between them (508 px in
configuration mode against 72 locked — the body is replaced by the summary), which is the check configuration mode against 72 locked — the body is replaced by the summary), which is the check
that the mode really moved. that the mode really moved.
- `[helpui] collapsed search: expanded=0 mapped=1 mapped_header=1 hint=12x12` (the last step, in
configuration mode, after the figures line) — the search block's "(i)" read on the expander's
**header** with the block **collapsed**. **`expanded=0 mapped=1` is the assertion**: a hint on the
expander's own *child* is not in the widget-parent chain while the block is collapsed and would
read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in
`src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the
header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads.
- `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) — a stop that stands on a page it is - `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) — a stop that stands on a page it is
not on, entered from the page it is not on. It puts the sidebar on **Measure**, starts the window's not on, entered from the page it is not on. It puts the sidebar on **Measure**, starts the window's
tour, and walks it a stop per tick (400 ms), because a stack allocates the new page's widgets on tour, and walks it a stop per tick (400 ms), because a stack allocates the new page's widgets on
+4 -3
View File
@@ -314,8 +314,9 @@ as distinct from the fault injection below, which exercises ingest:
figures:` read in both of the trigger pane's modes. Its lines and what each asserts are in figures:` read in both of the trigger pane's modes. Its lines and what each asserts are in
[probe-readings.md](probe-readings.md); the two things to read first are **`hints=0`** while the [probe-readings.md](probe-readings.md); the two things to read first are **`hints=0`** while the
toggle is off (off is *gone*, not greyed) and **`controllers=12 (was 12)`** after the dismissal (no toggle is off (off is *gone*, not greyed) and **`controllers=12 (was 12)`** after the dismissal (no
lingering input grab — a tour of ordinary overlay widgets takes none), and **`hint=16x16`** is the lingering input grab — a tour of ordinary overlay widgets takes none), and **`hint=12x12`** is the
size the shrunk "(i)" must keep (the default menu-button mark is 34x34). The chooser's own tour is size the shrunk "(i)" must keep (the default menu-button mark is 34x34). The `collapsed search:` line
is the header hint read on the search block's expander while it is collapsed. The chooser's own tour is
the other half and has its own entry points — see the **chooser** bullet below. Give it the other half and has its own entry points — see the **chooser** bullet below. Give it
`UDPSCOPE_EXIT_AFTER_MS=9500 UDPSCOPE_EXIT_DUMP=1` to get the `help layer:`/`help figures:` dump `UDPSCOPE_EXIT_AFTER_MS=9500 UDPSCOPE_EXIT_DUMP=1` to get the `help layer:`/`help figures:` dump
lines as well, and note the tour **auto-starts** in any run whose settings file does not carry lines as well, and note the tour **auto-starts** in any run whose settings file does not carry
@@ -345,7 +346,7 @@ as distinct from the fault injection below, which exercises ingest:
configuration carried, into the `Config` the model is built from. **With no `--server` and no probe configuration carried, into the `Config` the model is built from. **With no `--server` and no probe
the chooser blocks in its own nested loop** (no frame to end and no dump), so a chooser run is the chooser blocks in its own nested loop** (no frame to end and no dump), so a chooser run is
always this probe. It also prints the dialog's frame *and its own help layer*: always this probe. It also prints the dialog's frame *and its own help layer*:
`[sources] chooser ui: … hints=[2 {chooser_sources chooser_waveform}] hint=16x16` with the setting `[sources] chooser ui: … hints=[2 {chooser_sources chooser_waveform}] hint=12x12` with the setting
on, `hints=[0 {}] hint=0x0` with it off. `UDPSCOPE_PROBE_CHOOSER_HELP=1` drives the **Enable on, `hints=[0 {}] hint=0x0` with it off. `UDPSCOPE_PROBE_CHOOSER_HELP=1` drives the **Enable
contextual help** row and reads the file back; `UDPSCOPE_PROBE_CHOOSER_TOUR=1` drives the **Guided contextual help** row and reads the file back; `UDPSCOPE_PROBE_CHOOSER_TOUR=1` drives the **Guided
tour** row and walks the chooser's own tour to its last stop, printing `[sources] chooser tour: …` tour** row and walks the chooser's own tour to its last stop, printing `[sources] chooser tour: …`
+48 -6
View File
@@ -114,12 +114,54 @@
<section id="search"> <section id="search">
<title>Search the buffer</title> <title>Search the buffer</title>
<p><gui>Search the buffer</gui> runs the same condition over the whole retained ring and <p><gui>Search the buffer</gui>, at the foot of the trigger panel and collapsed until you open
lists every place it holds. The arrows step through hits; each step pauses on the hit, it, answers the question the trigger answers — <em>where does this condition hold?</em> — over
centred, exactly as the Pause button does. Up to 16 hits in the visible window are marked the <em>whole</em> retained buffer rather than at the live edge. It runs the trigger's own
on every row; when there are more the panel says how many and suggests zooming in. The evaluator over the ring in stream order, so a <gui>hit</gui> is not a level you point at: it is
search uses the trigger's own condition, set in one place, so a hit is by construction an an <em>instant</em> the trigger would have fired on, timed the way an acquisition would have
instant the trigger would have fired.</p> timed it. A pulse-width finding, for instance, is the closing edge of a qualifying excursion,
not the excursion itself.</p>
<p>The condition is the one the rest of the panel is set to — the same signal, shape, level and
options — so there is no second condition to keep in step: change the panel and the next search
asks the new question. Nothing is searched until you ask, and the status line says
<gui>not searched</gui>.</p>
<steps>
<item>
<p>Set the condition above, then press <gui>Search</gui>. Covering the whole ring takes a
moment, and the scan runs in chunks so the stream keeps arriving while it does; the status
line counts the ground covered — <gui>scanning 1.2 s of 8.0 s, 40 found</gui>.
<gui>Stop</gui> ends a scan early and keeps the hits found so far.</p>
</item>
<item>
<p>Step through the hits with the arrows either side of the position readout
(<gui>3 of 40</gui>). The first <gui>next</gui> goes to the first hit, and the ends do not
wrap — a wrap from the last hit to the first would look exactly like the view not having
moved. Each step <em>pauses</em> on its hit, centred, exactly as the Pause button does, so
what you read is the frozen window cut around that instant; press the pause button again to
go back to the stream.</p>
</item>
<item>
<p>Every plot row marks the hits that fall in the window, up to sixteen of them. When more
are in view the pane says so and suggests zooming in — <gui>16 of 38 marks shown — zoom in
to see the rest</gui> — and when the selected hit has scrolled off the window it says that
instead. A hit the live stream has since overwritten reads <gui>no longer in the buffer</gui>
beside its position.</p>
</item>
</steps>
<p>How a scan ended is part of the answer: <gui>N found in 8.0 s</gui> when it covered the whole
ring; <gui>N found; the stream overtook the oldest samples</gui> when the producer overwrote
samples before the scan reached them, so the count is of what was left; and
<gui>N found (the limit) in the newest …</gui> when a condition that holds almost everywhere —
an edge on a noisy signal — reached the ten-thousand-hit ceiling. The last is a stop, not a
crash: a hit list nobody could navigate is not worth building.</p>
<p>A result describes the ring it was taken from, so it is <em>dropped</em> when that ring is
replaced: a new <gui>CONFIG</gui> from the source, or a change to the signal set, re-describes
the stream and clears the hits. The condition above is untouched — only the answer to it
is.</p>
</section> </section>
<section id="knowing"> <section id="knowing">
+16 -1
View File
@@ -214,8 +214,23 @@
</child> </child>
<child> <child>
<object class="GtkExpander" id="search_exp"> <object class="GtkExpander" id="search_exp">
<property name="label" translatable="yes">Search the buffer</property>
<property name="tooltip-text" translatable="yes">find every place in the retained buffer where the condition above holds, and step through them</property> <property name="tooltip-text" translatable="yes">find every place in the retained buffer where the condition above holds, and step through them</property>
<!-- The header is a box, not a plain label, so the contextual help's "(i)" has a row to join
as its last child and sits beside the text while the block is collapsed. The block's own
*child* is not in the widget-parent chain until the expander is expanded (see
docs/claude/invariants-gtk-and-chrome.md), so a hint hung there would be off screen. -->
<child type="label">
<object class="GtkBox" id="search_head">
<property name="orientation">horizontal</property>
<property name="spacing">6</property>
<child>
<object class="GtkLabel">
<property name="label" translatable="yes">Search the buffer</property>
<property name="halign">start</property>
</object>
</child>
</object>
</child>
<property name="child"> <property name="child">
<object class="GtkBox" id="search_sbox"> <object class="GtkBox" id="search_sbox">
+8 -1
View File
@@ -246,6 +246,9 @@ struct App {
GtkWidget* search_marks_label = nullptr; GtkWidget* search_marks_label = nullptr;
GtkWidget* search_prev_button = nullptr; GtkWidget* search_prev_button = nullptr;
GtkWidget* search_next_button = nullptr; GtkWidget* search_next_button = nullptr;
/// The expander the block lives in, so the probe can read its collapsed state when it reads the
/// header hint.
GtkWidget* search_exp = nullptr;
/// Set true when the last `search_jump()` produced an empty cut — the hit was so close to the /// Set true when the last `search_jump()` produced an empty cut — the hit was so close to the
/// oldest retained sample that `publish_pause_view()` held nothing. Cleared at every jump. /// oldest retained sample that `publish_pause_view()` held nothing. Cleared at every jump.
bool search_jump_empty = false; bool search_jump_empty = false;
@@ -284,10 +287,14 @@ struct App {
GtkWidget* status_row = nullptr; GtkWidget* status_row = nullptr;
GtkWidget* window_control_slot = nullptr; GtkWidget* window_control_slot = nullptr;
/// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits /// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits
/// beside the thing it explains -- the "record events" switch, the search block's action row, and the /// beside the thing it explains -- the "record events" switch, the pre-trigger share row, and the
/// signals pane's hint line (whose box exists so a hint can join it beside the text). /// signals pane's hint line (whose box exists so a hint can join it beside the text).
GtkWidget* trigger_record_row = nullptr; GtkWidget* trigger_record_row = nullptr;
GtkWidget* trigger_pre_row = nullptr; GtkWidget* trigger_pre_row = nullptr;
/// The search block's expander *header* -- the box the contextual help's "trigger_search" hint joins
/// as its last child. The header, not the block: the expander's child is out of the widget-parent
/// chain until the block is expanded, so a hint there is invisible while it is collapsed.
GtkWidget* trigger_search = nullptr;
GtkWidget* signals_note = nullptr; GtkWidget* signals_note = nullptr;
/// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the /// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the
/// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the /// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the
+6
View File
@@ -109,6 +109,12 @@ constexpr HelpHint kHelpHints[] = {
"is the quiet time after a latch in which nothing new can be armed -- the two that decide what a " "is the quiet time after a latch in which nothing new can be armed -- the two that decide what a "
"capture looks like. The condition rows above appear and disappear with the condition chosen.", "capture looks like. The condition rows above appear and disappear with the condition chosen.",
"", &App::trigger_pre_row}, "", &App::trigger_pre_row},
{"trigger_search", "Searching the buffer",
"Applies the condition above to the whole retained ring -- the *same* evaluator the trigger runs, "
"so a hit is an instant the trigger would have fired on -- and lists every place it holds. The "
"arrows step through the hits, pausing centred on each, and up to sixteen in view are marked on "
"every row.",
"", &App::trigger_search},
{"signals_config", "Configuring signals", {"signals_config", "Configuring signals",
"The list is locked until the pencil above opens the per-signal controls: the calibration that " "The list is locked until the pencil above opens the per-signal controls: the calibration that "
"maps raw counts to a unit, the colour a trace is drawn in, and which plot a signal is dragged " "maps raw counts to a unit, the colour a trace is drawn in, and which plot a signal is dragged "
+18
View File
@@ -4773,6 +4773,24 @@ gboolean helpui_probe_step(gpointer user) {
helpui_probe_next(*a, 8, 400); helpui_probe_next(*a, 8, 400);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
// The search block's "(i)" rides the expander's *header* box, which is in the widget tree and on
// screen while the block is collapsed -- unlike the expander's child, which is out of the
// widget-parent chain until it is expanded (see the invariant). Read here, in config mode with the
// pane and the sidebar up and the block still collapsed: `mapped=1` and a real size *on a collapsed
// expander* is the whole point, and a hint hung on the child would read 0x0 here.
if (a->trigger_search != nullptr) {
GtkWidget* hint = gtk_widget_get_last_child(a->trigger_search);
int sw = 0;
int sh = 0;
context_help_size_in(hint, &sw, &sh);
std::fprintf(stderr,
"[helpui] collapsed search: expanded=%d mapped=%d mapped_header=%d hint=%dx%d\n",
a->search_exp != nullptr && gtk_expander_get_expanded(GTK_EXPANDER(a->search_exp))
? 1
: 0,
hint != nullptr && gtk_widget_get_mapped(hint) ? 1 : 0,
gtk_widget_get_mapped(a->trigger_search) ? 1 : 0, sw, sh);
}
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
+5
View File
@@ -599,6 +599,11 @@ GtkWidget* build_trigger_panel(App& app) {
app.search_pos_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_pos")); app.search_pos_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_pos"));
app.search_next_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_next")); app.search_next_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_next"));
app.search_marks_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_marks")); app.search_marks_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_marks"));
app.search_exp = GTK_WIDGET(gtk_builder_get_object(ui, "search_exp"));
// The expander's header box, where the contextual help's "trigger_search" hint joins as its last
// child (see `ui_context_help.cpp`): a hint on the block's own child would leave it unreachable
// while the block is collapsed.
app.trigger_search = GTK_WIDGET(gtk_builder_get_object(ui, "search_head"));
if (app.search_run_button != nullptr) { if (app.search_run_button != nullptr) {
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) { g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
+16 -8
View File
@@ -71,16 +71,24 @@ void install_css() {
" border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n" " border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n"
// The contextual-help "(i)": a *mark*, not a button. Adwaita's menu button is a 34x34 control // The contextual-help "(i)": a *mark*, not a button. Adwaita's menu button is a 34x34 control
// (a 24 px floor plus 5 px of padding) with a 16 px glyph, which beside a heading reads as a // (a 24 px floor plus 5 px of padding) with a 16 px glyph, which beside a heading reads as a
// second toolbar button -- the size the ask calls too big. Measured on libadwaita 1.10: the // second toolbar button. Measured on libadwaita 1.10: the outer `menubutton` node *and* its
// outer `menubutton` node *and* its `button` subnode both carry a floor and padding, and a class // `button` subnode both carry a floor and padding, and a class selector on either alone leaves
// selector on either alone leaves the other at 34 (`min-width` on the widget itself is ignored // the other at 34 (`min-width` on the widget itself is ignored by `GtkMenuButton`'s measure, and
// by `GtkMenuButton`'s measure, and so is a size request -- tried both). Zeroing all three -- // so is a size request -- tried both). Beyond those two there is a third node, and getting its
// the outer node, the subnode and its glyph -- is what brings it to **16x16**: the icon's own // path wrong is silent: the button's own tree is `menubutton > button > box > image` (read off
// size and nothing else, because a `min-width` is a floor, not a cap, so 16 px is where a 16 px // the CSS node dump), so the direct-grandchild form `> button > image` matches *nothing* and
// Adwaita glyph stops. That is the reading `hint=WxH` in the dump prints. // leaves the glyph at its natural 16 px -- which is the 16x16 the first version shipped and
// mistook for a floor. The glyph is reached through the `box`; trimming the outer node, the
// `button` subnode and the glyph together, and asking the glyph for 12 px with
// `-gtk-icon-size` (which `GtkImage` honours when it is `GTK_ICON_SIZE_INHERIT`, and which
// *scales* the drawing rather than merely clamping its box), is what brings the mark to
// **12x12**. There is no lower floor to find: the symbolic glyph renders at any request
// (measured down to 4 px), so 12 px is a legibility choice, and it is the size the rule always
// meant -- the first version already wrote 12 px, on a selector that never matched. That is
// the reading `hint=WxH` in the dump prints.
"menubutton.udpscope-hint { min-width: 0; min-height: 0; padding: 0; margin: 0; }\n" "menubutton.udpscope-hint { min-width: 0; min-height: 0; padding: 0; margin: 0; }\n"
"menubutton.udpscope-hint > button { min-width: 0; min-height: 0; padding: 0; }\n" "menubutton.udpscope-hint > button { min-width: 0; min-height: 0; padding: 0; }\n"
"menubutton.udpscope-hint > button > image { min-width: 12px; min-height: 12px;" "menubutton.udpscope-hint > button > box > image { min-width: 12px; min-height: 12px;"
" -gtk-icon-size: 12px; }\n"; " -gtk-icon-size: 12px; }\n";
GtkCssProvider* p = gtk_css_provider_new(); GtkCssProvider* p = gtk_css_provider_new();
gtk_css_provider_load_from_string(p, kCss); gtk_css_provider_load_from_string(p, kCss);