Files
plotter/include/plot/render/style.hpp
T
Martino Ferrari ed3a8ce645 licence: MIT LICENSE and SPDX identifiers in every first-party file
The repository had no licence at all, so every source was all-rights-reserved
by default. Add the verbatim MIT text at LICENSE (Copyright (c) 2026 Martino
Ferrari) and a two-line SPDX header to each of the 113 first-party sources --
48 include/plot, 26 src, 10 adapters, 5 examples, 21 tests, 3 tools -- so the
grant travels with each file rather than relying on the root document alone.

Scripts keep their shebang on line 1; the SPDX lines follow it. Genuinely
vendored code is untouched: third_party/stb_truetype.h carries no header of
ours. src/render/embedded_mono_font.cpp is generated, so tools/fonts/gen_atlas.py
emit() now writes the two lines into its header comment and the file was
regenerated -- gen_atlas.py --check passes and the regeneration adds only those
comment lines to the previously committed file.

README's provenance section now states MIT and links LICENSE.

No code, test or public API change.
2026-09-28 22:13:40 +02:00

136 lines
7.6 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#pragma once
#include <plot/core/color.hpp>
namespace plot::render {
/// Every colour and metric the renderer draws with. Defaults are the dark theme
/// the plan's render smoke test asserts against; a caller swaps a field and the
/// next frame picks it up, so nothing here is baked into a shader.
///
/// ## Units: every metric below is **logical**, and `device()` converts it
///
/// 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 panel and a 2x one
/// and a 1 px grid is the same hairline on both. The host states what its display
/// is with `ui_scale` (device pixels per logical unit) and every consumer converts
/// exactly once, through `device()`: `Plot::layout()` when it sizes the label
/// strips, the title strips and the legend's chrome, the renderer when it converts
/// widths and font px.
///
/// The one exception is `dense_threshold_spp`, which is a **density** rather than a
/// size: samples per *rasterised* pixel. It is compared against a samples-per-pixel
/// ratio computed from the framebuffer's own width, so scaling it would make the
/// chosen LOD level depend on the display and not on the data. It is used raw.
///
/// The same rule classifies the metrics that live outside this struct, which are
/// logical too and converted by the same call:
///
/// * `Legend`'s pad, swatch, gap and margin;
/// * `Trace`'s (and `TraceDesc`'s) `width` -- a stroke like `line_width_px`, and
/// the one a plot draws most of, so leaving it raw would invert the
/// trace/axis stroke hierarchy at any scale above 1;
/// * the renderer's marker arm lengths and text offsets.
struct Style {
// --- colours: neither logical nor device, so nothing to scale ------------
Color background = Color::hex(0x101418);
Color grid_major = Color::hex(0x2A3038);
Color grid_minor = Color::hex(0x1C2126);
Color axis_line = Color::hex(0x8A94A6);
Color axis_text = Color::hex(0xC8D0DC);
Color cursor = Color::hex(0xFFB020);
Color measure_fill = Color::hex(0xFFB020, 0.15f);
/// The crosshair's colour: its two lines and the readout panel's border (spec §15). Markers are
/// deliberately *not* this colour -- they carry each trace's own, because a marker says *which*
/// trace and the line says *where*.
///
/// **Hue-neutral by design**, and that is the whole choice: a saturated colour here would be
/// confusable with a trace, and the library's own default trace colour is a saturated cyan
/// (`Trace`'s default, 0x40C0FF) that a hue-based reading cannot tell from a cyan crosshair --
/// only lightness would separate them, which no reader can rely on. A near-white can never be a
/// saturated trace of *any* hue, and it is still plainly not `cursor`'s orange, which is what
/// the spec asks for. The 1 px stroke is kept: form (a full-height thin line over a polyline) is
/// already distinct, so no dashes.
Color crosshair = Color::hex(0xE6EBF2);
/// The crosshair's stroke and dash, in logical pixels (spec §17). The crosshair is a
/// *passive* hover instrument drawn over the data, so its default **recedes**: a
/// thinner stroke than a trace's (`Trace::width`'s 1.5) so it reads as chrome rather
/// than as data, and a dash so it is distinguishable from the grid and the traces at
/// a glance.
///
/// `crosshair_dash_on` is the ink length and `crosshair_dash_off` the gap; **`off == 0`
/// means solid**, which is the value every *other* line in the library is drawn at, so
/// the one pipeline serves them unchanged (the dash is a fragment-shader discard on
/// `v_along`, so no geometry is added). A `Crosshair` may override all three for one
/// plot without touching this global style.
f32 crosshair_width = 0.75f;
f32 crosshair_dash_on = 4.0f;
f32 crosshair_dash_off = 4.0f;
// --- the scale ----------------------------------------------------------
/// Device pixels per logical unit. 1.0 is the 1x display every default here was
/// chosen on. Supplied by the host (`Renderer::style()` is what an adapter
/// writes), and handed to the plot by `Renderer::draw()` before it lays out, so
/// layout and drawing cannot disagree about it.
f32 ui_scale = 1.0f;
// --- logical metrics: a size a user perceives, scaled by `device()` ------
f32 axis_font_px = 12.0f; ///< logical em the axis text is drawn at.
f32 grid_label_pad = 4.0f; ///< logical gap between a tick label and the axes,
///< and the inset inside a title strip.
/// The two grid classes, each with its own width and its own switch. The grid's
/// *positions* come from the tick set alone -- `Grid::build()` tags every segment
/// with the class of the tick it came from -- so these four decide only how a
/// class is drawn. 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 (spec §11). The colours
/// above are already per class.
f32 grid_major_width = 1.0f; ///< logical width of a major (labelled-tick) line.
f32 grid_minor_width = 1.0f; ///< logical width of a minor (subdivision) line.
bool grid_major_visible = true; ///< stage the major grid at all.
bool grid_minor_visible = true; ///< stage the minor grid at all.
f32 line_width_px = 1.5f; ///< logical width of the strokes framing the data
///< (axis lines, cursor markers).
/// Logical width of a *draggable handle* drawn highlighted: the tab a hovered or
/// grabbed cursor marker is widened to, and the tab a region's hot bound is drawn
/// as -- the latter sized by `Plot::layout()` with this same metric, so the two
/// kinds of handle are one width. Wider than `line_width_px` on purpose: the cue
/// for "this marker will follow your drag" has to be visible on a marker whose
/// own colour may already be the accent.
f32 handle_width_px = 5.0f;
f32 tick_min_px = 60.0f; ///< logical minimum spacing between major ticks, so
///< a 2x display gets the same tick *count*.
// --- device metric: a density, never scaled ------------------------------
f32 dense_threshold_spp = 1.5f; ///< spec §4.3: samples per **device** pixel at or
///< above which a trace is gathered from the LOD
///< pyramid instead of from its raw samples. A
///< density, not a size: it is deliberately not
///< scaled by `ui_scale`, or the chosen level
///< would move with the display's DPI. It selects
///< the gather level, never a primitive: both
///< sides draw through the one instanced
///< anti-aliased quad path.
/// `logical` in device pixels: the single conversion point for every logical
/// metric above, so the arithmetic appears once per consumer instead of a `*
/// ui_scale` scattered over the renderer and the layout.
///
/// A scale that is not positive is not a scale -- a host that has not measured
/// its display, a field nobody filled in -- and converts 1:1 rather than
/// collapsing every strip, font and stroke to nothing.
f32 device(f32 logical) const noexcept {
return ui_scale > 0.0f ? logical * ui_scale : logical;
}
};
} // namespace plot::render