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

+6
View File
@@ -249,6 +249,12 @@ void Host::refresh_sizes() noexcept {
fb_height_ = fh > 0 ? static_cast<u32>(fh) : 0u;
if (ww > 0 && fw > 0) fb_scale_x_ = static_cast<f32>(fw) / static_cast<f32>(ww);
if (wh > 0 && fh > 0) fb_scale_y_ = static_cast<f32>(fh) / static_cast<f32>(wh);
// The renderer draws its logical style metrics at this same ratio, so the chrome
// is the same physical size at any scale: one value states the display, and a
// 12 px label is 24 device px on the 2x panel both these sizes describe. Read here
// rather than in the frame loop because this is the one place the two sizes are,
// and re-read whenever the window moves to a display with another scale.
if (renderer_) renderer_->style().ui_scale = fb_scale_x_;
}
// ------------------------------------------------------------------ events
+4 -1
View File
@@ -48,7 +48,10 @@ using FrameFn = std::function<void(plot::Plot&, f64 dt)>;
/// `glfwGetWindowSize()` disagree. The host calls `begin_frame` with the
/// framebuffer size and scales every pointer coordinate by the ratio
/// `framebuffer / window`, so a gesture lands on the pixels the plot was laid out
/// in. On an unscaled display the ratio is 1.0 and nothing changes.
/// in. The same ratio states the display to the renderer
/// (`Style::ui_scale`), which is what makes the chrome -- labels, strokes, paddings
/// -- one physical size at any scale. On an unscaled display the ratio is 1.0 and
/// nothing changes.
///
/// Headless smoke mode, read once per `run()`:
///
+6
View File
@@ -303,6 +303,12 @@ void PlotArea::render() noexcept {
last_render_s_ = started;
if (frame_) frame_(*plot_, dt);
// The mapping's factor is the display's scale, so the renderer draws its logical
// style metrics at the same ratio the viewport and every forwarded coordinate are
// in: a 12 px label is the same physical size on a 1x and a 2x widget. It is the
// `::resize` ratio, not `gtk_widget_get_scale_factor()` -- the latter is the
// ceiling under fractional scaling and would over-scale the chrome by up to 60%.
renderer_->style().ui_scale = mapping_.factor;
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
renderer_->draw(*plot_);
renderer_->end_frame();
+8 -5
View File
@@ -110,11 +110,14 @@ using FrameFn = std::function<void(Plot&, f64 dt)>;
/// a delta is then a logical difference scaled by the factor in force now, never
/// the difference of two device positions (see `DragAnchor`).
///
/// Note that this also means the renderer's *style metrics* stay in device
/// pixels: at scale 2 the label font is 24 device px = 12 logical px, so labels
/// are smaller relative to the widget than at 1x. That is the same convention
/// the GLFW and Qt adapters use, and a host that wants a constant logical text
/// size scales `Renderer::style()` metrics by the factor itself.
/// The same factor states the display to the *renderer*: it is written to
/// `Renderer::style().ui_scale` every frame, which is what makes the chrome the
/// same physical size at any scale. The style's metrics are logical (see the unit
/// rule on `render::Style` -- spec §10 reverses the older convention that had them
/// in device pixels and accepted smaller labels on a high-DPI host), so at scale 2
/// the label font is 12 logical px drawn at 24 device px: the same size on screen as
/// at 1x, not a smaller one. This is the same convention the GLFW, Qt and ImGui
/// adapters use, and none of them has to scale a style metric by hand.
///
/// ## Input
///
+5
View File
@@ -119,6 +119,11 @@ void PlotWidget::draw(const char* id, Plot& plot, Vec2 size, Renderer& renderer)
controller_->set_viewport(region);
forward_input(hovered);
// The backend's framebuffer scale is the display's scale, so the renderer draws
// its logical style metrics at it: the same assertion above pins it positive,
// and the chrome is then the same physical size whatever surface the host
// renders into.
renderer.style().ui_scale = scale.x;
const u32 fb_w = static_cast<u32>(fb_w_f + 0.5f);
const u32 fb_h = static_cast<u32>(fb_h_f + 0.5f);
renderer.begin_frame(fb_w, fb_h, region);
+5
View File
@@ -179,6 +179,11 @@ void PlotWidget::paintGL() {
last_paint_s_ = started;
if (frame_) frame_(*plot_, dt);
// The same factor `device_viewport()` scales the widget by: it states the display
// for the renderer too, so the logical style metrics are drawn at this ratio and
// a 12 px label keeps its physical size on a 2x screen. Re-read every frame, like
// the viewport, so a window moved to another display is drawn at *its* scale.
renderer_->style().ui_scale = static_cast<f32>(devicePixelRatioF());
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
renderer_->draw(*plot_);
renderer_->end_frame();