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
+30
-17
@@ -202,7 +202,7 @@ bool Renderer::init(Status* st) noexcept {
|
||||
const render::UploadMode mode =
|
||||
persistent ? render::UploadMode::PersistentMapped : render::UploadMode::Orphan;
|
||||
|
||||
font_backend_.reset(default_font_, style_.axis_font_px);
|
||||
font_backend_.reset(default_font_, style_.device(style_.axis_font_px));
|
||||
|
||||
// The atlas is the backend's: `sync()` uploads whatever it exposes, at whatever
|
||||
// size it says, and restores the host's unpack alignment around the upload
|
||||
@@ -296,7 +296,7 @@ void Renderer::draw(Plot& plot) {
|
||||
// The plot reserves its label strips from TextBackend metrics, so it must
|
||||
// measure with the size the text pipeline draws with, and re-read the style
|
||||
// each frame so a changed axis_font_px takes effect on the next one.
|
||||
font_backend_.reset(default_font_, style_.axis_font_px);
|
||||
font_backend_.reset(default_font_, style_.device(style_.axis_font_px));
|
||||
// The cell every glyph staged this frame is drawn in, declared from the backend
|
||||
// rather than left to whichever glyph is staged first: the instances carry rect
|
||||
// origins, and the draw carries this size (see TextBackend::cell_uv).
|
||||
@@ -327,6 +327,11 @@ void Renderer::draw(Plot& plot) {
|
||||
// from the values the renderer draws with, so the label pad is never one number
|
||||
// for the layout and another for the text placed inside it.
|
||||
plot.set_style(style_);
|
||||
// The plot's chrome is laid out in device pixels and it converts the style's
|
||||
// logical metrics itself, so it needs the scale *before* `layout()`: this is the
|
||||
// propagation point the contract names, and it is what keeps the strips the
|
||||
// layout reserves and the strokes drawn into them at one scale.
|
||||
plot.set_ui_scale(style_.ui_scale);
|
||||
// One layout for the whole frame: autoscale, the strip fixed-point loop with
|
||||
// its tick rebuilds, and the grid build all run in here, and every helper
|
||||
// below reads the strips, the axes rect and the transforms it pins.
|
||||
@@ -538,6 +543,10 @@ void Renderer::draw_grid(const Plot& plot) {
|
||||
// transforms even when a trace uses another pair.
|
||||
const bool log_x = plot.x_axis(0).log;
|
||||
const bool log_y = plot.y_axis(0).log;
|
||||
// A grid line is a size a user perceives, so it is one logical pixel wide at
|
||||
// every DPI rather than a hairline on a 1x panel and a slab on a 2x one. (Spec §11
|
||||
// gives the two classes a width each; until then both are this one logical pixel.)
|
||||
const f32 grid_width = style_.device(1.0f);
|
||||
|
||||
// Two colour groups, one pass over the segments each: `LineBatch::clear()`
|
||||
// keeps its capacity, so the second group re-uses the first one's storage.
|
||||
@@ -547,7 +556,7 @@ void Renderer::draw_grid(const Plot& plot) {
|
||||
if (line.major != majors) continue; // grid.build() already dropped non-finite ones
|
||||
stage_segment(line.x0, line.y0, line.x1, line.y1, log_x, log_y);
|
||||
}
|
||||
flush_lines(map, majors ? style_.grid_major : style_.grid_minor, 1.0f);
|
||||
flush_lines(map, majors ? style_.grid_major : style_.grid_minor, grid_width);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -566,13 +575,13 @@ void Renderer::draw_axis_lines(const Plot& plot) {
|
||||
const Axis& axis = plot.x_axis(i);
|
||||
const f64 y = axis.place == AxisPlace::Top ? y_range.max : y_range.min;
|
||||
stage_segment(axis.range.min, y, axis.range.max, y, axis.log, log_y0);
|
||||
flush_lines(mapping(plot, i, 0), style_.axis_line, style_.line_width_px);
|
||||
flush_lines(mapping(plot, i, 0), style_.axis_line, style_.device(style_.line_width_px));
|
||||
}
|
||||
for (u32 i = 0; i < plot.y_axis_count(); ++i) {
|
||||
const Axis& axis = plot.y_axis(i);
|
||||
const f64 x = axis.place == AxisPlace::Right ? x_range.max : x_range.min;
|
||||
stage_segment(x, axis.range.min, x, axis.range.max, log_x0, axis.log);
|
||||
flush_lines(mapping(plot, 0, i), style_.axis_line, style_.line_width_px);
|
||||
flush_lines(mapping(plot, 0, i), style_.axis_line, style_.device(style_.line_width_px));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -603,7 +612,7 @@ void Renderer::draw_tick_labels(const Plot& plot, const PlotLayout& layout) {
|
||||
const f32 width = font_backend_.measure(tick.label.c_str());
|
||||
const f32 centre = static_cast<f32>(transform.to_screen_y(tick.value));
|
||||
const f32 top = std::clamp(centre - 0.5f * line_h, layout.y_label_area.y, top_limit);
|
||||
stage_text({layout.y_label_area.right() - style_.grid_label_pad - width, top},
|
||||
stage_text({layout.y_label_area.right() - style_.device(style_.grid_label_pad) - width, top},
|
||||
tick.label.c_str());
|
||||
}
|
||||
}
|
||||
@@ -628,7 +637,7 @@ void Renderer::draw_titles(const Plot& plot, const PlotLayout& layout) {
|
||||
// ones: keep the leading lines that fit and stage no more. A dropped title is
|
||||
// chrome that had no room, which is the same bargain the label strips already make
|
||||
// -- never drawn outside its strip, and never inside `axes`.
|
||||
const f32 pad = style_.grid_label_pad;
|
||||
const f32 pad = style_.device(style_.grid_label_pad);
|
||||
const f32 line_h = font_backend_.height();
|
||||
|
||||
// X titles: the strip's rows top down, one per titled axis, each read left to
|
||||
@@ -753,9 +762,9 @@ void Renderer::draw_cursors(const Plot& plot, const PlotLayout& layout) {
|
||||
|
||||
stage_segment(cursor.x, y_range.min, cursor.x, y_range.max, plot.x_axis(cursor.axis_x).log,
|
||||
log_y);
|
||||
flush_lines(mapping(plot, cursor.axis_x, 0), cursor.color, style_.line_width_px);
|
||||
flush_lines(mapping(plot, cursor.axis_x, 0), cursor.color, style_.device(style_.line_width_px));
|
||||
if (!cursor.label.empty()) {
|
||||
stage_text({px + 3.0f, layout.axes.y + 2.0f}, cursor.label.c_str());
|
||||
stage_text({px + style_.device(3.0f), layout.axes.y + style_.device(2.0f)}, cursor.label.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,6 +799,10 @@ void Renderer::draw_measures(const Plot& plot, const PlotLayout& layout) {
|
||||
const f64 x = transform.to_data_x(static_cast<f64>(px) + 0.5);
|
||||
stage_segment(x, y_range.min, x, y_range.max, plot.x_axis(a.axis_x).log, log_y);
|
||||
}
|
||||
// One device pixel, not a logical one: the band is a *fill* made of one
|
||||
// stroke per framebuffer column (`px += 1.0f` above), so its width is the
|
||||
// column pitch itself. Scaling it would overlap the columns it does not
|
||||
// match and leave gaps in the ones it does.
|
||||
flush_lines(mapping(plot, a.axis_x, 0), style_.measure_fill, 1.0f);
|
||||
}
|
||||
|
||||
@@ -805,13 +818,13 @@ void Renderer::draw_measures(const Plot& plot, const PlotLayout& layout) {
|
||||
std::snprintf(readout, kReadoutChars, "dx %s dy %s rms %s mean %s", dx, dy, rms, mean);
|
||||
|
||||
const f32 width = font_backend_.measure(readout);
|
||||
const f32 limit = std::max(axes.x + 2.0f, axes.right() - width - 2.0f);
|
||||
const f32 left = std::clamp(0.5f * (x0 + x1) - 0.5f * width, axes.x + 2.0f, limit);
|
||||
const f32 limit = std::max(axes.x + style_.device(2.0f), axes.right() - width - style_.device(2.0f));
|
||||
const f32 left = std::clamp(0.5f * (x0 + x1) - 0.5f * width, axes.x + style_.device(2.0f), limit);
|
||||
// The second line of the plot area, not the first: the first belongs to the
|
||||
// cursor labels, and a wide readout clamped to the left edge would overprint
|
||||
// them. The y axis title is not a consideration here -- it lives in its own
|
||||
// strip outside the axes.
|
||||
stage_text({left, axes.y + line_h + 2.0f}, readout);
|
||||
stage_text({left, axes.y + line_h + style_.device(2.0f)}, readout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -825,11 +838,11 @@ void Renderer::draw_annotations(const Plot& plot) {
|
||||
|
||||
// A small cross pins the point; the caption sits to its right. Both are in
|
||||
// framebuffer pixels, hence the identity mapping for the marker.
|
||||
lines_.add_segment({px - 3.0f, py}, {px + 3.0f, py});
|
||||
lines_.add_segment({px, py - 3.0f}, {px, py + 3.0f});
|
||||
flush_lines(pixel_mapping(), annotation.color, 1.0f);
|
||||
lines_.add_segment({px - style_.device(3.0f), py}, {px + style_.device(3.0f), py});
|
||||
lines_.add_segment({px, py - style_.device(3.0f)}, {px, py + style_.device(3.0f)});
|
||||
flush_lines(pixel_mapping(), annotation.color, style_.device(1.0f));
|
||||
if (!annotation.text.empty()) {
|
||||
stage_text({px + 5.0f, py - 0.5f * line_h}, annotation.text.c_str());
|
||||
stage_text({px + style_.device(5.0f), py - 0.5f * line_h}, annotation.text.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,7 +872,7 @@ void Renderer::draw_legend(const Plot& plot, const PlotLayout& layout) {
|
||||
lines_.add_segment({left, bottom}, {right, bottom});
|
||||
lines_.add_segment({left, top}, {left, bottom});
|
||||
lines_.add_segment({right, top}, {right, bottom});
|
||||
flush_lines(pixel_mapping(), style_.axis_line, 1.0f);
|
||||
flush_lines(pixel_mapping(), style_.axis_line, style_.device(1.0f));
|
||||
}
|
||||
|
||||
for (const LegendRow& row : legend.rows) {
|
||||
|
||||
Reference in new issue
Block a user