#pragma once #include 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