docs: horizontal plot split

This commit is contained in:
Martino Ferrari committed 2026-11-03 19:14:57 +01:00
1 parent 7730b35fe3
commit 2183317953
13 files changed
+319 -39

No files matched your search

+57 -1
View File
@@ -52,7 +52,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
up front and rebinding via `SlotSeries`; a *name* change (legend text) is the one
thing that forces a rebuild. Cursors are parked outside the axis range instead of
removed.
- **A `Figure`'s row count is fixed at construction**, so `RowSet::rebuild_all()` builds
- **A `Figure`'s shape (its cells per row) is fixed at construction**, so `RowSet::rebuild_all()` builds
a *new* figure and the area is handed it (`PlotArea::set_figure()`); it costs no GL
state. That makes one path for "a row's traces changed" and "the row count changed",
and it is why `rebuild_all()` restates a pinned event window (`pinned_pre_`/
@@ -105,6 +105,60 @@ 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.
- **A figure row can be split into cells, and every plot index in the host is the flattened one.**
`RowSet::row_count()` is the number of **plots** and `row(i)`, `Row::plot`, the cursors, the
measurements, `bind()`, the readouts and the persistence layers all index plots row-major (row 0's cells
left to right, then row 1's); `figure_row_count()` is the number of figure rows, and
`cells_in_row()`/`first_plot_of_row()`/`figure_row_of()`/`cell_of()` translate. **Code that means "the
rows of the figure" -- the editor's row `+`, the row drag, the row weights, `fl.rows` -- must say
`figure_row_count()`**: with no split the two are equal and a slip passes every unsplit probe, which
is how a row weight once drifted off its row (see the `row-weights` reading). The caps are
`kMaxCellsPerRow` = 4 per row and `kMaxRows` = 8 **plots** in all (the constant bounds plots now, not
figure rows), so every per-plot structure sized for 8 is unchanged. `plot_label()` is what the UI
prints for a plot: the row's number (`2`) for a one-cell row, the number and a letter (`2a`..`2d`) for
a split one -- never a flat index, which is 0-based and says nothing about where the plot is.
What the cells share and what each keeps:
- **One time window.** Every cell of every row is a member of the one x link group, so a pan or zoom
in any cell moves them all, the crosshair is one x drawn in every cell (the hovered cell shows its
y), and the **vertical cursor pair is one shared time drawn in every cell** (`cursors_aligned`).
A split is an arrangement of more plots, not a second time scale: nothing about the time axis changes
(see [invariants-time-axis-and-window.md](invariants-time-axis-and-window.md)).
- **Per-cell y.** Each cell has its own y axis (auto or fixed), its own horizontal rulers and its own
fit; a wheel or axis drag acts on the cell under the pointer, an x gesture on any bottom-row cell
moves the shared window. The cursors pane's ruler label is `plot 2a` for a plot of a split row and
`row N` (the plot index) for one of an unsplit row, so the pane reads the same as before until a row
is split.
- **Bottom-row x labels only.** The plotter gives every plot that is not in the last figure row
`XStrip::None`; the cells of the bottom row each keep their strip, so a middle-drag on the axis
works in any of them. The `t0` caption goes on those cells too.
- **One left gutter.** The y gutter is one maximum for the whole figure with the same reserved floor
as before (`Figure::set_min_y_gutter()`), **not a per-column one**: a per-column gutter would make a
cell's width depend on its labels, which "layout must not depend on content" forbids. A cell whose y
labels are narrower than the gutter simply has a wider margin.
- **Widths are weights with a floor.** The plotter keeps 64 px logical per cell and a column gap
(`kColGap`, 6 px); the editor's drag is stricter (below). A weight is a share of what is
left *after* the floors, so cells of `{3, 1}` read 533:220 in a 756 px figure, not 3:1, and the
editor's 96 px drag floor is a floor on the *share it asks for*: a cell dragged to the limit realises
`64 + share * remainder` and reads wider (144 px in the split probe's `pre-insert0` line), so the
boundary does not follow the pointer one for one near either extreme.
- **A cell is removed with its traces, and a new cell is empty.** `insert_cell(fr, at)` pushes the
cells at and after `at` right *with their contents* (the same rule as `insert_row()`), and
`remove_cell(plot)` on a one-cell row removes the row. Both return false at a cap or when they would
leave no plot, and never grow past 4 / 8.
**The editor's controls** (`rebuild_layout_editor()`): a cell `+` (`kLayoutCellKey`, tooltip "split
here") at a row's left edge, at its right edge and at every internal boundary -- `n + 1` of them for a
row of `n` cells, so a one-cell row can be split from either side -- inserting at that position; a drag
arrow (`kLayoutColKey`, `go-next-symbolic`) at each internal boundary that moves the boundary between
its two neighbours, **each floored at `kMinCellPx` = 96 px logical** (or half the pair when the pair is
narrower) and measured from the two widths at drag start, so the drag does not compound; and the per-plot
`×`, which removes the cell in a split row ("remove this plot") and the row in a one-cell row ("remove
this row"), insensitive when it would leave no plot. Cell `+` is insensitive at 4 cells in its row or 8
plots in all; the row `+` is insensitive at 8 plots. **The cell `+` is keyed by `kLayoutCellKey` (figure row * 8 + position + 1) and
never by `set_row_key()`**: `row_key()` is a plot or boundary index, `layout_button()` finds a button by it,
and a cell `+` that also carried one would be found as a row button; `sync_layout_editor()` tells the kinds
apart by which key it finds. Like every editor control the
cell buttons are placed from `FigureLayout::cells`, divided by `device_scale()`, and the editor does
not draw while the figure's rects are not yet one per plot.
- **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
@@ -189,6 +243,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
per-channel list, and why `format_elapsed()` steps to a coarser unit instead of
letting the counter grow: each is a status field whose *width* would otherwise depend
on the stream, and the status row is directly above the plot.
A split row keeps this rule: cell widths come from the weights and the 64 px floor and never from what
a cell's labels need, and the gutter stays one figure-wide maximum.
The reservation goes on the **figure** (`Figure::set_min_y_gutter()`, added to the
plotter for this), not on the rows: `Figure::layout()` clears every row's
`set_min_y_gutter()` to measure what its own labels need, so a floor written onto the