diff --git a/tests/test_controller.cpp b/tests/test_controller.cpp index 2aa6002..aae07ce 100644 --- a/tests/test_controller.cpp +++ b/tests/test_controller.cpp @@ -366,7 +366,7 @@ PLOT_TEST(key_f_auto_fits_traces) { /// The grab radius is a distance the user aims with, so it must be the same /// *physical* distance at any DPI: the controller compares device-pixel coordinates /// against `logical * Plot::ui_scale()`. The press positions below are the same -/// logical distance (0.9 and 1.1 of the 5 px radius) from the cursor at both scales, +/// logical distance (0.9 and 1.1 of the 7 px radius) from the cursor at both scales, /// so the same press grabs -- and the same press just outside does not. PLOT_TEST(grab_radius_scales_with_dpi) { for (const f32 scale : {1.0f, 2.0f}) { @@ -1053,6 +1053,52 @@ PLOT_TEST(horizontal_ruler_follows_the_pointer_on_an_inverted_y_range) { PLOT_CHECK(f.plot.cursor(ruler).y > 50.0); } +/// The horizontal half of `cursor_snaps_when_the_drag_ends`: an `HRulerSnap::NearestTick` ruler +/// dragged through the controller writes the free pointer row for the whole gesture and lands on +/// a y tick only on release -- the snap-on-drop a marker now uses, which a ruler did *not* before +/// (it snapped continuously inside `set_cursor_y`). This is the case that catches a regression +/// to that continuous behaviour: mid-drag the stored y is on no tick and equals the pointer's row, +/// and only the drop moves it to the grid. +PLOT_TEST(a_dragged_nearest_tick_ruler_snaps_only_when_the_drag_ends) { + Fixture f; + f.ay = f.plot.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false}); + const u32 ruler = f.plot.add_h_ruler(40.0, plot::Color::hex(0x35D0A8)); + f.plot.set_cursor_h_snap(ruler, plot::HRulerSnap::NearestTick); + f.plot.layout(); + + const std::vector& ticks = f.plot.y_axis(f.ay).ticks; + PLOT_CHECK(ticks.size() >= 3u); + // A row between two adjacent ticks, nearer the lower-valued one (the larger screen row). + const f64 lo = ticks[1].value; + const f64 hi = ticks[2].value; + PLOT_CHECK(lo < hi); + const f64 between = lo + 0.4 * (hi - lo); // equal to no tick, nearer `lo` + const auto is_tick = [&ticks](f64 y) { + for (const plot::Tick& t : ticks) { + if (std::fabs(t.value - y) < 1e-9) return true; + } + return false; + }; + PLOT_CHECK(!is_tick(between)); + + const f32 column = screen_x_of(f.plot, f.ax, 70.0); + const f32 to = static_cast(row_of(f.plot, f.ay, between)); + const f32 from = static_cast(row_of(f.plot, f.ay, f.plot.cursor(ruler).y)); + + press(f.controller, column, from); + PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveCursor); + PLOT_CHECK(f.controller.grabbed_cursor() == ruler); + move_to(f.controller, column, to); + // Mid-drag the stored y is the free pointer row and on no tick: the snap has not run. + PLOT_CHECK_NEAR(f.plot.cursor(ruler).y, data_at_row(f.plot, f.ay, to), 1e-9); + PLOT_CHECK(!is_tick(f.plot.cursor(ruler).y)); + + // The drop commits the snap: the stored y is the nearest tick and it was not before. + release(f.controller, column, to); + PLOT_CHECK_NEAR(f.plot.cursor(ruler).y, lo, 1e-9); + PLOT_CHECK(is_tick(f.plot.cursor(ruler).y)); +} + /// Nearest-first resolution *across* orientations: a pointer within reach of both a marker and a /// ruler takes the nearer of the two, so neither widens the other's target -- which is what lets /// one hit test serve two instruments. Each press below is 1 px from its winner and 4 px from the