feat(render): logical style metrics with a UI scale, so chrome has a consistent physical size
This commit is contained in:
1 parent
1f03631e98
commit
7500fc90d2
21 files changed
+708
-82
No files matched your search
@@ -50,6 +50,23 @@ enum class DragMode {
|
||||
///
|
||||
/// A drag that changes no range is harmless: a box-zoom selection under 4 px on
|
||||
/// either side is a click, and leaves the ranges alone.
|
||||
///
|
||||
/// ## Tolerances are logical, the coordinates they are compared against are not
|
||||
///
|
||||
/// The plot geometry the controller hit-tests in is device pixels (see
|
||||
/// `PlotLayout`), and each adapter converts its pointer coordinates into the same
|
||||
/// space before forwarding them. A tolerance written as a device constant would
|
||||
/// therefore change its *physical* size with the display: a 5 px grab radius is
|
||||
/// half the hand movement on a 2x panel, so the same cursor becomes harder to
|
||||
/// catch there. Both tolerances -- the cursor grab radius and the box-zoom click
|
||||
/// threshold -- are therefore logical constants multiplied by `Plot::ui_scale()`,
|
||||
/// which is also why the controller reads the scale off the plot it already holds
|
||||
/// instead of keeping a copy a host would have to keep in step.
|
||||
///
|
||||
/// The middle-drag axis zoom's per-pixel gain is deliberately *not* in that group:
|
||||
/// it is a data-navigation sensitivity rather than a size (the wheel and `+`/`-`
|
||||
/// are scale-free already, being counted in notches), and spec §12 reworks the
|
||||
/// zoom and pan gestures that own it.
|
||||
class Controller {
|
||||
public:
|
||||
explicit Controller(Plot& plot) noexcept : plot_(&plot) {}
|
||||
|
||||
@@ -20,6 +20,11 @@ enum class LegendPlace { TopLeft, TopRight, BottomLeft, BottomRight };
|
||||
/// All of it is deliberately coarse: the size, the row heights and the panel's
|
||||
/// extent come from the text backend's metrics and the names themselves, never from
|
||||
/// a caller's arithmetic, so a legend can only be *placed* and never mis-measured.
|
||||
///
|
||||
/// The sizes below are **logical** units, like every other metric a user
|
||||
/// perceives as a size (see the unit rule on `render::Style`): `Plot::layout()`
|
||||
/// converts each through `Style::device()` before it measures the panel, so a
|
||||
/// legend is the same physical size on a 2x panel as on a 1x one.
|
||||
struct Legend {
|
||||
/// Draw the legend at all. This is on by default and costs nothing to leave on:
|
||||
/// a legend with no entries -- no trace named, or no backend to measure a name
|
||||
|
||||
@@ -17,9 +17,11 @@
|
||||
namespace plot {
|
||||
|
||||
/// Pixel geometry of a plot's chrome, all of it in the same pixel space as
|
||||
/// `Rect` (origin top-left). `axes` is the area data is drawn into; the label
|
||||
/// areas are the strips the tick labels live in and the title areas the strips
|
||||
/// the axis titles live in, each outside `axes` and so never over the data.
|
||||
/// `Rect` (origin top-left) and therefore in **device** pixels: everything here is
|
||||
/// what the renderer draws with, already converted from the logical metrics in
|
||||
/// `Style` (see the unit rule on `Style`). `axes` is the area data is drawn into;
|
||||
/// the label areas are the strips the tick labels live in and the title areas the
|
||||
/// strips the axis titles live in, each outside `axes` and so never over the data.
|
||||
///
|
||||
/// `y_*` strips are left of `axes` and `x_*` ones below it, whatever an axis'
|
||||
/// `place` says: an axis placed on the opposite edge still has its labels and
|
||||
@@ -86,6 +88,15 @@ class Plot {
|
||||
/// uses the renderer's values.
|
||||
void set_style(const render::Style& s) noexcept { style_ = s; }
|
||||
const render::Style& style() const noexcept { return style_; }
|
||||
/// Device pixels per logical unit: what every metric in `style()` means, and what
|
||||
/// the controller's pointer tolerances are scaled by. Set from the renderer, which
|
||||
/// owns presentation -- `Renderer::draw()` writes `style().ui_scale` here before it
|
||||
/// lays the chrome out, so the strips and the strokes cannot disagree about the
|
||||
/// scale -- and set directly by a host that lays a plot out itself, or by a
|
||||
/// controller whose plot no renderer draws.
|
||||
void set_ui_scale(f32 scale) noexcept { style_.ui_scale = scale; }
|
||||
/// The scale the last layout was computed at. 1.0 until a host says otherwise.
|
||||
f32 ui_scale() const noexcept { return style_.ui_scale; }
|
||||
|
||||
/// The font layout() measures tick labels with. Borrowed, not owned, and may be
|
||||
/// null -- then the label areas fall back to PlotLayout's defaults and no string
|
||||
|
||||
@@ -52,8 +52,9 @@ class EmbeddedMonoFont final : public TextBackend {
|
||||
/// Top of the cell to the baseline, in font pixels.
|
||||
f32 ascent() const override;
|
||||
/// `kEmPx`: the size the face was baked at. The renderer scales a run by
|
||||
/// `Style::axis_font_px / em_px()`, so the same backend draws the size the
|
||||
/// style asked for whatever it was baked at.
|
||||
/// `Style::device(Style::axis_font_px) / em_px()` -- the style's logical em in
|
||||
/// device pixels over this em -- so the same backend draws the size the style
|
||||
/// asked for, on any DPI, whatever it was baked at.
|
||||
f32 em_px() const override;
|
||||
|
||||
/// The glyph for `codepoint`: its atlas cell as a normalised rect, no extra
|
||||
|
||||
@@ -91,13 +91,20 @@ class Renderer {
|
||||
/// Draws `plot`, refreshing its layout, ticks and grid first.
|
||||
///
|
||||
/// The renderer owns the presentation, style and text both: `draw()` copies
|
||||
/// `style()` into the plot and installs its own font backend on it, so the strips
|
||||
/// `style()` into the plot, hands it the style's `ui_scale` *before* it lays out
|
||||
/// (`Plot::set_ui_scale()`) and installs its own font backend on it, so the strips
|
||||
/// and tick densities the plot lays out with are the values the renderer draws
|
||||
/// with -- set colours and metrics here, not on the plot, and expect a backend
|
||||
/// set with `Plot::set_text_backend()` to be replaced on the first frame (a
|
||||
/// debug build names the one it dropped). The renderer must therefore outlive the
|
||||
/// plot's use of it.
|
||||
///
|
||||
/// Every metric in `style()` is a logical unit, so this is also where the scale
|
||||
/// becomes pixels: the font is reset at `style().device(axis_font_px)` and each
|
||||
/// stroke and padding is converted through the same call. A host therefore states
|
||||
/// its display once, in `style().ui_scale`, and the frame it gets back is the same
|
||||
/// physical size on a 1x panel and a 2x one.
|
||||
///
|
||||
/// `Plot::layout()` is computed exactly once, here, and handed to every helper:
|
||||
/// it runs the auto scale, the strip fixed-point loop and the grid build, so
|
||||
/// re-deriving it per helper would rebuild the ticks and the grid per helper.
|
||||
@@ -136,10 +143,13 @@ class Renderer {
|
||||
/// pixel-pack alignment if `dump_frame_ppm()` ran.
|
||||
void end_frame() noexcept;
|
||||
|
||||
/// The frame's colours and metrics; `draw()` hands them to the plot as well, so
|
||||
/// the layout it computes matches what is drawn. `line_width_px` is the width of
|
||||
/// the strokes framing the data (axis lines, cursor markers) -- grid lines stay
|
||||
/// hairlines and every trace carries its own width.
|
||||
/// The frame's colours and metrics; `draw()` hands them to the plot as well --
|
||||
/// style and `ui_scale` both -- so the layout it computes matches what is drawn.
|
||||
/// Every metric here is a logical unit (`Style`'s unit rule): the host writes
|
||||
/// `ui_scale` from the display it is on and this converts each metric with
|
||||
/// `device()`. `line_width_px` is the width of the strokes framing the data (axis
|
||||
/// lines, cursor markers) -- grid lines stay hairlines and every trace carries its
|
||||
/// own width.
|
||||
render::Style& style() noexcept { return style_; }
|
||||
const render::Style& style() const noexcept { return style_; }
|
||||
|
||||
@@ -162,11 +172,13 @@ class Renderer {
|
||||
bool using_persistent_upload() const noexcept {
|
||||
return line_vao_.persistent() && text_vao_.persistent();
|
||||
}
|
||||
/// The axis text size the renderer actually draws with: the on-screen cell
|
||||
/// height of the backend it draws with. `Style::axis_font_px` is an em size, so
|
||||
/// with an ascent-to-descent cell it is this that a host reads back as the line
|
||||
/// height it got -- and it is the backend's own `height()` scaled, so it always
|
||||
/// agrees with the metrics the plot laid the labels out with.
|
||||
/// The axis text size the renderer actually draws with, in **device** pixels: the
|
||||
/// on-screen cell height of the backend it draws with, which is
|
||||
/// `style().device(Style::axis_font_px)`'s own cell rather than the logical request
|
||||
/// itself. `Style::axis_font_px` is an em size, so with an ascent-to-descent cell
|
||||
/// it is this that a host reads back as the line height it got -- and it is the
|
||||
/// backend's own `height()` scaled, so it always agrees with the metrics the plot
|
||||
/// laid the labels out with.
|
||||
f32 axis_font_px() const noexcept { return font_backend_.cell_px(); }
|
||||
|
||||
/// Writes the framebuffer's viewport region as a binary PPM (P6, 8 bits per
|
||||
|
||||
@@ -6,7 +6,29 @@ 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, and 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);
|
||||
@@ -14,16 +36,47 @@ struct Style {
|
||||
Color axis_text = Color::hex(0xC8D0DC);
|
||||
Color cursor = Color::hex(0xFFB020);
|
||||
Color measure_fill = Color::hex(0xFFB020, 0.15f);
|
||||
f32 axis_font_px = 12.0f;
|
||||
f32 grid_label_pad = 4.0f;
|
||||
f32 line_width_px = 1.5f;
|
||||
f32 dense_threshold_spp = 1.5f; ///< spec §4.3: samples per pixel at or above
|
||||
///< which a trace is gathered from the LOD
|
||||
///< pyramid instead of from its raw samples.
|
||||
///< It selects the gather level, never a
|
||||
///< primitive: both sides draw through the one
|
||||
///< instanced anti-aliased quad path.
|
||||
f32 tick_min_px = 60.0f; ///< minimum spacing between major ticks.
|
||||
|
||||
// --- 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.
|
||||
f32 line_width_px = 1.5f; ///< logical width of the strokes framing the data
|
||||
///< (axis lines, cursor markers).
|
||||
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
|
||||
Reference in new issue
Block a user