test(plot): pin the crosshair's lookup, readout, placement, sync and clip

This commit is contained in:
Martino Ferrari committed 2026-09-28 19:07:22 +02:00
1 parent fd0c471430
commit 8e27e22ebe
6 files changed
+798 -17

No files matched your search

+85
View File
@@ -1640,3 +1640,88 @@ PLOT_TEST(a_figures_live_edge_hint_is_published_only_while_the_group_follows) {
PLOT_CHECK(solo.x_follow(0u) == plot::XFollow::Live);
PLOT_CHECK(!solo.x_live_owner(0u, edge));
}
// ------------------------------------------------- the crosshair (spec §15)
/// Spec §15, the wiring: a pointer over the data area places the crosshair at the data
/// coordinates under it, and a pointer that leaves the axes clears it. A press neither clears it
/// nor moves it away from the pointer -- it follows the pointer, and a pointer still over the
/// axes is still pointing at what it grabbed -- while a wheel, which moves the data without
/// moving the pointer, re-anchors it to the same pointer position rather than leaving it on the
/// value it was placed at.
PLOT_TEST(crosshair_tracks_motion_and_clears_on_leave) {
Fixture fx;
fx.plot.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
fx.plot.layout();
const plot::Rect axes = fx.plot.layout().axes;
const f32 cx = axes.x + 0.37f * axes.w;
const f32 cy = axes.y + 0.61f * axes.h;
// No pointer has been seen, so there is no crosshair: it is a hover, and a hover needs a
// pointer.
PLOT_CHECK(!fx.plot.crosshair().active);
// A motion inside the data area places it at the coordinates under the pointer, through the
// same transforms every gesture uses -- so the line, the value and the pointer cannot drift.
fx.controller.on_pointer({cx, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, false,
/*down=*/false, false});
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(cx), 1e-9);
PLOT_CHECK_NEAR(fx.plot.crosshair().y, fx.plot.y_transform(0u).to_data_y(cy), 1e-9);
// Both lines: a pointer over the axes knows its x and its y.
PLOT_CHECK(fx.plot.crosshair().vertical);
PLOT_CHECK(fx.plot.crosshair().horizontal);
// Moving re-anchors it, and a press keeps it: the pointer did not leave the axes.
const f32 moved_x = axes.x + 0.52f * axes.w;
fx.controller.on_pointer({moved_x, cy, moved_x - cx, 0.0f, plot::input::Button::Left, 0u, true,
true, false});
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(moved_x), 1e-9);
fx.controller.on_pointer({moved_x, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false,
true});
PLOT_CHECK(fx.plot.crosshair().active);
// A pointer that leaves the data area clears it -- into the y label strip, where no data is
// under the pointer at all.
fx.controller.on_pointer({axes.x - 4.0f, cy, -4.0f, 0.0f, plot::input::Button::Left, 0u, false,
false, false});
PLOT_CHECK(fx.plot.layout().y_label_area.contains(axes.x - 4.0f, cy)); // the premise
PLOT_CHECK(!fx.plot.crosshair().active);
// ... and back inside re-places it, with the coordinate of the new position.
fx.controller.on_pointer({cx, cy, 0.0f, 0.0f, plot::input::Button::Left, 0u, false, false,
false});
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(cx), 1e-9);
// A wheel moved the data under a stationary pointer, so the crosshair is re-anchored: the
// value it reports is the one under the pointer *now*, not the one it was placed at.
const f64 before = fx.plot.crosshair().x;
fx.controller.on_wheel({cx, cy, 0.0f, 1.0f, 0u});
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(cx), 1e-9);
PLOT_CHECK(fx.plot.crosshair().x != before);
// The same for an axis zoom from the keyboard: the ranges move, the pointer does not.
const f64 after_wheel = fx.plot.crosshair().x;
fx.controller.on_key({plot::input::kKeyZoomOut, 0u, true});
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(cx), 1e-9);
PLOT_CHECK(fx.plot.crosshair().x != after_wheel);
// A resize moves the axes under a stationary pointer, so the crosshair is re-placed against
// the new rect -- and a resize that leaves the pointer outside the new axes clears it, the
// same rule as a pointer that left.
const f32 far_x = axes.right() - 2.0f;
fx.controller.on_pointer({far_x, cy, far_x - cx, 0.0f, plot::input::Button::Left, 0u, false,
false, false});
PLOT_CHECK(fx.plot.crosshair().active);
fx.controller.set_viewport({0.0f, 0.0f, 800.0f, 120.0f}); // the pointer is now below it
PLOT_CHECK(!fx.plot.layout().axes.contains(far_x, cy)); // the premise: the pointer is out
PLOT_CHECK(!fx.plot.crosshair().active);
// Restoring the rect puts the pointer back over the data, and the crosshair with it.
fx.controller.set_viewport({0.0f, 0.0f, 800.0f, 400.0f});
PLOT_CHECK(fx.plot.layout().axes.contains(far_x, cy));
PLOT_CHECK(fx.plot.crosshair().active);
PLOT_CHECK_NEAR(fx.plot.crosshair().x, fx.plot.x_transform(0u).to_data_x(far_x), 1e-9);
}