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
@@ -25,6 +25,16 @@ f32 widest_major_label(const plot::Axis& a, const plot::render::TextBackend& t)
|
||||
return w;
|
||||
}
|
||||
|
||||
/// Every major tick's value, in order: what a tick-density assertion compares, so it
|
||||
/// can compare the *grid* the two scales produce rather than only a count.
|
||||
std::vector<f64> major_tick_values(const plot::Axis& a) {
|
||||
std::vector<f64> v;
|
||||
for (const plot::Tick& tick : a.ticks) {
|
||||
if (tick.major) v.push_back(tick.value);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/// A log axis must never be left inverted or non-positive by an auto-fit.
|
||||
void check_log_axis_range(const plot::Range& r) {
|
||||
PLOT_CHECK(std::isfinite(r.min) && std::isfinite(r.max));
|
||||
@@ -839,3 +849,88 @@ PLOT_TEST(legend_hidden_by_visible_false_draws_nothing) {
|
||||
scene.plot.set_legend(cfg);
|
||||
PLOT_CHECK(scene.plot.layout().legend.rows.size() == 3u);
|
||||
}
|
||||
|
||||
// --- the UI scale: logical metrics, one physical size at any DPI -------------
|
||||
|
||||
/// A minimum tick spacing is a physical gap between labels, so the same *logical*
|
||||
/// plot width must get the same major ticks at any scale. This is the failure mode
|
||||
/// on the under-eager side: an unscaled `tick_min_px` measured against a 2x-wide
|
||||
/// framebuffer packs twice as many ticks onto the same panel, and the grid's density
|
||||
/// becomes a property of the display instead of the data.
|
||||
PLOT_TEST(tick_spacing_is_physical) {
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
|
||||
// No y axis, and so no y label strip: with a text backend present the axes width
|
||||
// is then the viewport width *exactly* (`layout()` reserves a left strip only for
|
||||
// the axes that have labels), and the two runs differ in the display's scale and
|
||||
// nothing else. The strips' own scaling is the render smoke test's subject; this
|
||||
// case isolates the tick rule.
|
||||
plot::Plot one;
|
||||
one.set_text_backend(&font);
|
||||
one.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax1 = one.add_x_axis({"t", {0.0, 1000.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::PlotLayout& lay1 = one.layout();
|
||||
PLOT_CHECK_NEAR(lay1.axes.w, 800.0f, 1e-6);
|
||||
|
||||
plot::Plot two;
|
||||
two.set_text_backend(&font);
|
||||
two.set_ui_scale(2.0f);
|
||||
two.set_viewport({0.0f, 0.0f, 1600.0f, 800.0f}); // the same plot, a 2x display
|
||||
const u32 ax2 = two.add_x_axis({"t", {0.0, 1000.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const plot::PlotLayout& lay2 = two.layout();
|
||||
PLOT_CHECK_NEAR(lay2.axes.w, 1600.0f, 1e-6);
|
||||
|
||||
// Both axes are 800 logical units wide, so both must carry the same major ticks.
|
||||
PLOT_CHECK_NEAR(lay2.axes.w / 2.0f, lay1.axes.w, 1e-6);
|
||||
const std::vector<f64> majors1 = major_tick_values(one.x_axis(ax1));
|
||||
const std::vector<f64> majors2 = major_tick_values(two.x_axis(ax2));
|
||||
PLOT_CHECK(majors1.size() > 2u); // not vacuous: the case has ticks to lose
|
||||
PLOT_CHECK(majors2 == majors1);
|
||||
// The label strip is the same *logical* height too: its padding scaled even though
|
||||
// this backend's own metrics are fixed. (A frame drawn by a renderer also gets
|
||||
// metrics scaled with the display -- see the smoke test's two-scale case.)
|
||||
PLOT_CHECK(lay2.x_label_area.h > lay1.x_label_area.h);
|
||||
}
|
||||
|
||||
/// The scale is an opt-in: nothing changes for a caller that never sets one, which
|
||||
/// is what keeps this a unit change and not a behaviour change for existing code.
|
||||
PLOT_TEST(ui_scale_defaults_to_one) {
|
||||
plot::render::Style s;
|
||||
PLOT_CHECK(s.ui_scale == 1.0f);
|
||||
// At 1.0 the conversion is the identity, exactly -- no rounding, no reordering.
|
||||
PLOT_CHECK(s.device(0.0f) == 0.0f);
|
||||
PLOT_CHECK(s.device(1.5f) == 1.5f);
|
||||
PLOT_CHECK(s.device(12.0f) == 12.0f);
|
||||
PLOT_CHECK(s.device(1234.5f) == 1234.5f);
|
||||
// A scale that is not a scale -- a host that has not measured its display -- is
|
||||
// 1:1 rather than zero, which would collapse every strip and stroke to nothing.
|
||||
s.ui_scale = 0.0f;
|
||||
PLOT_CHECK(s.device(12.0f) == 12.0f);
|
||||
s.ui_scale = -2.0f;
|
||||
PLOT_CHECK(s.device(12.0f) == 12.0f);
|
||||
|
||||
plot::render::EmbeddedMonoFont font;
|
||||
// One plot left at the default and one told the scale is 1.0 lay out identically,
|
||||
// measured strips and legend included: the default *is* 1.0.
|
||||
plot::Plot plain;
|
||||
plot::Plot one;
|
||||
one.set_ui_scale(1.0f);
|
||||
plot::data::ArraySeries series(ramp(8), 0.0, 1.0);
|
||||
for (plot::Plot* p : {&plain, &one}) {
|
||||
p->set_text_backend(&font);
|
||||
p->set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax = p->add_x_axis({"t", {0.0, 7.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p->add_y_axis({"v", {0.0, 7.0}, false, plot::AxisPlace::Left, false});
|
||||
p->add_trace({&series, plot::Color::hex(0x40C0FF), 1.5f, ax, ay, "sine"});
|
||||
}
|
||||
const plot::PlotLayout& a = plain.layout();
|
||||
const plot::PlotLayout& b = one.layout();
|
||||
PLOT_CHECK(plain.ui_scale() == 1.0f);
|
||||
PLOT_CHECK(b.y_axis_width == a.y_axis_width);
|
||||
PLOT_CHECK(b.x_axis_height == a.x_axis_height);
|
||||
PLOT_CHECK(b.y_title_width == a.y_title_width);
|
||||
PLOT_CHECK(b.axes.x == a.axes.x);
|
||||
PLOT_CHECK(b.axes.h == a.axes.h);
|
||||
PLOT_CHECK(b.legend.panel.w == a.legend.panel.w);
|
||||
PLOT_CHECK(b.legend.panel.h == a.legend.panel.h);
|
||||
}
|
||||
Reference in new issue
Block a user