fix: the guided tour selects the page a stop's target lives on

A stop whose target is on another page of a stack was entered without
switching to that page, so the spotlight aimed at a widget that was not
on screen. A stop now declares the page it stands on (`TourStop::page`)
and the tour selects it as the step is entered -- the window's trigger
and signals stops put the sidebar on Signals, the chooser's tabs stop
shows the Waveforms page and stepping off it brings Sources back.

The stop filter moved into `tour_begin()` and now runs on
`gtk_widget_get_mapped()`: GTK 4.24's `is_visible()` ignores
`child_visible`, so it kept a stop on another page *and* one in a
collapsed pane alike. A wrong page is kept and selected; a collapsed
pane, whose stack is unmapped too, is still dropped.
This commit is contained in:
Martino Ferrari committed 2026-10-14 23:01:43 +02:00
1 parent 829176248a
commit d549223889
9 files changed
+385 -89

No files matched your search

+23 -1
View File
@@ -587,6 +587,28 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
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.
- **A stop declares the page it stands on, and the tour selects that page as the stop is entered.** A
stop whose target lives on another page of a stack -- the window's `signals` (and `trigger`) stops
on the sidebar's `signals` page, the chooser's "Two kinds of input" stop on its `waveforms` page --
names that page in `TourStop::page`, and `show_step()` selects it from `TourView::page_stack`
before anything is measured. The page switch is **not** followed by a one-shot measure: `show_step()`
selects the page and `tour_update()`, which already runs every frame, fills the spotlight in on the
frame *after*, when the new page has been laid out. A stack allocates the new page's widgets on the
frame after the switch, so a measurement taken in the switch's own tick reads zero (the allocation
bullet in [invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)) -- and reading the
page *name* back is fine immediately, because that is state and not geometry.
- **The stop filter runs on `get_mapped()`, not `is_visible()`, and that is what separates a wrong
page from a collapsed pane.** GTK 4.24's `gtk_widget_is_visible()` walks only the widgets' own
`visible` flags up the parents; it does **not** consult `child_visible`. Both a `GtkStack`'s
non-current pages and `AdwOverlaySplitView`'s hidden sidebar are hidden with
`gtk_widget_set_child_visible()`, so a widget on another page, *and* one in a collapsed pane, both
read `is_visible() == TRUE` (verified against 4.24.1) -- the old filter kept a stop it meant to
drop and left the spotlight on a target with no allocation. `stop_is_walkable()` keeps a stop when
its target is **mapped**, or when it names a page and the stack that page belongs to is mapped: a
wrong page leaves the stack mapped, so the stop is kept and the page switch reveals it; a collapsed
pane leaves the stack unmapped too, so no page can reveal it and the stop is dropped. The filter
lives in `tour_begin()`, not in each window's `start_tour()`, both so the two windows cannot drift
into two rules and because the rule needs the view's own `page_stack`.
- **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
@@ -605,7 +627,7 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
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
made the spotlight and the card jump to the corner and back; a *target that is not mapped* is the
different case (a genuinely absent surface) and is what empties it. The placement is also
re-measured on **every frame**, never once at the start: a widget's allocation is 0 on the frame it
is first shown, and a window resize moves it under a one-shot measurement.