From 0c94caf866aae2b18c62576e94e6dc96d42c4f2c Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Mon, 2 Nov 2026 21:03:03 +0100 Subject: [PATCH] docs: the badge bar, the slide-over and the dock (step 1 of the redesign) Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 6 +- README.md | 115 ++++++++++++------- docs/claude/architecture.md | 33 ++++++ docs/claude/invariants-gtk-and-chrome.md | 69 +++++++---- docs/claude/invariants-plot-and-layout.md | 27 ++++- docs/claude/invariants-session-and-export.md | 12 +- docs/claude/verification.md | 11 ++ 7 files changed, 203 insertions(+), 70 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 76d822c..17e9a5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -46,13 +46,13 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e | File | What it covers | | --- | --- | -| [architecture.md](docs/claude/architecture.md) | The two threads and what they share; the one `BufferStore` and the four display modes (`Live`, `Capturing`, `Held`, `Waiting`); the display clock and `stream_at_x0()`; the layers to keep straight (`SignalInfo` → `ChannelBuffer` → `SlotSeries` → `RowSet`); the source plugins. | +| [architecture.md](docs/claude/architecture.md) | The two threads and what they share; the one `BufferStore` and the four display modes (`Live`, `Capturing`, `Held`, `Waiting`); the display clock and `stream_at_x0()`; the layers to keep straight (`SignalInfo` → `ChannelBuffer` → `SlotSeries` → `RowSet`); the source plugins; the window's chrome (slide-over, badge bar, dock) and when each is built. | ### Invariants worth knowing before editing | File | What it covers | | --- | --- | -| [invariants-plot-and-layout.md](docs/claude/invariants-plot-and-layout.md) | What `plot::Figure`/`plot::Plot` can and cannot be asked to do, what a row's contents belong to, the layout editor's overlay children, and the rule that **layout must not depend on content**. | +| [invariants-plot-and-layout.md](docs/claude/invariants-plot-and-layout.md) | What `plot::Figure`/`plot::Plot` can and cannot be asked to do, what a row's contents belong to, the layout editor's and the slide-over's overlay children (with the pick rule), the badge bar's fixed height, and the rule that **layout must not depend on content**. | | [invariants-readouts-and-cursors.md](docs/claude/invariants-readouts-and-cursors.md) | A readout quotes a stored sample and never a value between two; how the cursors are read back from the plots; the one formatter both panes share. | | [invariants-data-path.md](docs/claude/invariants-data-path.md) | What rebuilds the buffer and drops history, why decimation at ingest is an **average** and never a stride, and the two rules the `f64` time ring and its cached first stamp impose. | | [invariants-rendering.md](docs/claude/invariants-rendering.md) | How the renderer maps a visible range onto samples (never the `dx()` grid), what `gather()` emits and relative to which origin, why every series needs a real `MinMaxPyramid`, and why a dense trace is drawn one pixel column at a time. | @@ -61,4 +61,4 @@ The detail lives in `docs/claude/`. Read the files that touch the area you are e | [invariants-measurements.md](docs/claude/invariants-measurements.md) | Why the pane is refreshed on a cadence, why neither pass may read its window raw, how a crossing is counted with hysteresis, and which mean over a split is which. | | [invariants-math-channels.md](docs/claude/invariants-math-channels.md) | A math channel *is* a channel; the engine runs on the producer thread, so a regression there costs packets and not frames; the node editor's own rules. | | [invariants-session-and-export.md](docs/claude/invariants-session-and-export.md) | What a session file persists and in what order, when a save is refused, and when an export is collected. | -| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the bundled icons and their authoring rules, and how the Mallard manual is opened. | +| [invariants-gtk-and-chrome.md](docs/claude/invariants-gtk-and-chrome.md) | The GTK signal-signature and dialog-lifetime traps this app has already fallen into, the status bar's two tiers, the bundled icons and their authoring rules, how the slide-over's click-away coexists with the plot's gestures, how the guided tour opens a page, and how the Mallard manual is opened. | diff --git a/README.md b/README.md index bf3e487..e5fb6e8 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ switches trigger mode on, waits for an event, then leaves trigger mode the way t "Go live" button does, closes the window, and prints on stdout the status line in both states, the frame and per-channel sample counts, each plot's painted-frame count, which channel each trace slot holds, and the allocated size and reveal -state of both side panels. +state of the dock and the events pane. Every size it prints is read from a value the frame tick captured while the window was alive, not from the widget: the dump runs from the destroy handler, where the widget pointer is already freed @@ -96,9 +96,24 @@ src/plot_host.* the one plot::Figure, its rows' traces and cursors, the x lin src/export.* the visible range as a table, and the .npy / .csv / .mf3 writers src/layout_store.* the per-source session layout file (TOML), written and read back src/event_series.hpp an immutable captured event as a plot::data::Series -src/ui.* the GTK4 window: toolbar, trigger panel, signal panel, plot box +src/ui.* the GTK4 window: toolbar, trigger form, signal list, plot box, the dock +src/ui_slideover.cpp the slide-over panel over the plot's right edge, one page at a time +src/ui_badges.cpp the badge bar under the plot: the Trigger and Source badges ``` +**The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px) whose +right-hand end carries two words-only badges: **Trigger** (`off`, `armed`, `capturing`, `triggered` +or `disarmed`) and **Source** (the kind's name and `connected`/`disconnected`; a read-out, not a button, +for a source with no controls of its own, and for a file). A badge opens the **slide-over**: a 360 px +panel laid over the plot's right edge, opaque, with its own title, a *Close* button and one scroller per +page. The Trigger badge opens the trigger form; the Source badge opens the source kind's own panels +(the E-1608's *Analog out* and *Digital I/O*, behind a segmented switch at the top of the panel). It +closes with a second click on its badge, with `Esc`, with *Close*, or with a click on the plot; it never +resizes the plot. The right-hand **dock** (the existing sidebar, 320 px) holds what is left of the old +tabs in one scroller: the signal list, the cursors and the measurements, shown from the start and +insensitive until the stream has signals. It is temporary -- see "Review findings not (yet) +implemented" for what the redesign still has to do with it. + Threading: one producer thread owns the data source (the UDPStreamer client is the built-in one) and calls `poll()`; the GTK main thread owns every widget and the GL contexts. They share `ScopeModel` under one mutex, which the producer takes once per DATA @@ -1006,7 +1021,9 @@ offset = 0.5 ``` It also stores `measure_stats` (the measurements statistics toggle, a boolean, absent means -`false`). It is written (400 ms after the last change, and flushed on close) whenever the +`false`) and `dock` (whether the dock is shown, a boolean, absent means `true`; it replaced the +sidebar's `side_tab`, and a file that still carries `side_tab` loads, the key being ignored). The +open slide-over page is not stored: a panel that opens by itself at start-up would be a surprise. It is written (400 ms after the last change, and flushed on close) whenever the window, the layout, a drop or a calibration changes, and applied once, when the first CONFIG names the signals -- signal *names* are the identity, because a CONFIG renumbers and an enable/disable rebuilds the channel list. Signals the @@ -1151,18 +1168,23 @@ up, the same delay the first-run auto-start uses, the pending request starts the chooser's rows show **no accelerators** -- the window prints `F1`/`Ctrl+T` because the window binds them, and this dialog binds none. -**The guided tour is five steps on a `GtkOverlay`** inserted above the window's content: the plot, -the side panel, the trigger, the signals and the status line. There is no popover anywhere in it and +**The guided tour is six steps on a `GtkOverlay`** inserted above the window's content: the plot, +the badge bar, the trigger form, the dock, the signals and the status line. There is no popover anywhere in it and therefore no pointer grab to lose: what makes the tour modal is four dim rectangles -- above, below and either side of the target -- that swallow the clicks that land on them. A stop declares the -**page it stands on** (the trigger and the signals steps live on the sidebar's *Signals* page, named -in `TourStop::page`), and the tour selects that page as the step is entered -- on the frame *after* -the switch, since a stack allocates a new page's widgets a frame late -- so the spotlight lands on a -widget the user had left behind on the other tab instead of on nothing. `gtk_widget_get_mapped()` -decides which steps exist at all: a stop on the *wrong page* is kept, because selecting its page -reveals it, while one whose *pane is collapsed* is dropped, because the stack is unmapped with it -(a hidden page keeps its own *visible* flag true -- the widget's own flag, which is all -`is_visible()` walks -- so the old test walked over surfaces that were not on screen). The chooser's +**slide-over page it stands on** (the trigger step names `trigger`, in `TourStop::page`): the tour opens +that page as the step is entered, and a stop with no page *closes* the panel, so the card never points +at the status line over an open panel. It does not spotlight the form until the panel has finished +sliding in (about 120 ms, during which the surface is dimmed and the card waits in the corner), and the +hole is the target's bounds clipped by every scroller above it -- on the documented 756x470 window the +trigger form is taller than the panel, and the spotlight covers the part that is on screen, not the +badge bar under it. Skip, Escape and the last step close a panel the tour opened (an idle dismissal +leaves one the user opened alone). `gtk_widget_get_mapped()` +decides which steps exist at all: a stop on a slide-over page is always kept, because opening the page +reveals it, while one whose *pane is collapsed* (the dock, in a window too narrow to put it beside the +plot) is dropped, because it is unmapped with its pane (a hidden page keeps its own *visible* flag +true -- the widget's own flag, which is all `is_visible()` walks -- so the old test walked over +surfaces that were not on screen); a narrow window therefore walks four steps. The chooser's tour works the same way over its own two pages: its tabs step shows the *Waveforms* page and stepping off it -- in either direction -- brings the *Sources* page back. @@ -2780,12 +2802,13 @@ reaches every widget). The full key table: |---|---| | `a` | arm / disarm the trigger | | `Space` | pause / resume | -| `t` | show / hide the trigger panel | -| `c` | show / hide cursors | -| `m` | show / hide measurements | -| `g` | show / hide math channels | +| `t` | enable / disable trigger mode | +| `c` | enable / disable the cursors | +| `m` | go to Measure (scroll the dock to the cursors and the measurements) | +| `g` | go to Signals (scroll the dock back to the signal list) | | `l` | show / hide the layout editor | -| `s` | show / hide the side panel | +| `s` | show / hide the dock | +| `Esc` | close the slide-over panel (the guided tour takes it first) | | `f` | fit to data (the plot's own key, not an application accelerator) | | `Ctrl+E` | export visible range | | `F1` | open the manual (`help:org.udpscope.app`) in the desktop help viewer | @@ -2897,32 +2920,42 @@ now — they are listed so they are not mistaken for anything else: progress. What the spec was protecting still holds — a *held* event is bound to its snapshot alone, and the axis is the event's own window from the edge onwards rather than a sliding live view. -- **The sidebar is two tabs, and a math channel is a row of the signal list.** `SPECS.md` - names the panes as separate collapsible sections — trigger, cursors, measurements, math, - signal list — in one scrolling column; with four of them open in a 320 px sidebar every - section was short of room, and finding one meant scrolling past the others. The built - sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals** - tab (the trigger and the channels), **each a text tab** (the sidebar was icon-only for a while; glyphs for "cursors and ruler" and - "trigger and channel list" were not understandable enough, so the words won). A source kind's own tabs - (the E-1608's **Analog out** and **Digital I/O**) sit beside them as text too. Both panes also have a **locked** mode, which is what a returning user gets: the +- **The sidebar is one dock, setup is a slide-over, and a math channel is a row of the signal list.** + `SPECS.md` names the panes as separate collapsible sections -- trigger, cursors, measurements, math, + signal list -- in one scrolling column; with four of them open in a 320 px sidebar every + section was short of room, and finding one meant scrolling past the others. The sidebar was then two + text tabs (a **Measure** tab and a **Signals** tab, with a source kind's own tabs beside them), and four + tabs did not fit 320 px. It is **one dock** now, with no tab switcher: the signal list, the cursors and the + measurements in one scroller, shown from the start and insensitive until the stream has signals; `m` and + `g` scroll it to the cursors and back to the signal list. The **trigger form** and a **source's own + controls** are pages of the slide-over, reached from the badge bar. Both panes also have a **locked** mode, which is what a returning user gets: the trigger pane shows one line saying what it is set to, the signal list shows only the signals that are - on, and neither offers a control that changes the configuration — each heading has an `[edit]` button - to switch to the full pane, and back. Dragging a signal onto a plot still works while locked — filling - plots is what the list is for, not configuration — and a locked row can be dragged from anywhere on it, - the colour chip included. In a window too narrow to show both, where the sidebar is *overlaid on* the - plot, the sidebar steps aside for the length of the drag so there is something under the pointer to drop + on, and neither offers a control that changes the configuration -- each heading has an `[edit]` button + to switch to the full pane, and back. Dragging a signal onto a plot still works while locked -- filling + plots is what the list is for, not configuration -- and a locked row can be dragged from anywhere on it, + the colour chip included. In a window too narrow to show both, where the dock is *overlaid on* the + plot, the dock steps aside for the length of the drag so there is something under the pointer to drop onto. A **first run** (no session file for that source) comes up unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the - header bar's button — clicking either moves both — because a pane that is *disabled* while its - feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The - sidebar is one **dock** now, with no tab switcher: the signal list, the cursors and the measurements - in one scroller, shown from the start and insensitive until the stream has signals; `m` and `g` - scroll it to the cursors and back to the signal list. With the math - list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in + header bar's button -- clicking either moves both -- because a pane that is *disabled* while its + feature is off is a pane whose controls you would otherwise have to leave to turn it back on. With the math + list folded into the signal list under a `MATH` divider whose `+` creates a channel -- a derived channel already *is* a channel in the model, so the two lists were the same list drawn twice. No toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and - trigger-off leaves the trigger panel fully editable — a trigger is configured *before* it is - switched on, and the locked view says `stream (trigger off)` instead of the settings — and the measurements' and math panes' toolbar toggles are gone with the panes. + trigger-off leaves the trigger form fully editable -- a trigger is configured *before* it is + switched on, and the locked view says `stream (trigger off)` instead of the settings -- and the measurements' and math panes' toolbar toggles are gone with the panes. +- **The badge bar / slide-over / dock redesign is in progress; this is its first step, and it deviates + from `docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md` in three known ways.** + (1) The dock keeps the sidebar's **320 px**, not the spec's ~280: the panels it holds were built for + 320, and the width changes when they become compact readouts (step 3). (2) The session file gains the + dock's *shown* state only, not a width -- the dock's width is fixed, so there is nothing to persist + -- and does not store the open slide-over page. (3) The **Trigger badge** shows `off`, `armed` and + `triggered` as the spec says, and two more words, `capturing` (a post-trigger window is filling) and + `disarmed` (trigger on, not armed), so the label never claims a state the engine is not in. Still to + come, in the spec's order: the signal badges and drag (2), the compact readouts (3), the events + section in the dock, the deletion of the left pane and the Events toggle, and `USER-GUIDE.md` and the + manual's figures (4). Until then the events pane, the header's Events toggle and the manual still + describe the previous layout. - **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot` cannot give up a trace and the slots are therefore created up front. - **One toggle drives both cursor orientations.** The vertical markers and the @@ -3016,6 +3049,10 @@ silence timeout and then resumes, which is the EMI case end to end. rather than rebuilds, and why the frame tick reads the markers back: a parked cursor's snapped position is a real number, so nothing may be inferred from it. The trigger marker is separate: its own cursor, independent of the cursor toggle. +- **The slide-over shows a form taller than the plot by scrolling it.** The panel is as tall as the + plot and never taller, and each page has a scroller of its own, so on the documented 756x470 window + the trigger form (about 474 px) shows 253 px of itself. The tour's spotlight is clipped to that, and at + that size the card of the trigger step overlaps the panel's edge by about 18 px (`clear=0`). - **Verified headless only.** Acquisition, partitioning, the trigger state machine and its integration with the buffers, and rendering under Xvfb (llvmpipe) were exercised; the interactive paths (drag and drop, the dialogs, diff --git a/docs/claude/architecture.md b/docs/claude/architecture.md index 40c8568..67454b7 100644 --- a/docs/claude/architecture.md +++ b/docs/claude/architecture.md @@ -91,6 +91,39 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*. every handler tests `Form::alive`, which the groups' `destroy` clears, because the chooser may have destroyed the sibling widgets the handlers reach by raw pointer. +### The window's chrome: plot, slide-over, badge bar, dock + +The main window is a plot with three things around it, each a unit of `src/ui_*.cpp` and none of them +touching the data path. + +- **`SlideOver`** (`src/ui_slideover.cpp`) is one `GtkRevealer` holding a `GtkStack`, added as the second + child placed *over* the plot (after the layout editor's buttons) in `GtkOverlay#plot_overlay`. A page is + a `slideover_add_page(app, id, title, widget)`: the widget is wrapped in a `GtkScrolledWindow` of its + own and registered under its id (`trigger`, `source-`; `signal:`, `signals`, `measure`, + `cursors` and `events` are reserved for later steps). One page is open at a time and `App::slideover.page` + is its id, empty when closed. The open page is **not** a session value. Esc, *Close*, a second click on the + badge and a click on the plot close it; the click-away is a capture-phase observer (see + [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). With two or more `source-*` pages, an + `AdwToggleGroup` under the header is the segmented switch between them. +- **`BadgeBar`** (`src/ui_badges.cpp`) is the fixed-height row between the plot and the status line + (`main-window.ui` `badge_bar`, two slots: signals, left, empty until step 2; system, right, holding the + **Trigger** and **Source** badges). A badge is a flat button whose `page` is the slide-over page it + toggles; the Source badge's page is the first `source-*` page, and a kind with none (UDPS, Flat UDP, a + file) leaves it a read-out with `can_target` off. +- **The dock** is the `AdwOverlaySplitView` sidebar (`side_box` > `dock_scroller` > `dock_page`): the + signal list, the cursors and the measurements in one scroller. It holds no tab switcher and no + revealer, and it does not hold a source kind's panels (those are slide-over pages). + +**When each is built and when it is read.** `build_slideover()` runs in `on_activate` right after the tour +overlay is built (the tour's overlay wraps the whole window, so its dim rectangles cover the slide-over +too); the trigger form is registered as +a page by `build_trigger_panel`; `build_source_panels()` registers the source pages; and +`build_badge_bar()` runs *after* all of them, because the Source badge needs to know the first page. +Per-tick state (the Trigger word, the Source name and `connected`, the open marks) is written by +`sync_badge_bar()`, called from `tick()` and from `slideover_open`/`slideover_close`; it writes a label +only when its text changed. Nothing in the badge bar or the slide-over may be read from the producer +thread. + ### Data path `ScopeModel` holds **one `BufferStore`** (`live()`): one shared time-base ring plus one diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index cc7d9fc..0fb5b0a 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -60,11 +60,6 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it -- an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is the Preferences window's icon. - - The sidebar has **no tabs any more** (it is one dock): two tab glyphs (`udpscope-tab-measure-symbolic`, - `udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable - enough, so the tabs were text, and both SVGs were deleted. The *trigger's* own icon was redrawn to the - Signals glyph's picture at that time and kept it. - **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them with `gtk_accelerator_parse()` — so they are `"space"`, `"e"`, `"slash"`, @@ -249,9 +244,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear `update_side_visibility()` and the tick's per-frame block set the column's visibility from the user's wish alone (`sidebar_wanted`) and the dock's *sensitivity* from `have_signals`; the toolbar's Dock button stays sensitive throughout. The wish is persisted (`dock = true|false` in the session file) and a change of it - schedules a save. There is no tab icon to keep clear any more: the `keep_tabs_icon_free()` workaround for - `AdwViewSwitcherButton`'s placeholder `[x]` image went with the switcher, and `dock:` in the dump - replaces the `switcher:` line. + schedules a save. `dock:` in the dump reads the result. - **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an `AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides @@ -594,6 +587,21 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps 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. +- **Click-away coexists with the plot's gestures because the observer never claims the sequence.** The + slide-over closes on a press anywhere outside it, and the plot underneath already owns pan, zoom, drop and + right-click. A `GtkGestureClick` added to `plot_overlay` in `GTK_PHASE_CAPTURE` sees the press before the + plot's own controllers and **does not call `gtk_gesture_set_state()`**, so the sequence stays unclaimed and + the plot's gestures run exactly as before; claiming it (or putting a full-area target over the plot) would + swallow the very pan the click was part of. The observer decides inside/outside by + `gtk_widget_compute_point()` into the revealer plus `gtk_widget_contains()`, never by pick. The press + that closes the panel still reaches the plot, which is accepted: it is one click and not a drag the user + started on purpose. `[slideover]` reads `click_inside_closes=0 click_outside_closes=1`. +- **`AdwToggleGroup` is the segmented switch** (libadwaita >= 1.7; the build asks for 1.8). It replaces the + `AdwViewSwitcher` for a source with several panels (the E-1608: Analog out | Digital I/O). Its toggles are + named by the slide-over page ids, and the group's `notify::active-name` is the *input*; writing it + ourselves (`slideover_open` on a source page) goes through `SlideOver::syncing`, or the program's own + write comes back as a click and re-opens the page it just opened. It is hidden unless a source page is + open and there are two or more, which `[badges] switch_visible=` reads. - **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, @@ -602,15 +610,26 @@ 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 +- **A stop declares the page it stands on, and the tour selects that page as the stop is entered -- + by a hook in the main window and by a stack in the chooser.** The main window has no page stack any + more: its pages are the slide-over's. A `TourStop::page` there is a slide-over page id (`trigger`), and + `TourView::on_page(page_user, page)` opens it -- or, for a stop whose `page` is null, **closes** the + panel, so the card never points at the status line over an open panel. `tour_end()` calls + `on_page(nullptr)` *only if a walk was running* (so Skip, Esc and the last Next close a panel the tour + opened, while dismissing an idle tour does not close one the user opened). The chooser has no hook and + keeps `TourView::page_stack` (an `AdwViewStack`, selected by name). `select_view_page()` is the single + entry for both, and `show_step()` calls it before anything is measured. + Two rules follow from a panel that *slides*. The page switch is **not** followed by a one-shot measure: + `tour_update()`, which already runs every frame, fills the spotlight in on a later one, and for a stop + with a page it waits for `TourView::page_settled` (the slide-over's `gtk_revealer_get_child_revealed()`) + -- until then the target is moving, so the surface is dimmed with an empty hole. And **the hole is the + target's bounds clipped by every `GtkScrolledWindow` above it** (`tour_target_bounds()`): a form taller + than the panel's scroller overruns it, and an unclipped hole spotlights the badge bar and the status + line under the panel. A target scrolled out entirely is a *measured, empty* rectangle (the function + returns true), not a failure -- `tour_update()` zeroes the hole on it, because taking "false" for "not + measured yet" keeps the previous frame's hole standing. A stack allocates a 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 + bullet in [invariants-trigger-and-pinning.md](invariants-trigger-and-pinning.md)) -- 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 @@ -619,11 +638,14 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps `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`. + its target is **mapped**; a stop that names a page of a view with an `on_page` hook is **always** + kept (the hook opens the page whatever the dock is doing, since the slide-over does not depend on it); + and in a view with a `page_stack` a stop that names a page is kept when the stack is mapped -- a wrong + page leaves the stack mapped, so the page switch reveals it, while a collapsed pane leaves it unmapped + too and the stop is dropped. So the dock's stop is dropped in a window narrow enough to overlay it, and + the main window's tour is four steps there (`[toursurface] collapsed: ... steps=4`). The filter + lives in `tour_begin()`, not in each window's `start_tour()`, so the two windows cannot drift + into two rules. - **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 @@ -657,8 +679,9 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps - **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 - dump" rule again. `is_visible()`, not `get_visible()`: a widget under a hidden parent -- the trigger - summary while the pane is in configuration mode -- keeps its own flag true and reads 0x0, and a crop + dump" rule again. `is_visible()` *and* `get_mapped()`, not `get_visible()`: a widget under a hidden parent -- the trigger + summary while the pane is in configuration mode -- keeps its own flag true and reads 0x0 (and a widget on a closed slide-over page, `trigger_*` + while the panel is shut, would read a bogus `630,46,0,0` without the mapped test), and a crop box drawn from that is a box over the wrong thing. Its numbers depend on the *window's* size, which under the documented `xvfb-run` is the window's 756x894 minimum (a 640x480 screen), so a run on a larger screen gives different rectangles for the same panels. diff --git a/docs/claude/invariants-plot-and-layout.md b/docs/claude/invariants-plot-and-layout.md index 897d782..b96155d 100644 --- a/docs/claude/invariants-plot-and-layout.md +++ b/docs/claude/invariants-plot-and-layout.md @@ -1,6 +1,6 @@ # Invariants: the plot, the figure and the layout -How `plot::Figure`/`plot::Plot` may and may not be driven, what a row's state belongs to, the layout editor's overlay, and the rule that layout must not depend on content. +How `plot::Figure`/`plot::Plot` may and may not be driven, what a row's state belongs to, the layout editor's and the slide-over's overlay children, and the rule that layout must not depend on content. Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it. @@ -97,6 +97,31 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear an equal division. `sync_layout_editor()` runs every frame on purpose: `gtk_widget_set_margin_*()` returns early on an unchanged value, so a settled layout costs the comparisons and nothing else, and a resize needs no notification. +- **The slide-over is the second thing placed over the plot, and it is one opaque widget, not a + transparent area.** The same pick rule as the editor's buttons applies, harder: the panel is a + `GtkRevealer` aligned to the end edge (`halign=END`, `valign=FILL`) at `kSlideOverWidth` = 360 px, so + its allocation *is* the part of the plot it covers and everything to its left is still the plot's -- + pan, zoom, drop and hover are untouched. A full-area child with a panel drawn in it would swallow all + of them. Closed, the revealer is `can_target(FALSE)` (and `gtk_overlay_set_clip_overlay()` keeps its + slide-out inside the plot), so the frames of a slide-out do not eat input either. `[slideover]` reads + it back: `outside_hits_plot=1 inside_hits_panel=1`. Three consequences: + the panel **never resizes the plot** (it is an overlay child; `plot_unchanged=1`), its height is the + plot's and never the page's (each page has a `GtkScrolledWindow` with a `NEVER` horizontal policy that + does not propagate its child's height), and content that is taller than the plot -- the trigger form + on a 470 px window -- scrolls inside the panel instead of growing it. The widgets that go in a page + are reparented into it; their widths must fit 360 minus the border (the trigger form measures 267 + minimum, the E-1608's panels 197). +- **The badge bar has a fixed height and sits outside the overlay.** `badge_bar` is 36 px + (`height-request`) between `plot_overlay` and the status line, so the plot above it never moves when a + badge's text changes (`bar_h` = `bar_h_after` in `[badges]`). This is "layout must not depend on + content" for the new row: a badge's state is a word from a short fixed set and the Source badge's name + is a kind's display name, so what changes with the stream is the row's *width* (the bar has room to + spare on the right) and never its height. The badge labels are not ellipsized: a name long enough to + need it would be a defect of the kind's `display_name()`. Rows that appear by trigger condition appear + inside the slide-over's scroller, never in the bar. +- **The dock's width is fixed too** (`side_column` `width-request` 300, the split view's 320 px floor + and cap), and it holds one scroller with the signal list, the cursors and the measurements. A + section's visibility is never what makes the dock taller; the scroller is what absorbs the extra. - **Trace slots are identified by signal name, not channel index.** Enabling a signal or a new CONFIG renumbers the channel list, so an index would silently re-point a trace at another signal. diff --git a/docs/claude/invariants-session-and-export.md b/docs/claude/invariants-session-and-export.md index 169ebdb..89fc64c 100644 --- a/docs/claude/invariants-session-and-export.md +++ b/docs/claude/invariants-session-and-export.md @@ -30,10 +30,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear not freeze a colour that is meant to follow the signal list), **the math channels with their graphs**, **the measurements pane**, and **`measure_stats`** — the measurements statistics toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false` - when absent, and **`dock`** -- the Dock button's state (the user's last word on it, `sidebar_wanted`), - a bare top-level boolean written where the sidebar's `side_tab` used to be, default `true`. The parser - ignores unknown keys, so a file written before the tabs became a dock (`side_tab = "measure"`) still loads - and simply opens with the dock shown; nothing needs to read the old key. Signal identity + when absent, and **`dock`** -- `LayoutState::dock_shown`, the Dock button's state (the user's last word on it, + `App::sidebar_wanted`): a bare top-level boolean written where `side_tab` used to be, default `true`. + It is the **only** thing the dock persists -- its 320 px width is fixed, so there is nothing to store -- + and **the open slide-over page is deliberately not persisted** (a panel that opens by itself at start-up + is a surprise, and a page id from another source kind may not exist). The parser ignores unknown keys + (its key chain has no erroring final `else`), so a file written before the tabs became a dock + (`side_tab = "measure"`, or `side_tab = "source-ao"`) still loads and opens with the dock shown; + nothing reads the old key, and `[dock-session]` asserts it. Signal identity is the *name* throughout (a CONFIG renumbers channels), 'enabled' is read off the channels the model built -- an enabled signal is exactly one that produced a channel -- and `apply_layout()` installs the graphs **before** assigning the rows, since a row may hold a math channel's name and diff --git a/docs/claude/verification.md b/docs/claude/verification.md index 01b9a71..6ef4f21 100644 --- a/docs/claude/verification.md +++ b/docs/claude/verification.md @@ -15,6 +15,17 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \ Two baselines every run is read against. The UDPS runs must be **unchanged by the source-plugin work**: a UDPS self-check, number-normalised (`sed 's/[0-9.]\+/N/g'`), diffs empty against one taken before it. +For the badge bar / slide-over / dock redesign the same diff is taken **against the commit before that +series (`c572ec2`)**, from a fresh `XDG_CONFIG_HOME` on each side (a stored mock session changes the +trigger lines), and it is "unchanged except" exactly these lines: `panels:` loses `switcher=` (the +trigger form is read through the slide-over, so its width and height change, and the `compact arm +probe` width with them); `switcher:` becomes `dock: shown= sensitive= w= scroller_pages=`; +`panel controls:` prints `dock=` for `switcher=`; `help figures:` loses `side_switcher=`, replaces +`side_stack=` with `badge_bar=` and `dock=`, moves the `trigger_*` rectangles into the slide-over and +shortens `plot=` by the 36 px bar; and the `tour started: steps=` count. A self-check taken **before** +`c572ec2` is no longer the baseline for those lines. The rest is the mock's own jitter (the sign of a +`square y=[...]` or a `trig engine:` figure, the position of one `trigger: edge` line), which the +normalisation does not remove -- anything else is a finding. A clean build emits **one** warning and no more: a `-Woverlength-strings` on the icons' generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with the five action icons and the two app icons that remain (a note about a generated file, which GCC