fix(input): cursors follow the pointer while dragging and snap on drop

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:46:14 +02:00
1 parent ea70cf4fca
commit d2ba67921e
7 files changed
+292 -27

No files matched your search

+5 -2
View File
@@ -89,7 +89,10 @@ using plot::RegionGrab;
/// grabbed cursor's orientation owns -- `set_cursor_x()` for a marker,
/// `set_cursor_y()` for a ruler -- and a dragged ruler is clamped to its own y
/// axis' range, because a pointer past the top or bottom of `axes` has no data
/// under it to follow (see `move_cursor()`).
/// under it to follow (see `move_cursor()`). The writes are *free*, so the marker
/// tracks the pointer for the whole gesture rather than hopping between the samples
/// it would otherwise snap to; the cursor's own snap rule is applied once, on
/// release (`Plot::snap_cursor()`), exactly as a region's is.
/// 2. **Regions next**, resolved nearest-first against the geometry `Plot::layout()`
/// measured: a bound within the grab radius beats the body (so the bound of a
/// wide region is grabbable even though the pointer is inside the fill), the
@@ -161,7 +164,7 @@ using plot::RegionGrab;
/// The plot geometry the controller hit-tests in is device pixels (see
/// `PlotLayout`), and each adapter converts its pointer coordinates into the same
/// space before forwarding them. A tolerance written as a device constant would
/// therefore change its *physical* size with the display: a 5 px grab radius is
/// therefore change its *physical* size with the display: a 7 px grab radius is
/// half the hand movement on a 2x panel, so the same cursor becomes harder to
/// catch there. Both tolerances -- the cursor grab radius and the box-zoom click
/// threshold -- are therefore logical constants, converted through
+17
View File
@@ -12,6 +12,23 @@ enum class SnapMode {
NearestSample, ///< the cursor is pulled onto the nearest sample of the trace's x grid.
};
/// How a setter stores the coordinate it is handed: the *act* of placing, not the
/// cursor's own state (which is `SnapMode`/`HRulerSnap`).
///
/// `Snapped` applies the cursor's own snap rule at once -- the default, and what a
/// programmatic placement wants: the coordinate a caller asks for is put on the grid the
/// cursor asked for. `Free` stores the value verbatim and leaves the snap to a later act
/// (`Plot::snap_cursor()`), which is what a *drag* wants: the marker follows the pointer
/// under the hand and lands on the grid only when the gesture ends, exactly as a region's
/// bounds do between `Plot::set_region()` and `Plot::snap_region()`. The two are told
/// apart by which act the caller means, not by a second copy of the snap state -- the
/// cursor still holds one `SnapMode` and one `HRulerSnap`, and `Free` here does not
/// change either.
enum class CursorPlacement {
Snapped, ///< apply the cursor's own snap rule now (the default).
Free, ///< store the coordinate verbatim; snap later, if at all.
};
/// Which of a cursor's two coordinates is live, and how it reads: the instrument
/// the plot has always had, and the same one turned a quarter turn (spec §14).
///
+30 -5
View File
@@ -388,19 +388,44 @@ class Plot {
// --- cursors and measurements -----------------------------------------
/// Moves a **vertical** cursor along x, and invalidates the measurements that depend
/// on cursors. A `SnapMode::NearestSample` cursor is pulled onto the nearest sample of
/// the first visible trace on its x axis.
/// on cursors. With the default `CursorPlacement::Snapped`, a `SnapMode::NearestSample`
/// cursor is pulled onto the nearest sample of the first visible trace on its x axis;
/// with `CursorPlacement::Free` the value is stored verbatim, whatever the snap, and the
/// grid is applied later by `snap_cursor()`.
///
/// The two are the two things a caller does with this method: place a marker
/// programmatically (default -- snap now), or move it under a pointer for the length of
/// a gesture (`Free`). `input::Controller::move_cursor()` is the second, so **the value
/// this stores while that drag runs, and the value `cursor(id).x` reports, is the free
/// pointer position, not the snapped one** -- the snap is committed once, on release
/// (`snap_cursor()`, from `Controller::end_drag()`). A caller reading `cursor(id).x`
/// mid-drag reads what the user sees under the pointer; after the drop it reads the
/// snapped result. A host that wants the cursor to *hop* between samples as it is
/// dragged -- the old behaviour -- calls this without `Free` on every motion.
///
/// A horizontal ruler's `x` is not a coordinate of its: this is *ignored* for one, so
/// the dead field of an orientation can never be written behind its back (see
/// CursorOrientation). `set_cursor_y()` is the ruler's half of the pair.
void set_cursor_x(u32 id, f64 x);
void set_cursor_x(u32 id, f64 x, CursorPlacement placement = CursorPlacement::Snapped);
/// Sets the snap of a **vertical** cursor, re-placing it under the new rule at once.
/// Ignored for a horizontal ruler, whose snap is `HRulerSnap` -- see
/// `set_cursor_h_snap()`.
void set_cursor_snap(u32 id, SnapMode mode);
/// Moves a **horizontal** ruler along y, re-placing it under its own snap rule.
/// Applies a cursor's own snap rule to the coordinate it already holds: the nearest
/// sample for a `SnapMode::NearestSample` marker, the nearest y tick for a
/// `HRulerSnap::NearestTick` ruler, and nothing for a `Free` cursor of either
/// orientation. The one act `input::Controller::end_drag()` commits a cursor drag with,
/// so the drop lands on the grid -- the same shape as `snap_region()`, which a region
/// drag ends with, and the reason both orientations snap on drop rather than
/// continuously.
void snap_cursor(u32 id);
/// Moves a **horizontal** ruler along y, re-placing it under its own snap rule unless
/// `CursorPlacement::Free` is passed -- in which case the value is stored verbatim and
/// the snap waits for `snap_cursor()`, exactly as `set_cursor_x()`'s `Free` does for a
/// marker. The two orientations therefore snap at the *same* moment of a gesture (the
/// drop), not one continuously and the other on release.
///
/// Ignored for a vertical cursor -- `y` is not its coordinate -- and stored verbatim for
/// a ruler, exactly as `set_cursor_x()` stores a `Free` cursor's x: a y outside the axis'
@@ -409,7 +434,7 @@ class Plot {
/// that has left the axes has no data under it, so `input::Controller` keeps a dragged
/// ruler on its axis' range rather than storing a coordinate no pointer could reach
/// (see `Controller::move_cursor()`).
void set_cursor_y(u32 id, f64 y);
void set_cursor_y(u32 id, f64 y, CursorPlacement placement = CursorPlacement::Snapped);
/// Sets the snap of a **horizontal** ruler: `NearestTick` pulls its y onto the nearest
/// tick of its y axis' tick set -- majors and minors alike, the one list the axis
/// refreshed -- and `Free` leaves it where it is.