fix(input): refuse non-finite region bounds, keep narrow regions movable, republish hot handles

This commit is contained in:
Martino Ferrari committed 2026-09-28 14:08:11 +02:00
1 parent 4681d7015b
commit 7cc06f07f5
7 files changed
+263 -19

No files matched your search

+21 -7
View File
@@ -83,16 +83,22 @@ using plot::RegionGrab;
/// dragging either of the cursors that bound it, because its edges *are* those
/// cursors -- no second path, no second tolerance.
/// 2. **Regions next**, resolved nearest-first against the geometry `Plot::layout()`
/// measured: a bound inside the grab radius beats the body (so the bound of a
/// 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
/// nearer of the two bounds wins, and with no bound in range the topmost region
/// whose interior holds the pointer is the body.
/// whose interior holds the pointer is the body. The radius is the one above, cut
/// back when a region is too narrow to hold a band at each end *and* an interior:
/// a region narrower than twice it would otherwise have every interior point
/// within a bound's reach, and could be resized but never moved.
/// 3. **Nothing hot**, and the press pans.
///
/// A region narrower than a few logical pixels is a click rather than an edge grab:
/// its two bounds are under the pointer together, so an edge would always win and
/// the region could never be moved again once collapsed. Such a region is
/// grabbed by its body instead, widened to that same minimum, and it stays movable.
/// grabbed by its body instead, widened to that same minimum, and it stays movable --
/// and between that minimum and twice the grab radius, where the two bounds would
/// still cover every interior point, the edge bands are cut back until the middle is
/// left, so a region in that band of widths can be moved as well as resized.
/// The *body* drag translates both bounds by one delta -- the width is preserved
/// exactly -- while a bound drag moves one end and leaves the other where it was,
/// so a drag that crosses one bound over the other swaps them rather than inverting
@@ -103,7 +109,8 @@ using plot::RegionGrab;
/// both ends back to where they started and never move at all.
///
/// The hovered and grabbed handles are published to the plot
/// (`Plot::set_hot_handles()`) after every pointer event, so the renderer draws them
/// (`Plot::set_hot_handles()`) after every event that can move the geometry under the
/// pointer -- a pointer event, a wheel notch, a key stroke -- so the renderer draws them
/// highlighted without knowing what a pointer is; `hovered_cursor()`,
/// `grabbed_cursor()`, `hovered_region()`, `grabbed_region()` and `region_grab()` are
/// the same answer for a host that wants it directly -- a resize cursor, a tooltip,
@@ -190,7 +197,8 @@ class Controller {
/// The region the pointer is on, or `kInvalid`. `region_grab()` says which zone.
u64 hovered_region() const;
/// Which zone of `region` the pointer is on: the bound nearest within the grab
/// radius (`Left`/`Right`), else `Body` when the pointer is inside the interval --
/// radius (`Left`/`Right`) -- cut back for a narrow region, see `zone_hit()` -- else
/// `Body` when the pointer is inside the interval --
/// widened to the minimum a collapsed region keeps -- and `None` when it is on
/// neither, when the pointer is outside the plot area, or when no pointer has been
/// seen yet. What a host sets its resize cursor from; an id that names no region
@@ -229,6 +237,10 @@ class Controller {
RegionGrab zone = RegionGrab::None;
f32 distance = 0.0f;
};
/// The band a bound answers to is the controller's grab radius, cut back so that a
/// narrow region keeps an interior a body drag can reach (see the class comment):
/// `min(radius, (width - minimum interior) / 2)`, and no band at all when the two
/// bounds are closer together than the minimum width that separates them.
ZoneHit zone_hit(const RegionLayout& region, f32 px, f32 py) const;
/// Resolves that hit against the geometry `Plot::layout()` measured -- the same
/// rects the renderer paints, so what is drawn and what can be grabbed cannot
@@ -242,8 +254,10 @@ class Controller {
/// the width -- so a changed pointer motion never accumulates error.
bool begin_handle_drag(const PointerEvent& e);
/// Publishes what the pointer is on to the plot, for the renderer to highlight
/// (see HotHandles). Called after every pointer event, so hover and grab are the
/// same answer the hit test gave.
/// (see HotHandles). Called after every event that can move the geometry under the
/// pointer -- a pointer event, a wheel notch, a key stroke -- so hover and grab are
/// always the answer the hit test gives for the geometry of this frame rather than
/// the geometry of the last time the pointer moved.
void publish_hot();
/// The data x a zoom of `axis` holds fixed, for a gesture at (px, py). The
+11
View File
@@ -161,6 +161,12 @@ class Plot {
/// onto the sample grid as it is added -- exactly as `add_cursor()` places a
/// snapping cursor -- so a region handed to a plot that already holds samples is on
/// the grid from its first frame.
///
/// A region whose bounds are not both finite is *refused*: nothing is added, and the
/// returned id names no region (`region()` reads a default for it and `region_count()`
/// does not count it). Storing one would be worse than useless, because a `Free` region
/// holding a bound that is not a number could never be dragged back -- every motion
/// re-derives both bounds from the stored ones.
u32 add_region(const Region& region);
// --- accessors --------------------------------------------------------
@@ -340,6 +346,11 @@ class Plot {
/// bounds under the pointer and put them on the sample grid once, on release,
/// instead of both ends sticking as the pointer moves. Also drops the region's
/// cached statistics.
///
/// A pair of bounds that is not both finite is *refused*: `std::min`/`std::max` would
/// propagate a NaN through both stored bounds and a finite partner would be lost with
/// it, so a rejected call changes nothing at all -- the interval stays as it was, and
/// so do the statistics cached for it.
void set_region(u32 id, f64 x0, f64 x1);
/// Pulls a snapping region's bounds onto the sample grid: onto the nearest sample
+5 -3
View File
@@ -20,9 +20,11 @@ enum class RegionSnap {
/// The bounds are **ordered**: `Plot::set_region()` stores their min and max, so
/// `x0 > x1` cannot be held however the bounds arrived -- which is what keeps a
/// drag that crosses one bound over the other a *swap* (the region stays draggable
/// and never turns inside out) rather than a degenerate interval. The interval is
/// inclusive of both ends: the samples a region covers are those whose x is at or
/// after `x0` and at or before `x1`.
/// and never turns inside out) rather than a degenerate interval. A pair that is not
/// both finite is not a pair of bounds and is refused rather than stored (see
/// `Plot::set_region()`); the bounds below are finite once the region is in a plot.
/// The interval is inclusive of both ends: the samples a region covers are those whose x
/// is at or after `x0` and at or before `x1`.
///
/// A region names no x axis, unlike a cursor: it shades an interval of the plot's
/// data area as a whole -- the rect `PlotLayout::axes` and the grid are built from