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
AdwOverlaySplitViewsidebar ... 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'smeasureorcursorspage; 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 secondformat_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-widthinmain-window.ui), every dock label is ellipsized with a fixedwidth-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
GtkOverlaychild 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 addsmeasureandcursors. One slide-over page at a time; each page has its own scroller (slideover_add_pageprovides 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 inupdate_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 callsrebuild_measures_pane()/rebuild_cursors_pane()destroys widgets it may have been emitted from; hold the emitter withoutlive_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'sgtk warnings:reads0; noGtk-CRITICAL/Gtk-WARNING. - Verification runs are short and serial; kill every mock you start (
tools/mock_udpstreamer.pyport 8114,tools/mock_e1608.pyport 54211); seed a tour-seen config so the first-run tour does not close slide-over pages (see.superpowers/sdd/task-5-report.mdfor 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): themeasurepage 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
measureandcursorspages 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 inui_dock.cpp):GtkWidget* build_readouts_dock(App& app);-- the Readouts box (a verticalGtkBox: a "Readouts" heading; a cursors block = a row with the "Cursors" label and an "Edit"GtkButton(text), then aGtkGridof dock cursor lines; a measurements block = a row with the "Measurements" label and an "Edit" button, then a vertical box of lines). Storesapp.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 ofrebuild_cursors_pane(): rebuildsapp.dock_cursor_lines(oneDockCursorLine {GtkWidget *label,*a,*b,*d;}perapp.cursors_lines, same order) indock_cursor_grid. The first row is the same headercursors_header()writes (who/A/B/delta), small.void dock_rebuild_measures(App& app);-- called fromrebuild_measures_pane()after the page's rows: one line perapp.measures(signal chip, type name ellipsized, value label), pushing the value label toapp.dock_measure_labels(same index asapp.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);andvoid 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_openof an unknown id is a no-op; Task 2 registers the pages). -
Step 1: Dock widgets and rebuild functions. Create
ui_dock.cppwith the functions above. Fixed sizes: dock labelwidth-chars/max-width-charsof 9 for value cells, ellipsize end, CSS classcaptionandnumeric(checkui_widgets.cppfor the existing CSS before adding any). The whole Readouts box is the first child ofapp.dock_page(insert before the cursors panel withgtk_box_prepend). -
Step 2: One string, two widgets. In
update_cursors_pane()(ui_cursors.cpp) replace theput(line.a, va)etc. by a helper that sets the page cell and callsdock_cursor_cell(app, index, va, vb, vd); do the same for the blanking branch ("--"). Inupdate_measures()(ui_measure.cpp, the twogtk_label_set_text(label, ...)sites that also writeapp.measure_text[i]) calldock_measure_value(app, i, text)from the same variable. Never format twice. -
Step 3: Rebuild hooks.
rebuild_cursors_pane()(ui.cpp ~1564) ends withdock_rebuild_cursors(app);rebuild_measures_pane()callsdock_rebuild_measures(app)before its finalupdate_measures(...). Both are no-ops untilbuild_readouts_dockran (null guards). -
Step 4: Assemble. In
ui.cppwhere the dock is assembled (aftergtk_box_append(dock_page, measures_panel)), prependbuild_readouts_dock(*app)and add its separator. The Edit buttons callslideover_open(app, "cursors")/slideover_open(app, "measure"); clicking a measurement line (aGtkGestureClickon the line box, not on the value label which is selectable) callsopen_measure_dialog(app, i)with the index stored in the widget data, asmeasures_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: printdock cursors: lines=<n> page_lines=<n> same_text=<0|1>anddock measures: lines=<n> page_rows=<n> same_text=<0|1>wheresame_textcompares every dock label's text to the page label's text. Alsodock_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 inBothmode (cursors mode set viarows->set_cursor_mode). -
Step 6: Build, run the probes against
tools/mock_udpstreamer.py, verifysame_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; messageui: 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", contentapp.cursors_panel) andmeasure(title "Measurements", content the measures panel);AppAction::ShowMeasuretoggles themeasurepage;dock_show_section()and its declaration/uses are deleted. -
Step 1: Register the pages next to the
triggerandsignalsregistrations (ui.cpp~5156):slideover_add_page(*app, "cursors", "Cursors", app->cursors_panel)andslideover_add_page(*app, "measure", "Measurements", measures_panel). Remove bothgtk_box_append(dock_page, ...)of the panels and the separator between them; the dock keeps onlybuild_readouts_dock(and, later, step 4's events). -
Step 2: Keys and actions.
AppAction::ShowMeasure->slideover_toggle(app, "measure"). Deletedock_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-width280,side_columnwidth-request260; 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 thecursors_switchinside 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, thedock:reading in the layout dump, thecursors=... in the toolbar, dock=..., reset=... in the cursors paneline, any that walksdock_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.txtonly 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/*.pagementions of the cursors / measurements panels in the dock or the side panel (the full Mallard sweep andUSER-GUIDE.mdare step 4) -
Step 1: Tour. Steps pointing at
cursors_panel/cursors_head/measures_head/side_boxnow target the dock's readouts, or open thecursors/measurepage 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_headlive 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 howsignals_headwas handled). Run the help-figures probe and the tour probe (UDPSCOPE_PROBE_*inverification.md) and keep their lines sane. -
Step 3: Docs. README (layout, keys, the dock, readouts, deviations listed above),
architecture.md(chrome section),verification.mdandprobe-readings.md(new[dock]/[readouts]lines, changeddock: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).