feat(plot): horizontal rulers alongside the vertical cursors

This commit is contained in:
Martino Ferrari committed 2026-09-28 17:18:43 +02:00
1 parent 2d819c4228
commit 63f9655ae1
11 files changed
+514 -29

No files matched your search

+100
View File
@@ -759,6 +759,14 @@ f32 column_of(const plot::Plot& p, u32 axis, f64 data) {
return static_cast<f32>(p.x_transform(axis).to_screen_x(data));
}
/// The y twins of `data_at()`/`column_of()`: what a horizontal ruler is placed by and hit in.
f64 data_at_row(const plot::Plot& p, u32 axis, f32 row) {
return p.y_transform(axis).to_data_y(static_cast<f64>(row));
}
f32 row_of(const plot::Plot& p, u32 axis, f64 data) {
return static_cast<f32>(p.y_transform(axis).to_screen_y(data));
}
void hover(plot::input::Controller& c, f32 px, f32 py = 200.0f, u32 mods = 0) {
c.on_pointer({px, py, 0.0f, 0.0f, plot::input::Button::Left, mods, false, false, false});
}
@@ -823,6 +831,98 @@ PLOT_TEST(cursor_is_grabbed_without_a_modifier) {
release(f.controller, moved_to - 40.0f, 200.0f, kCtrl);
}
/// Spec §14 on the real pointer path: a horizontal ruler is grabbed by the same hit test as a
/// marker -- no modifier, the same radius -- and the drag writes the coordinate its own
/// orientation owns. The ruler's y follows the pointer's row; its x stays the dead field it is;
/// a vertical cursor elsewhere on the plot is not disturbed; and the view does not pan.
PLOT_TEST(horizontal_ruler_is_grabbed_and_dragged_along_y) {
Fixture f;
f.ay = f.plot.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
const u32 marker = f.plot.add_cursor(50.0, plot::Color::hex(0x00FF00));
const u32 ruler = f.plot.add_h_ruler(40.0, plot::Color::hex(0x35D0A8));
f.plot.layout();
const f32 column = screen_x_of(f.plot, f.ax, 70.0); // clear of the marker's own column
const f32 row = row_of(f.plot, f.ay, 40.0);
// Hover first: what the renderer highlights and what a press grabs are one answer.
hover(f.controller, column, row);
PLOT_CHECK(f.controller.hovered_cursor() == ruler);
PLOT_CHECK(f.plot.hot_handles().cursor == ruler);
press(f.controller, column, row);
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveCursor);
PLOT_CHECK(f.controller.grabbed_cursor() == ruler);
PLOT_CHECK(f.plot.hot_handles().grabbed_cursor == ruler);
// Up is more y: the ruler follows the pointer's row through its own y axis.
move_to(f.controller, column, row - 60.0f);
PLOT_CHECK_NEAR(f.plot.cursor(ruler).y, data_at_row(f.plot, f.ay, row - 60.0f), 1e-9);
PLOT_CHECK(f.plot.cursor(ruler).y > 40.0);
// The coordinate the ruler does not own, and the marker that owns it, are both untouched.
PLOT_CHECK_NEAR(f.plot.cursor(ruler).x, 0.0, 1e-12);
PLOT_CHECK_NEAR(f.plot.cursor(marker).x, 50.0, 1e-12);
// The gesture belonged to the ruler, not to the view: no range moved.
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).min, 0.0, 1e-9);
PLOT_CHECK_NEAR(f.plot.x_range(f.ax).span(), 100.0, 1e-9);
PLOT_CHECK_NEAR(f.plot.y_range(f.ay).min, 0.0, 1e-9);
release(f.controller, column, row - 60.0f);
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::None);
PLOT_CHECK(f.controller.grabbed_cursor() == plot::kInvalid);
PLOT_CHECK(f.plot.hot_handles().grabbed_cursor == plot::kInvalid);
// And it stays where the drag left it.
PLOT_CHECK(f.plot.cursor(ruler).y > 40.0);
}
/// 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
/// other, both inside the 5 logical px grab radius. What the winner's drag writes is its own
/// coordinate and nothing else.
PLOT_TEST(vertical_and_horizontal_cursors_do_not_confuse_each_other) {
Fixture f;
f.ay = f.plot.add_y_axis({"v", {0.0, 100.0}, false, plot::AxisPlace::Left, false});
const u32 marker = f.plot.add_cursor(50.0, plot::Color::hex(0x00FF00));
const u32 ruler = f.plot.add_h_ruler(40.0, plot::Color::hex(0x35D0A8));
f.plot.layout();
const f32 marker_x = screen_x_of(f.plot, f.ax, 50.0);
const f32 ruler_y = row_of(f.plot, f.ay, 40.0);
const f32 near = 1.0f;
const f32 far = 4.0f;
// Nearer the marker: the point is on both, and the marker is what hovers and what a press
// grabs -- the ruler's y does not move with the drag that follows.
const f32 marker_side_x = marker_x + near;
const f32 ruler_side_y = ruler_y + far;
hover(f.controller, marker_side_x, ruler_side_y);
PLOT_CHECK(f.controller.hovered_cursor() == marker);
press(f.controller, marker_side_x, ruler_side_y);
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveCursor);
PLOT_CHECK(f.controller.grabbed_cursor() == marker);
move_to(f.controller, marker_side_x + 40.0f, ruler_side_y);
PLOT_CHECK_NEAR(f.plot.cursor(marker).x, data_at(f.plot, f.ax, marker_side_x + 40.0f), 1e-9);
PLOT_CHECK_NEAR(f.plot.cursor(ruler).y, 40.0, 1e-12);
PLOT_CHECK_NEAR(f.plot.cursor(ruler).x, 0.0, 1e-12);
release(f.controller, marker_side_x + 40.0f, ruler_side_y);
// The symmetric case: the marker put back under the pointer, and the press 1 px from the ruler
// and 4 px from the marker. The ruler takes it, and its y alone moves.
f.plot.set_cursor_x(marker, data_at(f.plot, f.ax, marker_x));
const f32 near_ruler_x = marker_x + far;
const f32 near_ruler_y = ruler_y - near;
hover(f.controller, near_ruler_x, near_ruler_y);
PLOT_CHECK(f.controller.hovered_cursor() == ruler);
press(f.controller, near_ruler_x, near_ruler_y);
PLOT_CHECK(f.controller.drag_mode() == plot::input::DragMode::MoveCursor);
PLOT_CHECK(f.controller.grabbed_cursor() == ruler);
const f64 marker_before = f.plot.cursor(marker).x;
move_to(f.controller, near_ruler_x, near_ruler_y + 30.0f);
PLOT_CHECK_NEAR(f.plot.cursor(ruler).y, data_at_row(f.plot, f.ay, near_ruler_y + 30.0f), 1e-9);
PLOT_CHECK_NEAR(f.plot.cursor(marker).x, marker_before, 1e-12);
release(f.controller, near_ruler_x, near_ruler_y + 30.0f);
PLOT_CHECK(f.plot.cursor(ruler).y < 40.0);
}
/// The other half of "a superset of the old gesture": a press that is on no handle
/// reaches the pan exactly as it did before, with and without ctrl, and neither the
/// cursor nor the region under the view is disturbed.