Files
udpscope/docs/superpowers/plans/2026-11-03-ui-readouts-dock.md

16 KiB

Compact readouts dock -- step 3 of the badge-bar redesign Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: The dock shows the cursors and the measurements as compact, read-only readouts (about 280 px wide, always on screen); the full cursors panel and the full measurements panel (mode, reset, add, statistics, edit) become the cursors and measure slide-over pages, reached by an "Edit" text button in the dock, by the m key, and by clicking a measurement's dock line.

Architecture: A new unit src/ui_dock.cpp (spec: ReadoutsDock) builds the dock's "Readouts" section: a cursors block (a small grid mirroring App::cursors_lines) and a measurements block (one line per App::measures). It owns no formatting: update_cursors_pane() and update_measures() already compute the strings, and they now write them to the page's widget and to the dock's, from the one string. cursors_panel and the measures panel are reparented (as the trigger form and the signal list were) into slide-over pages with ids cursors and measure. The dock's old contents (the two full panels) leave it.

Tech Stack: C++20, GTK4, libadwaita >= 1.8, CMake. No unit-test suite: verification is the app's own headless self-check (xvfb-run, mock sources) -- extend src/ui_probe.cpp probes, never a separate harness.

Spec: docs/superpowers/specs/2026-11-02-ui-badges-dock-design.md ("Right dock", "Slide-over panel", rollout step 3). Earlier plans: 2026-11-02-ui-badges-dock-shell.md (step 1), 2026-11-03-ui-badges-signals.md (step 2). Branch: feature/ui-badges-dock (continue on it).

Global Constraints

