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

+89 -25
View File
@@ -57,6 +57,48 @@ enum class EventGroupKind {
Compare, ///< two events in two plots side by side, at one scale, with synchronised axes
Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness
};
// --- the guided tour, shared by both windows (ui_tour.cpp) -------------------
//
// The tour is *two* tours now -- the main window's and the source chooser's -- because the chooser is
// the window a first run meets first and the surfaces it must explain are its own. The machinery is
// one implementation: a `TourView` is the overlay children (the four dim rectangles, the spotlight
// outline and the card) plus where the walk stands in it, and either window holds one. What differs
// is only the *stops*: a stop's target is a widget the owner resolves, so each window keeps its own
// table (the chooser runs before `App` exists and could not name one of `App`'s fields).
/// One stop of a guided tour: the widget to spotlight, and the two paragraphs of text beside it.
/// The target is resolved when the tour starts, so the table may name widgets that do not exist yet.
struct TourStop {
GtkWidget* target;
const char* title;
const char* body;
};
/// A tour's widgets and its place in the walk. `hole` is the spotlight's rectangle in the overlay's
/// own coordinates, kept from the last frame that could measure the target: a frame can come between
/// two layout passes, where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied
/// there would make the spotlight and the card jump.
struct TourView {
GtkWidget* overlay = nullptr;
GtkWidget* dim[4] = {};
GtkWidget* spot = nullptr;
GtkWidget* card = nullptr;
GtkWidget* title = nullptr;
GtkWidget* body = nullptr;
GtkWidget* counter = nullptr;
GtkWidget* back = nullptr;
GtkWidget* next = nullptr;
int hole[4] = {0, 0, 0, 0};
bool running = false;
int index = 0;
std::vector<TourStop> plan;
/// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour
/// on to the application through `Config::tour_pending`.
void (*on_done)(void* user) = nullptr;
void* done_user = nullptr;
};
struct App {
ScopeModel* model = nullptr;
Acquisition* acq = nullptr;
@@ -232,26 +274,17 @@ struct App {
/// are not a panel with a heading.
GtkWidget* status_row = nullptr;
GtkWidget* window_control_slot = nullptr;
/// 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
/// 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
/// signals pane's hint line (whose box exists so a hint can join it beside the text).
GtkWidget* trigger_record_row = nullptr;
GtkWidget* trigger_pre_row = nullptr;
GtkWidget* signals_note = nullptr;
/// 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
/// steps whose target was on screen when the tour started, so a surface that is not there is not
/// walked over. `tour_hole` is the spotlight's rectangle in the overlay's own coordinates, kept
/// from the last frame that could measure the target: a frame can come between two layout passes,
/// where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied there would make
/// the spotlight and the card jump for that frame.
GtkWidget* tour_overlay = nullptr;
GtkWidget* tour_dim[4] = {};
GtkWidget* tour_spot = nullptr;
GtkWidget* tour_card = nullptr;
GtkWidget* tour_title = nullptr;
GtkWidget* tour_body = nullptr;
GtkWidget* tour_counter = nullptr;
GtkWidget* tour_back = nullptr;
GtkWidget* tour_next = nullptr;
int tour_hole[4] = {0, 0, 0, 0};
bool tour_running = false;
int tour_index = 0;
std::vector<int> tour_plan;
/// walked over. The machinery is shared with the source chooser's tour -- see `TourView`.
TourView tour;
/// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump.
/// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before".
bool tour_auto_started = false;
@@ -1732,19 +1765,50 @@ void on_contextual_help_toggled(GtkToggleButton* button, gpointer user);
/// The "?" for one surface by id, for a host that only exists while a dialog is open (the math
/// editor is rebuilt on every open, so it cannot be a row the window's own build visits).
void context_help_add(App& app, const char* id, GtkWidget* host);
/// The same affordance for a surface that is *not* in `App`'s table -- the source chooser's, which
/// runs before `App` exists and so cannot name one of its fields. `host` is a `GtkBox` and the button
/// joins it as its last child, which is the rule the table's own rows follow; `on` false takes any
/// hint off `host` instead. `shortcut` may be "".
void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body,
const char* shortcut, bool on);
/// How many hints a widget subtree carries, and their ids, for a window that has no `App` (the
/// chooser's probe). `context_help_count()` is this with `App::window` as the root.
int context_help_count_in(GtkWidget* root, std::string* ids);
/// The size of the first *visible* hint button in a subtree, read the way the probe's geometry is
/// (`gtk_widget_compute_bounds()`), and the number a shrinking affordance is checked against. Both
/// are 0 when the layer is off or nothing on screen carries one.
void context_help_size_in(GtkWidget* root, int* width, int* height);
/// `context_help_size_in()` with `App::window` as the root.
void context_help_size(App& app, int* width, int* height);
/// Builds the tour's overlay children (the four dim rectangles, the spotlight outline and the card)
/// inside `app.tour_overlay`. Called once, as the overlay is inserted.
/// Builds a tour's overlay children (the four dim rectangles, the spotlight outline and the card)
/// inside `overlay`. Called once, as the overlay is inserted, by either window that draws a tour.
void tour_build(TourView& view, GtkWidget* overlay);
/// Starts at the first stop. `stops` is the walk, already filtered to what is on screen; `on_done`
/// (may be null) runs when the walk passes its last stop, and is how the chooser hands off.
void tour_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
void* done_user);
/// Moves `delta` stops, finishing the walk (and calling `on_done`) on the way past the last one.
void tour_advance(TourView& view, int delta);
/// Ends the walk without finishing it: every child of the overlay is hidden and nothing is left on
/// screen. Skip and Escape.
void tour_end(TourView& view);
/// Called from the tick while the walk runs: re-measures the target each frame, so a window resize
/// keeps the spotlight on the widget, and places the card where it does not cover it.
void tour_update(TourView& view);
/// The widget the current stop points at, or nullptr when the walk is not on a stop.
GtkWidget* tour_target(TourView& view);
/// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted.
void build_tour(App& app);
/// Starts the tour at its first step and writes `tour_seen`. Safe to call while it already runs --
/// it restarts at step 0.
/// Starts the window's tour at its first step and writes `tour_seen`. Safe to call while it already
/// runs -- it restarts at step 0.
void start_tour(App& app);
/// Moves `delta` steps, ending the tour on the way past the last one.
void tour_step(App& app, int delta);
/// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen.
void tour_dismiss(App& app);
/// Called from the tick while the tour runs: re-measures the target each frame, so a window resize
/// keeps the spotlight on the widget, and places the card where it does not cover it.
/// The window's tour, re-measured each frame.
void update_tour(App& app);
bool tour_active(const App& app);
/// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe