feat(input): live x follow with relative zoom and mouse panning

This commit is contained in:
Martino Ferrari committed 2026-09-28 13:01:48 +02:00
1 parent eaae07d8e0
commit 77e861f79e
10 files changed
+971 -28

No files matched your search

+40 -7
View File
@@ -45,12 +45,32 @@ enum class DragMode {
/// Both axis-zoom drags read the vertical `dy` in pointer pixels with the
/// wheel's sign convention (dragging down zooms in), anchored at the pointer's
/// data coordinate on the axis they zoom. The drag has its own per-pixel
/// sensitivity -- a pixel is not a wheel notch -- of one decade per 200 px, so
/// a 200 px drag scales the span by 10.
/// sensitivity -- a pixel is not a wheel notch -- of one decade per 200 **logical**
/// px, so a 200 px drag scales the span by 10 whatever the display's scale.
///
/// A drag that changes no range is harmless: a box-zoom selection under 4 px on
/// either side is a click, and leaves the ranges alone.
///
/// ## Zoom and pan on a live x axis (spec §12)
///
/// An x axis armed with `XFollow::Live` keeps following through every gesture
/// below. Nothing here reads or writes the follow state, and the ranges these
/// gestures set are read by `Plot::set_x_range` as a span and an offset behind the
/// live edge rather than as absolute positions, so both halves of the requirement
/// fall out of the paths that already existed:
///
/// - the wheel *multiplies* the span, so notches compound and the zoom is
/// scale-free. Its anchor is the data coordinate under the pointer while the
/// pointer is inside `axes`; outside it there is no such coordinate, and on a
/// live axis the live edge is used instead (see `x_zoom_anchor()`), which leaves
/// the window ending where it ended. Either way the offset is adjusted to keep
/// the anchor still, and following stays armed;
/// - a pan shifts the offset by the drag's data delta and leaves the span alone, so
/// a user can walk back through the retained window and stops at the live edge
/// rather than past it.
///
/// An `Off` axis -- every axis by default -- behaves exactly as it always has.
///
/// ## Tolerances are logical, the coordinates they are compared against are not
///
/// The plot geometry the controller hit-tests in is device pixels (see
@@ -65,10 +85,13 @@ enum class DragMode {
/// knows what its device pixel is worth, so the controller keeps no copy of the scale
/// for a host to keep in step.
///
/// The middle-drag axis zoom's per-pixel gain is deliberately *not* in that group:
/// it is a data-navigation sensitivity rather than a size (the wheel and `+`/`-`
/// are scale-free already, being counted in notches), and spec §12 reworks the
/// zoom and pan gestures that own it.
/// The middle-drag axis zoom's per-pixel gain is logical for the same reason, by a
/// different route: it is stated in *logical* pixels (200 of them is one decade),
/// and the drag delta arrives in device pixels like every other coordinate here, so
/// `axis_zoom()` divides it by `Plot::ui_scale()` before applying the gain. Without
/// that the same physical drag would zoom twice as much at 2x. The wheel needs no
/// such conversion: a notch is already device-independent, and the wheel path is
/// left alone.
class Controller {
public:
explicit Controller(Plot& plot) noexcept : plot_(&plot) {}
@@ -115,8 +138,18 @@ class Controller {
/// kInvalid when (px, py) is outside the plot axes.
u64 cursor_at(f32 px, f32 py) const;
/// The data x a zoom of `axis` holds fixed, for a gesture at (px, py). The
/// coordinate under the pointer while the pointer is inside `axes`, which is what
/// makes a zoom feel anchored to the data being pointed at; outside `axes` there
/// is no such coordinate, and a live axis (`XFollow::Live`) uses its live edge
/// instead, so a zoom made from the label strip or off the plot zooms the window
/// about the newest sample and leaves it ending where it ended.
f64 x_zoom_anchor(u32 axis, f32 px, f32 py) const;
/// Zoom one axis about `anchor` (a data coordinate) by `factor`, recomputing
/// the range so the anchor keeps its screen position. Leaves `auto_scale` false.
/// the range so the anchor keeps its screen position. Leaves `auto_scale` false;
/// under `XFollow::Live` the range is read by `Plot::set_x_range` as a span and an
/// offset behind the live edge, which is what keeps following armed.
void zoom_x_axis(u32 axis, f64 anchor, f64 factor);
void zoom_y_axis(u32 axis, f64 anchor, f64 factor);