figure: rows can hold several plots (cells), cell weights and column gap

This commit is contained in:
Martino Ferrari committed 2026-11-03 16:47:47 +01:00
1 parent f79a97be9b
commit 7d8c453232
4 files changed
+341 -31

No files matched your search

+58 -6
View File
@@ -27,6 +27,11 @@ struct FigureLayout {
/// strips and axes come from its own `Plot::layout()` against this rect, so a row
/// with a y title keeps its column and no two rows can disagree about chrome.
std::vector<Rect> rows;
/// One rect per plot, flattened row-major (row 0's cells left to right, then row 1's,
/// ...): the **viewport** each `Plot` was given. A row of one cell has exactly its row
/// rect here; a row of several tiles its rect left to right with `col_gap` between
/// cells, the cells' widths summing to the row's less the gaps.
std::vector<Rect> cells;
/// The strip the figure's one shared x axis is drawn in: the bottom row's own
/// `PlotLayout::x_label_area`, at the bottom of the bottom row's rect and so as
/// wide as that row's axes rather than the whole figure. Read back from the row
@@ -44,6 +49,10 @@ struct FigureLayout {
/// figure edge to edge, which is the default; it is dropped for a figure too
/// short to hold it, so the rows never lose their height to a gap.
f32 row_gap = 0.0f;
/// The horizontal gap between two cells of a row, in device pixels. `set_col_gap()`'s
/// value, or 0 when any row holding several cells was too narrow to hold it and
/// dropped it, so the cells never lose their width to a gap.
f32 col_gap = 0.0f;
};
/// Several plots stacked in one column, sharing one x axis.
@@ -94,13 +103,30 @@ class Figure {
/// which is the *unlinked* group: rows that share nothing, each with the range
/// its own plot was given.
explicit Figure(u32 rows);
/// A figure of `cells_per_row.size()` rows, row `r` holding `cells_per_row[r]` plots side
/// by side (a count of 0 is clamped to 1). Every row of one cell makes this the same
/// figure as `Figure(rows)`. There is deliberately no `initializer_list` constructor, so
/// `Figure{3u}` keeps meaning three rows.
explicit Figure(std::span<const u32> cells_per_row);
u32 row_count() const noexcept { return static_cast<u32>(plots_.size()); }
/// One row's plot. A row that does not exist yields a default-constructed `Plot`
/// rather than reading out of bounds -- the rule every accessor on `Plot` follows
/// -- so a stale row can only ever affect itself.
Plot& plot(u32 row) noexcept;
const Plot& plot(u32 row) const noexcept;
/// The number of figure rows -- the bands stacked top to bottom -- which equals
/// `plot_count()` for a figure no row of which is split.
u32 row_count() const noexcept { return static_cast<u32>(cells_per_row_.size()); }
/// The number of plots, i.e. cells over all rows. `plot()`, `link_x()`, `x_link()` and
/// the hovered plot of `set_crosshair()` index this flattened (row-major) range.
u32 plot_count() const noexcept { return static_cast<u32>(plots_.size()); }
/// How many cells row `row` holds; 0 for a row that does not exist.
u32 cells_in_row(u32 row) const noexcept;
/// The flattened index of row `row`'s first cell; `plot_count()` for `row ==
/// row_count()` and for anything beyond.
u32 first_plot_of_row(u32 row) const noexcept;
/// The row that holds flattened plot `plot` (the last row for an index past the end).
u32 row_of_plot(u32 plot) const noexcept;
/// One plot, by flattened cell index. A plot that does not exist yields a
/// default-constructed `Plot` rather than reading out of bounds -- the rule every
/// accessor on `Plot` follows -- so a stale index can only ever affect itself.
Plot& plot(u32 index) noexcept;
const Plot& plot(u32 index) const noexcept;
// --- geometry ---------------------------------------------------------
@@ -119,6 +145,21 @@ class Figure {
void clear_row_weights() noexcept { row_weights_.clear(); }
bool has_row_weights() const noexcept { return !row_weights_.empty(); }
/// How much of row `row`'s width each of its cells gets: `weights[i]` is cell i's
/// share of what is left after every cell has its floor width (64 logical pixels,
/// capped at an equal share of a narrow row). Normalised. Refused, changing nothing,
/// when the row does not exist, the count is not the row's cell count, or a weight is
/// not a positive finite number. A row with no weights divides equally.
Status set_cell_weights(u32 row, std::span<const f32> weights);
/// Back to an equal division of row `row`; a row that does not exist is ignored.
void clear_cell_weights(u32 row) noexcept;
bool has_cell_weights(u32 row) const noexcept;
/// The horizontal gap left between two cells of a row, in device pixels (0.0 by
/// default). Negative values are clamped to 0, and a gap too wide for a row to hold is
/// dropped for that layout rather than squeezing the cells to nothing.
void set_col_gap(f32 device_px) noexcept { col_gap_ = device_px > 0.0f ? device_px : 0.0f; }
f32 col_gap() const noexcept { return col_gap_; }
/// The gap left between two rows, in device pixels (0.0 by default, so the rows
/// tile the figure). Negative values are clamped to 0, and a gap too large for the
/// figure to hold is dropped for that layout rather than squeezing the rows to
@@ -325,7 +366,18 @@ class Figure {
/// a following group's window, push both to all members.
void sync_x_links();
/// Fills the row bookkeeping (`cells_per_row_`, `row_first_`, `cell_weights_`) from a
/// per-row cell count, clamping each to at least one. Does not touch `plots_`, which the
/// constructors size from `total_cells()`.
void build_cells(std::span<const u32> cells);
std::vector<Plot> plots_;
std::vector<u32> cells_per_row_;
/// Prefix sums of `cells_per_row_`: `rows + 1` entries, `row_first_[r]` the flattened
/// index of row r's first cell and the last entry the plot count.
std::vector<u32> row_first_;
std::vector<std::vector<f32>> cell_weights_;
f32 col_gap_ = 0.0f;
/// What `plot()` yields for a row that does not exist, so a stale row can be used
/// without reading out of bounds. Owned rather than a static, so one figure's
/// stray row can never write into another's.