feat(render): logical style metrics with a UI scale, so chrome has a consistent physical size

This commit is contained in:
Martino Ferrari committed 2026-09-28 12:17:53 +02:00
1 parent 1f03631e98
commit 7500fc90d2
21 files changed
+708 -82

No files matched your search

+32 -20
View File
@@ -322,7 +322,10 @@ const PlotLayout& Plot::layout() {
// which is precisely what stops this from fighting the range the user chose.
autoscale_axes();
const f32 pad = style_.grid_label_pad;
// The label padding is a logical size, so it is converted once, here: every strip
// and inset below reads this value in device pixels, like the backend metrics it
// is added to.
const f32 pad = style_.device(style_.grid_label_pad);
// Seeds: PlotLayout's defaults when nobody can measure text, otherwise the
// metrics the text backend does know (its line height, and just the padding
// until the first tick list says how wide the labels are).
@@ -398,9 +401,11 @@ const PlotLayout& Plot::layout() {
void Plot::refresh_ticks() {
// Ticks are generated for the final axes size: an x axis' density depends on
// the plot's width and a y axis' on its height.
for (Axis& a : x_axes_) a.refresh(layout_.axes.w, style_.tick_min_px, true);
for (Axis& a : y_axes_) a.refresh(layout_.axes.h, style_.tick_min_px, true);
// the plot's width and a y axis' on its height. The minimum spacing is a logical
// size, so it is converted here: a 2x display therefore gets the same *count* of
// major ticks over the same logical width rather than twice as many.
for (Axis& a : x_axes_) a.refresh(layout_.axes.w, style_.device(style_.tick_min_px), true);
for (Axis& a : y_axes_) a.refresh(layout_.axes.h, style_.device(style_.tick_min_px), true);
}
void Plot::build_grid() {
@@ -451,7 +456,7 @@ f32 Plot::y_title_strip_width() const {
if (text_ == nullptr) return 0.0f;
const u32 titles = titled_y_axis_count();
if (titles == 0u) return 0.0f;
return static_cast<f32>(titles) * text_->height() + 2.0f * style_.grid_label_pad;
return static_cast<f32>(titles) * text_->height() + 2.0f * style_.device(style_.grid_label_pad);
}
f32 Plot::y_label_width() const {
@@ -463,7 +468,7 @@ f32 Plot::y_label_width() const {
widest = std::max(widest, text_->measure(tick.label.c_str()));
}
}
return widest + 2.0f * style_.grid_label_pad;
return widest + 2.0f * style_.device(style_.grid_label_pad);
}
void Plot::build_legend() {
@@ -474,9 +479,17 @@ void Plot::build_legend() {
// plot that lays out exactly as it did before legends existed.
if (text_ == nullptr || !legend_.visible) return;
const f32 pad = legend_.pad;
// Every one of these is a logical size -- the legend is chrome a user reads --
// while `text_->height()` and the name measures are device: converting each one
// once, into a local, is what keeps the panel's arithmetic in device pixels
// throughout, like the strips it is fitted into.
const f32 pad = style_.device(legend_.pad);
const f32 margin = style_.device(legend_.margin);
const f32 swatch_w = style_.device(legend_.swatch_w);
const f32 swatch_h = style_.device(legend_.swatch_h);
const f32 row_gap = style_.device(legend_.row_gap);
const f32 line_h = text_->height();
const f32 row_h = std::max(legend_.swatch_h, line_h);
const f32 row_h = std::max(swatch_h, line_h);
// Entry widths, prefix-maxed: dropping from the end then costs no re-measure, and
// the panel's width after a drop is the width of an entry that is still listed.
legend_name_px_.clear();
@@ -492,8 +505,8 @@ void Plot::build_legend() {
if (entries == 0u) return;
const f32 ellipsis_w = text_->measure(kLegendEllipsis);
const f32 room_w = layout_.axes.w - 2.0f * legend_.margin;
const f32 room_h = layout_.axes.h - 2.0f * legend_.margin;
const f32 room_w = layout_.axes.w - 2.0f * margin;
const f32 room_h = layout_.axes.h - 2.0f * margin;
// Fit, from every entry down: the panel is a row per entry with `row_gap` between,
// and, once an entry has been dropped, one more row for the marker that says so.
@@ -510,8 +523,8 @@ void Plot::build_legend() {
const f32 widest = truncated ? std::max(legend_name_px_[keep - 1u], ellipsis_w)
: legend_name_px_[keep - 1u];
const f32 rows = static_cast<f32>(keep + (truncated ? 1u : 0u));
panel_w = 2.0f * pad + legend_.swatch_w + pad + widest;
panel_h = 2.0f * pad + rows * row_h + (rows - 1.0f) * legend_.row_gap;
panel_w = 2.0f * pad + swatch_w + pad + widest;
panel_h = 2.0f * pad + rows * row_h + (rows - 1.0f) * row_gap;
if (panel_w <= room_w && panel_h <= room_h) break;
if (--keep == 0u) return; // not even one entry and its marker fit
}
@@ -519,13 +532,13 @@ void Plot::build_legend() {
// Anchored in the corner `place` names and held `margin` off both edges, which is
// the inset the fit above was measured against -- so the panel is inside `axes` by
// construction and can never cross it, whatever corner it was put in.
f32 x = layout_.axes.x + legend_.margin;
f32 y = layout_.axes.y + legend_.margin;
f32 x = layout_.axes.x + margin;
f32 y = layout_.axes.y + margin;
if (legend_.place == LegendPlace::TopRight || legend_.place == LegendPlace::BottomRight) {
x = layout_.axes.right() - legend_.margin - panel_w;
x = layout_.axes.right() - margin - panel_w;
}
if (legend_.place == LegendPlace::BottomLeft || legend_.place == LegendPlace::BottomRight) {
y = layout_.axes.bottom() - legend_.margin - panel_h;
y = layout_.axes.bottom() - margin - panel_h;
}
legend.panel = Rect{x, y, panel_w, panel_h};
@@ -534,19 +547,18 @@ void Plot::build_legend() {
// The name box starts one swatch and one gutter in -- exactly what the panel's
// width allowed for -- and both boxes are centred in the row's height, so a swatch
// shorter than the text line is centred on it instead of sitting on its baseline.
const f32 name_x = x + pad + legend_.swatch_w + pad;
const f32 name_x = x + pad + swatch_w + pad;
f32 row_y = y + pad;
for (u32 i = 0; i < traces_.size() && legend.rows.size() < keep; ++i) {
const Trace& trace = traces_[i];
if (!trace.visible() || trace.name.empty()) continue;
LegendRow row;
row.trace = i;
row.swatch = Rect{x + pad, row_y + 0.5f * (row_h - legend_.swatch_h), legend_.swatch_w,
legend_.swatch_h};
row.swatch = Rect{x + pad, row_y + 0.5f * (row_h - swatch_h), swatch_w, swatch_h};
row.name = Rect{name_x, row_y + 0.5f * (row_h - line_h),
text_->measure(trace.name.c_str()), line_h};
legend.rows.push_back(row);
row_y += row_h + legend_.row_gap;
row_y += row_h + row_gap;
}
if (keep < entries) {
// Entries were dropped; the marker takes the row after the last kept one, with no