Files
udpscope/docs/superpowers/specs/2026-11-03-horizontal-split-design.md
2026-11-03 14:57:53 +01:00

93 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Horizontal plot split -- design
## Goal
Let each row of the stacked plots be split into several plots side by side. All plots share one
time window (one x link, one zoom/pan, one crosshair). A split only arranges more plots on screen; it
is not an independent time window and not an XY plot.
## Decisions
1. **Shared time window.** Cells in a row, and rows in the figure, are all members of the one x link
group. No new time semantics.
2. **Per-row split.** Each figure row holds 1..4 cells, independently of the other rows.
3. **Draggable cell widths**, saved with the session, mirroring row heights (`weight`).
4. **Caps.** At most 4 cells per row and at most 8 plots in total (`RowSet::kMaxRows` keeps its value
and now bounds plots, not figure rows). Every per-plot structure sized for 8 stays as is.
5. **Approach.** Extend the vendored plotter's `Figure` so a figure row holds several plots; the host
treats each cell as one plot. Rejected: several `Figure`/`PlotArea`s linked by hand (cannot express a
per-row split, reintroduces manual x/crosshair sync) and a general tiling tree (not asked for).
## Plotter (`deps/plotter`)
- `Figure` is constructed from cells-per-row (e.g. `{1,2,1}`). `Figure(u32 rows)` stays and means one
cell per row, so existing behaviour and `tests/test_figure.cpp` are unchanged.
- `plot(i)` indexes the flattened cells, top to bottom, left to right. `sync_x_links()` and the
crosshair already work on any set of plots and need no change.
- New: `set_cell_weights(row, weights)` and `set_col_gap(px)`. Widths use the height scheme: a floor
per cell, the weights share the rest, the last cell closes the row exactly (no seam).
- `FigureLayout` gains `cells` (one `Rect` per plot, flattened). `rows` stays the full-width rect per
figure row.
- Only the bottom figure row's cells carry x labels and the x title; other cells get `XStrip::None`.
- The left y gutter stays one common maximum for the whole figure with the same reserved floor. A
per-column gutter would make cell width depend on content, which the layout rule forbids.
- A figure too narrow for the floor widths shrinks the cells proportionally and drops the column gap,
as heights do; no rect is ever negative.
- Pointer hit-testing finds the plot under the cursor by its cell rect.
- The change is documented in `docs/claude/build-and-run.md` with the other vendored changes.
## Host (`src/plot_host.*`, `src/ui_layout.cpp`, `src/layout_store.*`, `src/ui.cpp`)
- Everything keyed by a plot index keeps meaning "flattened plot index": cursors, measurements,
`bind()`, readouts, persistence layers, `cursors_pane_bound`.
- `RowSet` gains `cells_per_row_` and per-figure-row cell weights, re-applied after every rebuild like
`row_weights_`. `split_cell(i)` / `remove_cell(i)` change a row's cell count; a removed cell drops its
traces, a new cell starts empty. `insert_row` / `remove_row` act on figure rows and carry their cells.
- `row_count()` stays the plot count; `figure_row_count()` is added. The plan may rename if this reads
badly.
- Cursors: the vertical pair is one shared time drawn in every cell; horizontal rulers are per cell.
The crosshair is one x on every cell, the hovered cell shows its y.
- Y axes are per cell (auto or manual); fit-all fits each; wheel and axis drags act on the cell under
the pointer; an x gesture anywhere moves the shared window.
- A signal dropped on a cell lands in that cell with the same slot rule as a row. The signals page's
"place on row" choice lists cells ("row 2, left") only for split rows.
- Persistence layers fill free slots of the same cell.
## Layout editor
Mirrors the row controls:
- Each vertical boundary between two cells gets a `+` (insert a cell) and a drag arrow (resize the two
neighbours). Each row's left and right edge gets a `+`, so a one-cell row can be split from either side.
- Each cell keeps its top-left `×`: in a multi-cell row it removes the cell; in a one-cell row it
removes the row, as today. Insensitive when it would leave no plot.
- Cell `+` is insensitive at 4 cells in the row or 8 plots in total; the row `+` is insensitive at 8
plots in total.
- A divider drag moves the two neighbouring widths, each floored at about 96 px logical, relative to the
weights at drag start (as the row drag is). Saved on drag end.
- Outside the editor nothing changes except the extra plots.
## Session file
`[[plot]]` tables gain `row` (the figure row) and `width` (the cell's weight). A session without `row`
loads as one cell per row, so existing sessions open unchanged. Row `weight` stays per figure row (carried
on the row's first cell).
## Verification
- `tests/test_figure.cpp`: `{1}` identical to today; `{1,2}` tiles exactly; weights and floors respected;
a narrow figure degrades without negative rects; x link and crosshair span all cells.
- New headless probe `UDPSCOPE_PROBE_SPLIT_MS`: split a row, drop a signal in each cell, drag a divider;
check the cells tile the width, the shared x range is identical, both vertical cursors line up in every
cell, widths survive a session round-trip, and a session without `row` still loads. Expect
`gtk warnings: 0`, no `Gtk-CRITICAL`.
- Regression: layout, cursors, measurements, readouts and E-1608 probes read as before; the baseline file
is re-baselined only where this feature explains a difference.
- Layout dumps and probes that print per-row layout gain a cells list and a column-weights line.
## Docs
`README.md`, `docs/USER-GUIDE.md`, `help/C/org.udpscope.app/` (layout page), `docs/claude/probe-readings.md`,
`docs/claude/verification.md`, `docs/claude/invariants-plot-and-layout.md` (cell rules),
`docs/claude/build-and-run.md` (plotter change), and `ui_context_help.cpp` where the layout editor is described.