fix(render): convert Trace::width through the UI scale, and fold the controller's tolerance conversion into Style::device()

This commit is contained in:
Martino Ferrari committed 2026-09-28 12:26:06 +02:00
1 parent 7500fc90d2
commit 69495a3799
7 files changed
+158 -24

No files matched your search

+9 -16
View File
@@ -21,10 +21,11 @@ constexpr f64 kKeyZoomNotches = 1.0;
/// kZoomPerNotch: 200 px of drag is one decade, exp(-200 * ln10/200) == 0.1.
constexpr f64 kZoomPerPixel = std::numbers::ln10 / 200.0;
/// How close the pointer must be to a cursor to grab it, in **logical** pixels: the
/// listener's aim is a physical distance, so the device-pixel radius it is compared
/// against is this times `Plot::ui_scale()` (see `grab_radius()`). An unscaled
/// constant would make grabbing physically harder -- half as forgiving -- on a 2x
/// display, which is the defect the scale exists to prevent.
/// listener's aim is a physical distance, so it is converted to the device-pixel
/// radius the comparison needs through `Style::device()` at the plot's own scale
/// (see `cursor_at()`). An unscaled constant would make grabbing physically harder
/// -- half as forgiving -- on a 2x display, which is the defect the scale exists to
/// prevent.
constexpr f32 kCursorHitRadiusLogical = 5.0f;
/// A box-zoom selection narrower than this on either side is a click. Logical for
/// the same reason: the gesture is the same hand movement on any display.
@@ -44,15 +45,6 @@ f64 from_space(f64 v, bool log) noexcept {
/// key) zoom in, so the span shrinks by `exp(-notches * kZoomPerNotch)`.
f64 zoom_factor(f64 notches) noexcept { return std::exp(-notches * kZoomPerNotch); }
/// Device pixels per logical unit for the tolerances below. Read from the plot the
/// controller already holds -- every forwarded coordinate is in device pixels, and
/// the plot is the one that knows what a device pixel is worth -- rather than kept
/// as a copy that a host would have to update in step with the renderer's.
f32 tolerance_scale(const Plot& plot) noexcept {
const f32 scale = plot.ui_scale();
return scale > 0.0f ? scale : 1.0f;
}
/// True when a coordinate is one arithmetic can work with. Every event here comes
/// from a host, and a host can hand over anything: a toolkit's "no pointer"
/// sentinel, a NaN from a degenerate mapping of its own, an infinity from a lost
@@ -283,8 +275,9 @@ void Controller::apply_box_zoom() {
const Rect sel = selection_;
// A selection under a few *logical* pixels on either side is a click: no zoom.
// The selection is measured in device pixels like everything the controller is
// handed, so the threshold is the logical one at this plot's scale.
const f32 min_box = kMinBoxLogical * tolerance_scale(*plot_);
// handed, so the threshold is converted at the plot's own scale -- through the one
// place that conversion lives, so a change to its fallback is picked up here too.
const f32 min_box = plot_->style().device(kMinBoxLogical);
if (sel.w < min_box || sel.h < min_box) return;
// Clamped to the axes rect the transforms are built from, so a drag that left
@@ -341,7 +334,7 @@ u64 Controller::cursor_at(f32 px, f32 py) const {
const PlotLayout& lay = std::as_const(*plot_).layout();
if (!lay.axes.contains(px, py)) return kInvalid;
const f32 radius = kCursorHitRadiusLogical * tolerance_scale(*plot_);
const f32 radius = plot_->style().device(kCursorHitRadiusLogical);
u64 best = kInvalid;
f32 best_d = 0.0f;
for (u32 i = 0; i < plot_->cursor_count(); ++i) {