191 lines
11 KiB
Markdown
191 lines
11 KiB
Markdown
# Styling
|
||
|
||
Every colour and metric the renderer draws with lives in `render::Style`
|
||
(`include/plot/render/style.hpp`). A caller sets fields on `Renderer::style()`; the next frame
|
||
picks them up, because nothing here is baked into a shader.
|
||
|
||
Set colours and metrics on the **renderer**, not on the plot: `Renderer::draw()` copies its
|
||
`Style` into the plot and hands it the `ui_scale` *before* it lays out, so the strips, tick
|
||
densities and strokes the plot measures with are the values the renderer draws with. A plot
|
||
drawn without a renderer lays out with whatever `Plot::set_style()` was given.
|
||
|
||
## The unit rule: logical versus device
|
||
|
||
A metric a user perceives as a **size** — a font, a stroke width, a padding, a marker, a
|
||
swatch, the minimum spacing between ticks — is written in **logical** units, so a 12 px label
|
||
is the same physical size on a 1x and a 2x panel. The host states its display with `ui_scale`
|
||
(device pixels per logical unit); every consumer converts exactly once, through `device()`:
|
||
|
||
```cpp
|
||
f32 device(f32 logical) const noexcept {
|
||
return ui_scale > 0.0f ? logical * ui_scale : logical; // non-positive scale converts 1:1
|
||
}
|
||
```
|
||
|
||
`Plot::layout()` uses it to size the label strips, title strips and legend chrome; the renderer
|
||
uses it to convert widths and font px (the font is reset at `device(axis_font_px)`).
|
||
|
||
The one exception is `dense_threshold_spp`, which is a **density** (samples per *rasterised*
|
||
pixel), compared against a ratio computed from the framebuffer's own width. Scaling it would
|
||
make the chosen LOD level depend on the display rather than on the data, so it is used raw.
|
||
|
||
## `Style` fields
|
||
|
||
Colours (neither logical nor device — nothing to scale):
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `background` | `0x101418` | frame clear colour and legend/readout panel base |
|
||
| `grid_major` | `0x39414C` | major (labelled-tick) grid lines |
|
||
| `grid_minor` | `0x1C2126` at 50% alpha | minor (subdivision) grid lines |
|
||
| `axis_line` | `0x8A94A6` | axis lines |
|
||
| `axis_text` | `0xC8D0DC` | tick labels and axis titles |
|
||
| `cursor` | `0xFFB020` | cursor markers; also the hovered/grab highlight |
|
||
| `measure_fill` | `0xFFB020` at α 0.15 | the measurement band |
|
||
| `invalid_fill` | `0x8A94A6` at α 0.18 | a span the host declared invalid (`Plot::set_invalid_spans()`) |
|
||
| `invalid_repaired_fill` | `0x8A94A6` at α 0.08 | the same span once the host has marked it repaired |
|
||
| `crosshair` | `0xE6EBF2` | crosshair lines and the readout panel's border (hue-neutral by design; trace markers keep each trace's own colour) |
|
||
|
||
The scale:
|
||
|
||
| field | default | unit |
|
||
|---|---|---|
|
||
| `ui_scale` | `1.0` | device pixels per logical unit; supplied by the host |
|
||
|
||
Logical metrics (scaled by `device()`):
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `axis_font_px` | `12.0` | logical em the axis text is drawn at |
|
||
| `grid_label_pad` | `4.0` | gap between a tick label and the axes, and the inset inside a title strip |
|
||
| `marker_below_samples` | `50` | a dot per raw sample when the window holds at most this many (`0` = never) |
|
||
| `marker_px` | `3.0` | logical size of that dot |
|
||
| `grid_major_width` | `1.0` | width of a major grid line |
|
||
| `grid_minor_width` | `0.75` | width of a minor grid line (sub-pixel on purpose) |
|
||
| `grid_minor_dash_on` | `3.0` | minor grid dash: ink length in logical pixels |
|
||
| `grid_minor_dash_off` | `3.0` | minor grid dash: gap; `0` draws the minor grid solid |
|
||
| `grid_major_visible` | `true` | stage the major grid at all |
|
||
| `grid_minor_visible` | `true` | stage the minor grid at all |
|
||
| `line_width_px` | `1.5` | width of the strokes framing the data (axis lines, cursor markers) |
|
||
| `handle_width_px` | `5.0` | width of a highlighted draggable handle (a hovered/grabbed cursor tab, a region's hot bound) |
|
||
| `tick_min_px` | `60.0` | minimum spacing between major ticks |
|
||
|
||
Crosshair stroke and dash (logical):
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `crosshair_width` | `0.75` | the crosshair's stroke width (deliberately thinner than a trace's `1.5`) |
|
||
| `crosshair_dash_on` | `4.0` | ink length of the dash |
|
||
| `crosshair_dash_off` | `4.0` | gap length of the dash; **`0` means solid** |
|
||
|
||
Device metric (never scaled):
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `dense_threshold_spp` | `1.5` | samples per device pixel at or above which a trace is gathered from the LOD pyramid instead of from raw samples |
|
||
|
||
## The grid
|
||
|
||
The grid's **positions** come from the tick set alone: `Grid::build()` emits one `GridLine`
|
||
per tick, spanning the perpendicular axis' full range, tagged `major` (a labelled tick) or
|
||
not. `Style` decides only how each class is drawn — its width and visibility — and the two
|
||
colours above.
|
||
|
||
A class switched off stages **no geometry at all**: the renderer skips it before the first
|
||
segment is staged, so a dense plot's minor grid costs nothing rather than being drawn
|
||
transparent. `Grid` therefore carries both classes' geometry whatever the style says; a
|
||
caller that draws the grid itself applies the same two switches. `Grid::vertical` and
|
||
`Grid::horizontal` (both `true`) control which directions emit lines at all.
|
||
|
||
The labelled class is the **structure** and the subdivisions are **texture**, and the minor
|
||
grid's three defaults are what makes that read: it is thinner than the major hairline
|
||
(`grid_minor_width`, a sub-pixel width that at 1x is expressed as coverage and on a HiDPI
|
||
display as a narrower line), its colour carries an **alpha** so it blends into the
|
||
background instead of sitting on it, and it is **dashed** (`grid_minor_dash_on`/`_off`) so an
|
||
unlabelled line at an unlabelled position is never mistaken for a labelled one. Together
|
||
those put a minor line at an order of magnitude less ink than a major one — at full opacity and the
|
||
same width a dense minor grid reads as a second set of ticks. The dash is a fragment-shader
|
||
discard, not extra geometry: the vertices are the line either way. Set
|
||
`grid_minor_dash_off = 0` for the old solid subdivision.
|
||
|
||
## A cursor's stroke
|
||
|
||
`set_cursor_color()` gives a cursor its own colour and `set_cursor_stroke()` its own width and
|
||
dash (`Cursor::width`, `Cursor::dash`). Per cursor rather than per plot, because a host uses
|
||
cursors for two different jobs: the instruments that frame a reading — a marker at an edge, a pull
|
||
handle — and the *readout marks* a computation places on the data (a measured level, a peak, a
|
||
pulse's edges). Those want different weights. `width <= 0` means the style's default for that
|
||
cursor (`handle_width_px` while it is hovered or grabbed, `line_width_px` otherwise), and a hover
|
||
can only make the stroke *thicker*: the cue that the pointer has hold of something must not be the
|
||
thing that hides it. `dash.off <= 0` (the default) is solid — the same convention as a trace's,
|
||
the crosshair's and the grid's.
|
||
|
||
## The crosshair's dash
|
||
|
||
`crosshair_dash_on`/`crosshair_dash_off` are the fragment shader's discard pattern, so no
|
||
geometry is added. `dash_off == 0` draws a solid line — the value every other line in the
|
||
library uses, so the one pipeline serves them unchanged. A single plot may override all three
|
||
knobs without touching the global `Style` through `CrosshairStyle` (below).
|
||
|
||
## `Legend`
|
||
|
||
`Legend` (`include/plot/legend.hpp`), set with `Plot::set_legend()`. All sizes are **logical**,
|
||
converted through `Style::device()` when `Plot::layout()` measures the panel.
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `visible` | `true` | draw the legend at all (free when nothing is named) |
|
||
| `place` | `LegendPlace::TopRight` | corner of `axes` the panel is anchored to |
|
||
| `frame` | `true` | paint the panel (background at partial alpha plus a 1-px border) |
|
||
| `pad` | `6.0` | inset from the panel edge to its first row, and the swatch↔name gutter (one knob for both) |
|
||
| `swatch_w` | `14.0` | swatch width |
|
||
| `swatch_h` | `8.0` | swatch height; the row height is `max(swatch_h, line height)` |
|
||
| `row_gap` | `3.0` | blank space between rows |
|
||
| `margin` | `8.0` | inset from the axes edge, *and* the budget the panel must fit in (so it drops entries rather than overhanging) |
|
||
|
||
There is no "outside the plot" placement: the strips around `axes` belong to the tick labels
|
||
and axis titles, and a legend that reserved space would resize the plot the moment a trace was
|
||
named. A truncated legend ends with the constant `kLegendEllipsis` (`"..."`).
|
||
|
||
## `CrosshairStyle`
|
||
|
||
A per-`Crosshair` override of the three crosshair knobs, in logical pixels
|
||
(`include/plot/crosshair.hpp`), set with `Plot::set_crosshair_style()`. Every field defaults to
|
||
`-1.0`, which means "not overridden": the field falls back to the matching `Style::crosshair_*`
|
||
metric, so a default-constructed `CrosshairStyle` is exactly the global style.
|
||
|
||
| field | default | meaning |
|
||
|---|---|---|
|
||
| `width` | `-1.0` | logical stroke width; `< 0` takes `Style::crosshair_width` |
|
||
| `dash_on` | `-1.0` | logical ink length; `< 0` takes `Style::crosshair_dash_on` |
|
||
| `dash_off` | `-1.0` | logical gap; `< 0` takes `Style::crosshair_dash_off`, and `0` (once resolved) means solid |
|
||
|
||
## Traces
|
||
|
||
A trace's own `width` (`TraceDesc::width`, `Trace::width`) is **logical** too — a stroke like
|
||
`line_width_px`, converted by the same `device()` call. Its default colour is `0x4FA3FF` and
|
||
its default width `1.5`. Region fill/edge colours are the region's own (`Region::fill`
|
||
`0xFFB020` at α 0.15, `Region::edge` `0xFFB020`); a cursor's colour is per-cursor
|
||
(`0xFFB020` by default).
|
||
|
||
## Installing a text backend
|
||
|
||
The font a plot measures with and the renderer draws with is a `render::TextBackend`
|
||
(`include/plot/render/text.hpp`): `measure()`, `height()`, `ascent()`, `em_px()`, `glyph()`,
|
||
`cell_uv()`, `atlas_pixels()`, `atlas_width()`, `atlas_height()` and `revision()`.
|
||
|
||
- `Plot::set_text_backend(const render::TextBackend*)` sets the backend a plot lays its labels
|
||
out with. It is **borrowed**, not owned, and may be null (then the label areas fall back to
|
||
`PlotLayout`'s defaults and no string is measured).
|
||
- `Renderer::set_text_backend(const render::TextBackend*)` sets the face the renderer draws
|
||
with. It is also borrowed and must outlive the frames.
|
||
|
||
**Note the interaction:** `Renderer::init()` binds `render::EmbeddedMonoFont`, and every
|
||
`Renderer::draw()` installs that binding on the plot it is given — replacing whatever a host
|
||
set with `Plot::set_text_backend()` (a debug build names the one it dropped). So installing a
|
||
backend on the *plot* only takes effect for a plot no renderer draws. To draw with your own
|
||
face (say a `plot::text::TrueTypeFont` from the optional `plot::font_ttf` target), install it
|
||
with `Renderer::set_text_backend()`; the swap takes effect on the next frame, which re-reads
|
||
the backend's metrics at `style().device(axis_font_px)` and re-uploads its atlas when the
|
||
backend's `revision()` moved. Passing `nullptr` restores the embedded face.
|