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

+20 -5
View File
@@ -28,15 +28,18 @@ constexpr f64 kZoomPerPixel = std::numbers::ln10 / 200.0;
/// 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;
/// prevent. Comfortably wider than a marker's 1 px stroke, so a press lands on the
/// handle the hover highlight promised: `cursor_at()` is the *one* value both the
/// hover and the grab read, and a region's edge bands are cut back from this same
/// number, so the affordance the host sees and the gesture it gets agree.
constexpr f32 kCursorHitRadiusLogical = 7.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.
constexpr f32 kMinBoxLogical = 4.0f;
/// The narrowest *interior* a region's body keeps, in **logical** pixels: the edge bands are
/// cut back until this much of the middle is left, so a region too narrow for the full grab
/// radius on each side is still something a pointer can take hold of and *move*. Without it
/// every interior point of a region narrower than twice the radius (10 logical px) lies
/// every interior point of a region narrower than twice the radius (14 logical px) lies
/// within a bound's reach, and the bound branch would always win -- such a region could be
/// resized but never moved. Half of it either side of the midpoint, which is what makes the
/// middle of even the narrowest region with grabbable bounds (3 logical px) reachable.
@@ -457,6 +460,10 @@ void Controller::end_drag(const PointerEvent& e) {
// the drag itself stores them verbatim, so they follow the pointer instead of
// sticking to the sample grid as it moves (see Plot::snap_region()).
if (drag_region_ != kInvalid) plot_->snap_region(static_cast<u32>(drag_region_));
// And the same for a grabbed cursor: `move_cursor()` wrote the free pointer position
// for the whole gesture, so this is where the marker lands on its own snap rule --
// the drop commits the snap, both orientations alike (see Plot::snap_cursor()).
if (drag_cursor_ != kInvalid) plot_->snap_cursor(static_cast<u32>(drag_cursor_));
mode_ = DragMode::None;
selection_ = Rect{};
drag_cursor_ = kInvalid;
@@ -539,6 +546,12 @@ void Controller::move_cursor(const PointerEvent& e) {
// A grabbed cursor mutates the coordinate its own orientation owns and nothing else, and
// each half names the one setter for it -- so a ruler dragged along y can never disturb a
// marker's x, and the dead coordinate of either is untouched by construction.
//
// Both writes are `CursorPlacement::Free`: the marker follows the pointer for the whole
// gesture and lands on its grid only when the drag ends (`end_drag()` calls
// `Plot::snap_cursor()`), so a snapping cursor tracks the hand instead of hopping between
// samples -- the defect reported from use. A caller reading `cursor(id).x` / `.y` while
// the drag runs reads that free position.
if (cursor.is_horizontal()) {
// Clamped to the ruler's own y axis: a pointer that has left the axes has no data under
// it, and the coordinate it extrapolates to is past the end of the axis the ruler reads
@@ -553,10 +566,12 @@ void Controller::move_cursor(const PointerEvent& e) {
// the transform widens it. Order the bounds rather than assuming `min <= max`, or an
// inverted axis would collapse the dragged ruler onto one end (and the call is UB).
plot_->set_cursor_y(id, std::clamp(y, std::min(axis.range.min, axis.range.max),
std::max(axis.range.min, axis.range.max)));
std::max(axis.range.min, axis.range.max)),
CursorPlacement::Free);
return;
}
plot_->set_cursor_x(id, plot_->x_transform(cursor.axis_x).to_data_x(static_cast<f64>(e.x)));
plot_->set_cursor_x(id, plot_->x_transform(cursor.axis_x).to_data_x(static_cast<f64>(e.x)),
CursorPlacement::Free);
}
void Controller::move_region(const PointerEvent& e) {
+22 -4
View File
@@ -571,7 +571,7 @@ void Plot::autoscale_axes() {
}
}
void Plot::set_cursor_x(u32 id, f64 x) {
void Plot::set_cursor_x(u32 id, f64 x, CursorPlacement placement) {
if (id >= cursors_.size()) return;
Cursor& cursor = cursors_[id];
// `x` is the vertical cursor's coordinate and nobody else's: a ruler's `x` is dead, and
@@ -579,10 +579,26 @@ void Plot::set_cursor_x(u32 id, f64 x) {
// cursor is (see CursorOrientation).
if (cursor.is_horizontal()) return;
cursor.x = x;
cursor.x = snapped_x(cursor);
// `Free` stores the value and stops: a drag moves the marker under the pointer and the
// snap is a separate act (`snap_cursor()`), which is what keeps the two orientations
// agreeing about *when* they land on their grid -- see CursorPlacement.
if (placement == CursorPlacement::Snapped) cursor.x = snapped_x(cursor);
mark_measures_dirty();
}
void Plot::snap_cursor(u32 id) {
if (id >= cursors_.size()) return;
const Cursor& cursor = cursors_[id];
// One act, the orientation's own rule: the ruler's half of the same shape a region's
// `snap_region()` has. Both setters default to `Snapped`, so re-storing the coordinate
// the cursor already holds is what applies the rule.
if (cursor.is_horizontal()) {
set_cursor_y(id, cursor.y);
} else {
set_cursor_x(id, cursor.x);
}
}
void Plot::set_cursor_snap(u32 id, SnapMode mode) {
if (id >= cursors_.size()) return;
Cursor& cursor = cursors_[id];
@@ -595,12 +611,14 @@ void Plot::set_cursor_snap(u32 id, SnapMode mode) {
mark_measures_dirty();
}
void Plot::set_cursor_y(u32 id, f64 y) {
void Plot::set_cursor_y(u32 id, f64 y, CursorPlacement placement) {
if (id >= cursors_.size()) return;
Cursor& ruler = cursors_[id];
if (!ruler.is_horizontal()) return;
ruler.y = y;
ruler.y = snapped_y(ruler);
// As `set_cursor_x()`: `Free` is the drag's half of the gesture, and `snap_cursor()`
// commits the ruler's own rule on release.
if (placement == CursorPlacement::Snapped) ruler.y = snapped_y(ruler);
// No mark_measures_dirty(), for `add_h_ruler()`'s reason: a measurement is read from two
// x coordinates, and a ruler's y is not one of them.
}