fix(input): refuse non-finite region bounds, keep narrow regions movable, republish hot handles
This commit is contained in:
1 parent
4681d7015b
commit
7cc06f07f5
7 files changed
+263
-19
No files matched your search
@@ -1204,3 +1204,105 @@ PLOT_TEST(region_snap_is_applied_once_on_release) {
|
||||
PLOT_CHECK_NEAR(f.plot.region(rid).x1, 22.0, 1e-9);
|
||||
PLOT_CHECK(f.plot.region(rid).x1 > f.plot.region(rid).x0); // the interval survived both ends moving
|
||||
}
|
||||
|
||||
/// A region the brief promises a pointer can still *move*: one whose bounds sit closer
|
||||
/// together than the fixed grab radius allows on both sides. With that radius kept on each
|
||||
/// bound, every point of a region narrower than twice it (10 logical px) lies within a
|
||||
/// bound's reach, so the edge branch always wins and the interior is unreachable -- such a
|
||||
/// region could be resized but never moved, and the collapsed-region rescue only engages
|
||||
/// below 3 px. The bands are therefore cut back until the middle is left, while a region
|
||||
/// wide enough for two full bands and an interior keeps the radius it always had.
|
||||
PLOT_TEST(narrow_region_stays_movable_by_its_body) {
|
||||
Fixture f;
|
||||
const f32 radius = f.plot.style().device(5.0f); // the controller's own grab radius, logical
|
||||
f32 column = 300.0f; // one strip of columns per region below
|
||||
|
||||
for (const f32 width_px : {4.0f, 6.0f, 9.0f}) {
|
||||
const f64 a = data_at(f.plot, f.ax, column);
|
||||
const f64 b = data_at(f.plot, f.ax, column + width_px);
|
||||
const u32 rid = f.plot.add_region(free_region(a, b));
|
||||
const f32 middle = column + 0.5f * width_px;
|
||||
|
||||
// Both bounds are grabbable: the edge still wins where the pointer is on it ...
|
||||
hover(f.controller, column);
|
||||
PLOT_CHECK(f.controller.region_grab(rid) == plot::RegionGrab::Left);
|
||||
hover(f.controller, column + width_px);
|
||||
PLOT_CHECK(f.controller.region_grab(rid) == plot::RegionGrab::Right);
|
||||
// ... and the middle is the body, so the region can be moved as well as resized.
|
||||
hover(f.controller, middle);
|
||||
PLOT_CHECK(f.controller.region_grab(rid) == plot::RegionGrab::Body);
|
||||
press(f.controller, middle);
|
||||
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveRegion);
|
||||
PLOT_CHECK(f.controller.grabbed_region() == rid);
|
||||
move_to(f.controller, middle + 30.0f);
|
||||
const f64 delta = data_at(f.plot, f.ax, middle + 30.0f) - data_at(f.plot, f.ax, middle);
|
||||
PLOT_CHECK_NEAR(f.plot.region(rid).x0, a + delta, 1e-9);
|
||||
PLOT_CHECK_NEAR(f.plot.region(rid).x1, b + delta, 1e-9); // moved, width intact
|
||||
release(f.controller, middle + 30.0f);
|
||||
column += 80.0f;
|
||||
}
|
||||
|
||||
// Where there is room the radius is unchanged: a press a few pixels inside a wide
|
||||
// region's bound is that bound and not the body.
|
||||
const u32 wide = f.plot.add_region(
|
||||
free_region(data_at(f.plot, f.ax, 600.0f), data_at(f.plot, f.ax, 700.0f)));
|
||||
hover(f.controller, 600.0f + radius - 1.0f);
|
||||
PLOT_CHECK(f.controller.region_grab(wide) == plot::RegionGrab::Left);
|
||||
press(f.controller, 600.0f + radius - 1.0f);
|
||||
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::ResizeRegionLeft);
|
||||
release(f.controller, 600.0f + radius - 1.0f);
|
||||
}
|
||||
|
||||
/// `publish_hot()` is the single writer of `HotHandles`, and the wheel and the keyboard
|
||||
/// change the geometry under a *stationary* pointer. Without a publish of their own, a
|
||||
/// frame drawn right after a scroll would highlight a handle the pointer has left -- or
|
||||
/// miss one it has just landed on -- until the next mouse move, which is exactly the
|
||||
/// stale-highlight defect the publish exists to prevent. The layout has to be refreshed
|
||||
/// with it: the region rects the hit test reads were measured for the range the zoom has
|
||||
/// just replaced.
|
||||
PLOT_TEST(hot_handles_follow_a_gesture_that_moves_the_geometry) {
|
||||
Fixture f;
|
||||
const u32 cid = f.plot.add_cursor(50.0, plot::Color::hex(0x00FF00));
|
||||
const u32 off_centre = f.plot.add_cursor(75.0, plot::Color::hex(0xFF00FF));
|
||||
const u32 rid = f.plot.add_region(free_region(70.0, 90.0));
|
||||
const f32 radius = f.plot.style().device(5.0f);
|
||||
|
||||
// The pointer sits four pixels to the right of the marker: on it, and published as such.
|
||||
const f32 px = screen_x_of(f.plot, f.ax, 50.0) + 4.0f;
|
||||
hover(f.controller, px);
|
||||
PLOT_CHECK(f.plot.hot_handles().cursor == cid);
|
||||
|
||||
// A zoom *in* about the pointer grows the marker's offset from it by the zoom factor --
|
||||
// 4 px becomes 6.3 -- so the marker leaves without the pointer moving at all.
|
||||
f.controller.on_wheel({px, 200.0f, 0.0f, 3.0f, 0u});
|
||||
PLOT_CHECK(std::fabs(screen_x_of(f.plot, f.ax, 50.0) - px) > radius);
|
||||
PLOT_CHECK(f.controller.hovered_cursor() == plot::kInvalid);
|
||||
PLOT_CHECK(f.plot.hot_handles().cursor == plot::kInvalid);
|
||||
|
||||
// And the other way round: a zoom out about the same column brings it back under the
|
||||
// stationary pointer, where the published state has to be the marker again.
|
||||
f.controller.on_wheel({px, 200.0f, 0.0f, -3.0f, 0u});
|
||||
PLOT_CHECK(std::fabs(screen_x_of(f.plot, f.ax, 50.0) - px) <= radius);
|
||||
PLOT_CHECK(f.controller.hovered_cursor() == cid);
|
||||
PLOT_CHECK(f.plot.hot_handles().cursor == cid);
|
||||
|
||||
// The keyboard path: a zoom about the plot centre moves the *other* marker, which the
|
||||
// pointer is on, out from under it.
|
||||
const f32 on_marker = screen_x_of(f.plot, f.ax, 75.0);
|
||||
hover(f.controller, on_marker);
|
||||
PLOT_CHECK(f.plot.hot_handles().cursor == off_centre);
|
||||
f.controller.on_key({plot::input::kKeyZoomIn, 0u, true});
|
||||
PLOT_CHECK(std::fabs(screen_x_of(f.plot, f.ax, 75.0) - on_marker) > radius);
|
||||
PLOT_CHECK(f.plot.hot_handles().cursor == plot::kInvalid);
|
||||
|
||||
// And a region bound: the zone that is published after the gesture is the zone the
|
||||
// geometry now gives, not the one the bound had before the zoom.
|
||||
const f32 bound = bound_x(f.plot, rid, true);
|
||||
hover(f.controller, bound);
|
||||
PLOT_CHECK(f.plot.hot_handles().region == rid);
|
||||
PLOT_CHECK(f.plot.hot_handles().zone == plot::RegionGrab::Left);
|
||||
f.controller.on_key({plot::input::kKeyZoomIn, 0u, true});
|
||||
PLOT_CHECK(std::fabs(bound_x(f.plot, rid, true) - bound) > radius); // the bound moved away
|
||||
PLOT_CHECK(f.plot.hot_handles().zone == f.controller.region_grab(rid));
|
||||
PLOT_CHECK(f.plot.hot_handles().zone != plot::RegionGrab::Left);
|
||||
}
|
||||
@@ -1641,3 +1641,66 @@ PLOT_TEST(removing_a_region_shifts_the_ids_and_keeps_the_statistics) {
|
||||
PLOT_CHECK(!p.region_stats(2u, tr).valid);
|
||||
PLOT_CHECK(!p.region_stats(9u, tr).valid);
|
||||
}
|
||||
|
||||
/// A bound that is not a number is not a bound. `std::min`/`std::max` propagate a NaN
|
||||
/// through *both* ends of the pair, so accepting one would not merely invert the interval:
|
||||
/// it would replace the finite partner too, and a `Free` region stored that way could never
|
||||
/// be dragged back -- every motion re-derives both bounds from the stored ones. The other
|
||||
/// bound setters in the file refuse a non-finite value outright, and these two write paths
|
||||
/// have to as well: `set_region` leaves the region on the interval it already held, and
|
||||
/// `add_region`, which does not route through it, adds nothing at all.
|
||||
PLOT_TEST(non_finite_region_bounds_are_refused) {
|
||||
plot::data::ArraySeries s(ramp(100), 0.0, 1.0);
|
||||
plot::Plot p;
|
||||
p.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
|
||||
const u32 ax = p.add_x_axis({"t", {0.0, 99.0}, false, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {0.0, 99.0}, false, plot::AxisPlace::Left, false});
|
||||
const u32 tr = p.add_trace({&s, plot::Color::hex(0xFFFFFF), 1.0f, ax, ay});
|
||||
|
||||
plot::Region reg;
|
||||
reg.x0 = 10.0;
|
||||
reg.x1 = 20.0;
|
||||
reg.snap = plot::RegionSnap::Free;
|
||||
const u32 rid = p.add_region(reg);
|
||||
const f64 nan = std::numeric_limits<f64>::quiet_NaN();
|
||||
const f64 inf = std::numeric_limits<f64>::infinity();
|
||||
|
||||
// Each of the four ways a pair can be unusable, and the interval is the same one after
|
||||
// every one of them -- both bounds, not just the bound that arrived bad.
|
||||
p.set_region(rid, nan, 20.0);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x0, 10.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x1, 20.0, 1e-12);
|
||||
p.set_region(rid, 10.0, nan);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x0, 10.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x1, 20.0, 1e-12);
|
||||
p.set_region(rid, 30.0, inf);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x0, 10.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x1, 20.0, 1e-12);
|
||||
p.set_region(rid, -inf, 5.0);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x0, 10.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.region(rid).x1, 20.0, 1e-12);
|
||||
// The interval it still holds is the one its statistics are over.
|
||||
PLOT_CHECK(p.region_stats(rid, tr).valid);
|
||||
PLOT_CHECK_NEAR(p.region_stats(rid, tr).min, 10.0, 1e-9);
|
||||
PLOT_CHECK_NEAR(p.region_stats(rid, tr).max, 20.0, 1e-9);
|
||||
|
||||
// A region whose bounds are not numbers is refused rather than stored: the count does not
|
||||
// move, no id is handed out, and the id that came back names nothing.
|
||||
const u32 before = p.region_count();
|
||||
plot::Region bad = reg;
|
||||
bad.x0 = nan; // the finite partner goes with it, which is why the pair is refused
|
||||
const u32 refused = p.add_region(bad);
|
||||
PLOT_CHECK(p.region_count() == before);
|
||||
PLOT_CHECK(refused != before);
|
||||
PLOT_CHECK_NEAR(p.region(refused).x0, 0.0, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.region(refused).x1, 0.0, 1e-12);
|
||||
PLOT_CHECK(!p.region_stats(refused, tr).valid);
|
||||
// The next region a caller adds still takes the next free id, so a refusal leaves the plot
|
||||
// exactly as it was rather than a hole a later id would fall into.
|
||||
plot::Region good = reg;
|
||||
good.x0 = 30.0;
|
||||
good.x1 = 40.0;
|
||||
PLOT_CHECK(p.add_region(good) == before);
|
||||
PLOT_CHECK(p.region_count() == before + 1u);
|
||||
PLOT_CHECK_NEAR(p.region(before).x0, 30.0, 1e-12);
|
||||
}
|
||||
Reference in new issue
Block a user