Files
plotter/docs/styling.md
T

162 lines
8.4 KiB
Markdown
Raw 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.
# 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` | `0x2A3038` | major (labelled-tick) grid lines |
| `grid_minor` | `0x1C2126` | 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 |
| `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 |
| `grid_major_width` | `1.0` | width of a major grid line |
| `grid_minor_width` | `1.0` | width of a minor grid line |
| `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 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.