feat(plot): the figure crosshair marks the hovered row, and the stroke is thin, dashed and configurable

This commit is contained in:
Martino Ferrari committed 2026-09-28 20:17:01 +02:00
1 parent cae672bc99
commit eee7c34dd5
12 files changed
+166 -12

No files matched your search

+16
View File
@@ -449,6 +449,22 @@ void Figure::set_crosshair(f64 x, bool active) {
for (Plot& row : plots_) row.set_crosshair(x, active);
}
void Figure::set_crosshair(f64 x, u32 hovered_row, f64 y_in_row, bool active) {
// The shared x first, on every row -- the same state the single-coordinate overload leaves,
// so the vertical lines and every row's readout are placed by one rule whether or not the
// pointer's row is known. A bad x is refused by the row call, which changes nothing, so the
// y below must not run either or the hover would be half-placed.
if (active && !std::isfinite(x)) return;
for (Plot& row : plots_) row.set_crosshair(x, active);
// Then the pointer row's own y: the *only* row that gets a horizontal line, because the y is
// a value on that row's scale and no other row's. A row that does not exist (the pointer is in
// the gap between rows) leaves every row vertical-only, which is the honest answer -- there is
// no row under the pointer to mark -- and a non-finite y leaves the row vertical-only rather
// than clearing a hover the pointer still has.
if (!active || hovered_row >= plots_.size()) return;
plots_[hovered_row].set_crosshair(x, y_in_row, true);
}
void Figure::clear_crosshair() {
for (Plot& row : plots_) row.clear_crosshair();
}
+14 -1
View File
@@ -1265,6 +1265,8 @@ void Plot::build_crosshair() {
crosshair.active = crosshair_.active;
crosshair.vertical = Rect{};
crosshair.horizontal = Rect{};
crosshair.stroke = 0.0f;
crosshair.dash = Vec2{};
crosshair.panel = Rect{};
if (!crosshair_.active) {
crosshair.rows.clear();
@@ -1286,7 +1288,18 @@ void Plot::build_crosshair() {
// rule: the box is where the line may be drawn, and Task 1's clip never has to trim one
// that already fits. The lines are placed through the primary axis pair, like the grid and
// a region's fill -- one crosshair is one column and one row of the data area.
const f32 stroke = style_.device(style_.line_width_px);
//
// The stroke and the dash are resolved *here*, once, from the crosshair's own override where
// it has one and the global style otherwise, and converted to device pixels through
// `device()`. The boxes below and the renderer's strokes are then one set of numbers rather
// than two that must be kept in step (spec §17): `stroke` is the device width, `dash` the
// device (on, off) pair with `off <= 0` meaning solid.
const CrosshairStyle& cs = crosshair_.style;
const f32 stroke = style_.device(cs.width >= 0.0f ? cs.width : style_.crosshair_width);
crosshair.stroke = stroke;
crosshair.dash =
Vec2{style_.device(cs.dash_on >= 0.0f ? cs.dash_on : style_.crosshair_dash_on),
style_.device(cs.dash_off >= 0.0f ? cs.dash_off : style_.crosshair_dash_off)};
const f32 half = 0.5f * stroke;
if (crosshair_.vertical) {
const f32 at =