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

5.7 KiB
Raw Blame History

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/PlotAreas 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.