Copied from the spec and the repository's rules; every task includes them.

  • Right dock (spec, verbatim): "Fixed width, about 280 px; always on screen, shown but insensitive until the stream has signals ('disable, do not hide'). It stays an AdwOverlaySplitView sidebar ... Readouts (top): the active cursor values and measurement values, read-only, one compact line per value, through the one formatter both panes share (invariants-readouts-and-cursors.md). An 'Edit' affordance opens the slide-over's measure or cursors page; clicking a measurement row opens that measurement's editor."
  • One formatter: a readout quotes a stored sample and never a value between two (docs/claude/invariants-readouts-and-cursors.md); the dock and the page show the same string, set from the same variable in the same call -- no second format_measure() call site for the same value.
  • Layout must not depend on content (invariants-plot-and-layout.md): the dock has a fixed width (280 px: min-sidebar-width = max-sidebar-width in main-window.ui), every dock label is ellipsized with a fixed width-chars/max-width-chars, so a long unit or value never widens the dock; the badge bar keeps its fixed height; the slide-over overlays and never resizes the plot.
  • A GtkOverlay child is picked wherever its allocation is: this step adds no overlay child.
  • Page ids: trigger, signal:<name>, signals, measure, cursors, events, source-<id>. This step adds measure and cursors. One slide-over page at a time; each page has its own scroller (slideover_add_page provides it); the open page is not saved.
  • "Disable, do not hide": with the cursors off, the dock's cursor block stays on screen with -- cells (the existing blanking in update_cursors_pane), insensitive; an empty measurement list shows one dim line, never nothing.
  • Text buttons only for new controls ("Edit", not an icon). Existing icon buttons that move into the pages are not changed in this step.
  • The widget-lifetime trap (invariants-gtk-and-chrome.md): a click handler that calls rebuild_measures_pane() / rebuild_cursors_pane() destroys widgets it may have been emitted from; hold the emitter with outlive_the_rebuild(widget) first. The dock's lines are rebuilt by those two functions, so a click on a dock line must not be handled by a widget that the rebuild destroys synchronously (open the dialog from the handler, rebuild later).
  • Every new source file carries the SPDX MIT header (// SPDX-License-Identifier: MIT / // SPDX-FileCopyrightText: 2026 Martino Ferrari).
  • Baselines: a clean build emits exactly one warning (the icons' -Woverlength-strings), an incremental build zero; the dump's gtk warnings: reads 0; no Gtk-CRITICAL/Gtk-WARNING.
  • Verification runs are short and serial; kill every mock you start (tools/mock_udpstreamer.py port 8114, tools/mock_e1608.py port 54211); seed a tour-seen config so the first-run tour does not close slide-over pages (see .superpowers/sdd/task-5-report.md for commands). Do not re-run to chase cosmetics.
  • Do not push, merge or amend. Commit per task on feature/ui-badges-dock.

Deliberate step-3 deviations from the spec (state them in the final report)

  • The measurement editor stays a dialog window (open_measure_dialog): the measure page holds the list, Add, the Statistics switch and each row's edit button, which open that dialog. Moving the dialog's form into the slide-over is not needed for the readouts and is not done here.
  • The measure and cursors pages hold the existing panels unchanged (heading, mode dropdown, reset, switch, grid / add, statistics, rows), so their content is not re-designed.
  • Dock cursor lines carry the same cells as the page's grid (who, A, B, delta), at a smaller size; they are not reduced to the time line only, because the modes that read per-row values would then have no readout on screen.

File Structure

File Create/Modify Responsibility
src/ui_dock.cpp Create The dock's Readouts section: build, rebuild of the cursor lines and the measurement lines, the Edit buttons, the click-to-edit on a measurement line, the [dock] probe line.
CMakeLists.txt Modify Add src/ui_dock.cpp next to src/ui_badges.cpp.
src/ui_app.hpp Modify App::DockCursorLine, App::dock_cursor_lines, App::dock_measure_labels, App::dock_readouts widgets; declarations.
src/ui_cursors.cpp Modify update_cursors_pane() writes each cell's string to the page and to the dock; the rebuild calls the dock's rebuild.
src/ui_measure.cpp Modify update_measures() writes each reading to the page's label and to the dock's; rebuild_measures_pane() rebuilds the dock lines.
src/ui.cpp Modify Pages cursors / measure registered; panels removed from dock_page; dock_show_section() removed; ShowMeasure opens the page; dock assembly.
src/ui/main-window.ui Modify Dock width 280 (min/max-sidebar-width), side_column width-request 260, comments.
src/ui_tour.cpp, src/ui_context_help.cpp Modify Steps / hints that pointed at the cursors and measurements panels in the dock now open the page (as the signals step does).
src/ui_probe.cpp Modify Probes that walked, measured or scrolled the panels while they lived in the dock; the dock: readings.
README.md, docs/claude/*.md Modify Task 3. (USER-GUIDE.md and the Mallard manual are spec step 4.)

Task 1: The compact readouts in the dock (panels still in the dock)

The Readouts section is added at the top of the dock, above the existing full panels, so the app stays whole: nothing is removed yet. Both read from the same strings.

Files:

  • Create: src/ui_dock.cpp
  • Modify: CMakeLists.txt, src/ui_app.hpp, src/ui_cursors.cpp, src/ui_measure.cpp, src/ui.cpp (dock assembly, ~line 5161), src/ui_probe.cpp (a [dock] line in an existing probe)

Interfaces:

  • Produces (declared in ui_app.hpp, defined in ui_dock.cpp):

    • GtkWidget* build_readouts_dock(App& app); -- the Readouts box (a vertical GtkBox: a "Readouts" heading; a cursors block = a row with the "Cursors" label and an "Edit" GtkButton (text), then a GtkGrid of dock cursor lines; a measurements block = a row with the "Measurements" label and an "Edit" button, then a vertical box of lines). Stores app.dock_cursor_grid, app.dock_measures_box, app.dock_cursors_edit, app.dock_measures_edit.
    • void dock_rebuild_cursors(App& app); -- called at the end of rebuild_cursors_pane(): rebuilds app.dock_cursor_lines (one DockCursorLine {GtkWidget *label,*a,*b,*d;} per app.cursors_lines, same order) in dock_cursor_grid. The first row is the same header cursors_header() writes (who/A/B/delta), small.
    • void dock_rebuild_measures(App& app); -- called from rebuild_measures_pane() after the page's rows: one line per app.measures (signal chip, type name ellipsized, value label), pushing the value label to app.dock_measure_labels (same index as app.measure_labels). With no measurements: one dim line "no measurements".
    • void dock_cursor_cell(App& app, std::size_t line, const std::string& a, const std::string& b, const std::string& d); and void dock_measure_value(App& app, std::size_t i, const std::string& text); -- set the dock's labels; no-ops when the index is out of range (mid-rebuild).
  • Consumes: App::cursors_lines, App::measures, App::measure_labels, measure_color(), chip_markup(), signal_color_index(), open_measure_dialog(App&, int), slideover_open(App&, const std::string&) (page ids used in Task 2; in this task the Edit buttons call it and the page may not exist yet -- slideover_open of an unknown id is a no-op; Task 2 registers the pages).

  • Step 1: Dock widgets and rebuild functions. Create ui_dock.cpp with the functions above. Fixed sizes: dock label width-chars/max-width-chars of 9 for value cells, ellipsize end, CSS class caption and numeric (check ui_widgets.cpp for the existing CSS before adding any). The whole Readouts box is the first child of app.dock_page (insert before the cursors panel with gtk_box_prepend).

  • Step 2: One string, two widgets. In update_cursors_pane() (ui_cursors.cpp) replace the put(line.a, va) etc. by a helper that sets the page cell and calls dock_cursor_cell(app, index, va, vb, vd); do the same for the blanking branch ("--"). In update_measures() (ui_measure.cpp, the two gtk_label_set_text(label, ...) sites that also write app.measure_text[i]) call dock_measure_value(app, i, text) from the same variable. Never format twice.

  • Step 3: Rebuild hooks. rebuild_cursors_pane() (ui.cpp ~1564) ends with dock_rebuild_cursors(app); rebuild_measures_pane() calls dock_rebuild_measures(app) before its final update_measures(...). Both are no-ops until build_readouts_dock ran (null guards).

  • Step 4: Assemble. In ui.cpp where the dock is assembled (after gtk_box_append(dock_page, measures_panel)), prepend build_readouts_dock(*app) and add its separator. The Edit buttons call slideover_open(app, "cursors") / slideover_open(app, "measure"); clicking a measurement line (a GtkGestureClick on the line box, not on the value label which is selectable) calls open_measure_dialog(app, i) with the index stored in the widget data, as measures_row's edit button does.

  • Step 5: Probe. Add to the existing cursors probe (find it with grep -n "cursors" src/ui_probe.cpp) and measures probe a reading proving the dock agrees with the page: print dock cursors: lines=<n> page_lines=<n> same_text=<0|1> and dock measures: lines=<n> page_rows=<n> same_text=<0|1> where same_text compares every dock label's text to the page label's text. Also dock_w=<allocated width of the dock readouts box> and assert it does not exceed the sidebar width. Run with 3 measurements and the cursors on in Both mode (cursors mode set via rows->set_cursor_mode).

  • Step 6: Build, run the probes against tools/mock_udpstreamer.py, verify same_text=1, gtk warnings: 0.

  • Step 7: Commit (git add src/ui_dock.cpp CMakeLists.txt src/ui_app.hpp src/ui_cursors.cpp src/ui_measure.cpp src/ui.cpp src/ui_probe.cpp; message ui: compact cursor and measurement readouts at the top of the dock).


Task 2: The cursors and measure pages; the dock is only readouts

Files:

  • Modify: src/ui.cpp, src/ui/main-window.ui, src/ui_app.hpp, src/ui_probe.cpp, src/ui_dock.cpp

Interfaces:

  • Consumes: Task 1's build_readouts_dock, slideover_add_page(App&, id, title, widget), slideover_toggle(App&, id).

  • Produces: slide-over pages cursors (title "Cursors", content app.cursors_panel) and measure (title "Measurements", content the measures panel); AppAction::ShowMeasure toggles the measure page; dock_show_section() and its declaration/uses are deleted.

  • Step 1: Register the pages next to the trigger and signals registrations (ui.cpp ~5156): slideover_add_page(*app, "cursors", "Cursors", app->cursors_panel) and slideover_add_page(*app, "measure", "Measurements", measures_panel). Remove both gtk_box_append(dock_page, ...) of the panels and the separator between them; the dock keeps only build_readouts_dock (and, later, step 4's events).

  • Step 2: Keys and actions. AppAction::ShowMeasure -> slideover_toggle(app, "measure"). Delete dock_show_section(). Make sure the tour/help code does not call it (grep -n dock_show_section src).

  • Step 3: Dock width. main-window.ui: min-sidebar-width/max-sidebar-width 280, side_column width-request 260; update its comments.

  • Step 4: Badge/header state. The page ids join the "open" bookkeeping: slideover_page(app) reports them; nothing in the badge bar marks them (no badge). The header's Cursors toggle still mirrors the cursors_switch inside the page (both exist).

  • Step 5: Probes. Fix every probe that read the panels' allocation, sensitivity or scroll position while they sat in the dock (dock_section_probe, the dock: reading in the layout dump, the cursors=... in the toolbar, dock=..., reset=... in the cursors pane line, any that walks dock_page): they open the page first or read the widget tree without allocation. Add [readouts] pages cursors=<0|1> measure=<0|1> dock_children=<n> dock_w=<n> (pages registered; dock holds only the readouts box; width 280). Re-baseline .superpowers/sdd/ui-signals-baseline-udps.txt only where this step explains the difference.

  • Step 6: Run the UDPS and E-1608 probe runs serially; gtk warnings: 0; Esc closes the pages; the measure Add and the measurement dialog still work from the page (the existing measure probes).

  • Step 7: Commit (ui: the cursors and measurements panels are slide-over pages; the dock is the readouts).


Task 3: Tour, context help, docs

Files:

  • Modify: src/ui_tour.cpp, src/ui_context_help.cpp, README.md, docs/claude/{architecture,verification,probe-readings,invariants-plot-and-layout,invariants-readouts-and-cursors,invariants-gtk-and-chrome}.md, help/C/*.page mentions of the cursors / measurements panels in the dock or the side panel (the full Mallard sweep and USER-GUIDE.md are step 4)

  • Step 1: Tour. Steps pointing at cursors_panel/cursors_head/measures_head/side_box now target the dock's readouts, or open the cursors / measure page through the existing "tour opens a page" mechanism (invariants-gtk-and-chrome.md), as the signals step does. Rewrite the step text: readouts in the dock, "Edit" opens the page.

  • Step 2: Context help. Hints attached to cursors_head / measures_head live in the pages: show only while the page is open, or re-attach to the dock block headings -- whichever the existing layer supports without a second code path (follow how signals_head was handled). Run the help-figures probe and the tour probe (UDPSCOPE_PROBE_* in verification.md) and keep their lines sane.

  • Step 3: Docs. README (layout, keys, the dock, readouts, deviations listed above), architecture.md (chrome section), verification.md and probe-readings.md (new [dock]/[readouts] lines, changed dock: readings), invariants-readouts-and-cursors.md (the dock and the page share one string: rule and why), invariants-plot-and-layout.md (dock width 280, ellipsized cells).

  • Step 4: Run the tour/help-figure probes and one E-1608 run; confirm baselines (build 0 warnings, gtk warnings: 0).

  • Step 5: Commit (docs: the readouts dock, the cursors and measurement pages).