feat(plot): crosshair with an interpolated per-signal hover readout

This commit is contained in:
Martino Ferrari committed 2026-09-28 18:57:33 +02:00
1 parent a153b73ead
commit fd0c471430
11 files changed
+809 -22

No files matched your search

+35 -1
View File
@@ -113,6 +113,10 @@ void Controller::set_viewport(Rect px) {
// transforms read.
plot_->set_viewport(px);
plot_->layout();
// A resize moves the axes (and with them the data under a stationary pointer), so the
// crosshair is re-placed against the new rect -- and cleared if the pointer is now outside
// it, which is the same rule as a pointer that left.
if (has_pointer_) update_crosshair(pointer_x_, pointer_y_);
// The hover the layout is part of: a handle under the pointer may be somewhere
// else now, and the highlight the renderer draws reads what this publishes.
publish_hot();
@@ -132,6 +136,11 @@ void Controller::on_pointer(const PointerEvent& raw) {
} else if (e.down && mode_ != DragMode::None) {
update_drag(e);
}
// The crosshair is placed (or cleared) from the pointer's own position, after the gesture
// above: a press does not clear it -- the pointer is still over the axes, still pointing at
// whatever it just grabbed -- and a drag keeps it re-anchored to the pointer as the data
// moves under it.
update_crosshair(e.x, e.y);
// Every exit above ends here: a hover that moved, a gesture that opened, and a
// gesture that closed all change what is under the pointer, and the publish is
// what makes the renderer's highlight (and a host's resize cursor) follow.
@@ -174,6 +183,11 @@ void Controller::on_wheel(const WheelEvent& raw) {
// emphasizes the handle the pointer is over *now*, rather than the one it was over before,
// without waiting for the mouse to move again.
plot_->layout();
// The data under the pointer moved with the ranges, so the crosshair -- which is a data
// coordinate -- is re-anchored to the pointer rather than left on the value it was placed
// at: it follows the pointer, and this is the one thing that moves the data without moving
// the pointer.
if (has_pointer_) update_crosshair(pointer_x_, pointer_y_);
publish_hot();
}
@@ -206,11 +220,31 @@ void Controller::on_key(const KeyEvent& e) {
// A key stroke that moved a range moved the geometry under a stationary pointer, exactly
// as a wheel notch does: refresh the layout and republish what is hot, so the highlight a
// frame draws is the handle the pointer is over now (see on_wheel). A key this controller
// has no gesture for changes nothing and returns above.
// has no gesture for changes nothing and returns above -- and so does not touch the
// crosshair either.
plot_->layout();
if (has_pointer_) update_crosshair(pointer_x_, pointer_y_);
publish_hot();
}
void Controller::update_crosshair(f32 px, f32 py) {
// Inside the data area the crosshair is at the coordinates under the pointer; outside it
// there is no data to read, so there is no crosshair -- the null answer, rather than a
// value clamped to the nearest edge of the window.
const PlotLayout& layout = std::as_const(*plot_).layout();
if (!layout.axes.contains(px, py)) {
plot_->clear_crosshair();
return;
}
// Both coordinates, on the primary axis pair (the same pair the grid and a region's fill
// are placed through): a pointer over the axes knows its x and its y, so a single plot's
// crosshair is the full cross. A `Figure` is driven by `Figure::set_crosshair()` instead,
// which applies one x to every row and draws no horizontal line -- its rows share x and
// not y.
plot_->set_crosshair(plot_->x_transform(0u).to_data_x(static_cast<f64>(px)),
plot_->y_transform(0u).to_data_y(static_cast<f64>(py)), true);
}
u64 Controller::hovered_cursor() const {
if (!has_pointer_) return kInvalid;
return cursor_at(pointer_x_, pointer_y_);