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

+51 -10
View File
@@ -920,7 +920,9 @@ void Renderer::draw_traces(const Plot& plot, const PlotLayout& layout) {
}
void Renderer::draw_cursors(const Plot& plot, const PlotLayout& layout) {
const Range x_range = plot.x_range(0);
const Range y_range = plot.y_range(0);
const bool log_x = plot.x_axis(0).log;
const bool log_y = plot.y_axis(0).log;
// The handles the input layer last found under the pointer: a marker hovered or
// being dragged is drawn in the style's cursor colour and as a *tab*, so the cue
@@ -933,20 +935,59 @@ void Renderer::draw_cursors(const Plot& plot, const PlotLayout& layout) {
if (!cursor.visible) continue;
const u64 id = static_cast<u64>(i);
const bool on = id == hot.cursor || id == hot.grabbed_cursor;
const Transform transform = plot.x_transform(cursor.axis_x);
const f32 px = static_cast<f32>(transform.to_screen_x(cursor.x));
// A cursor scrolled out of the visible x range is off the plot: drawing it
// would put a marker over the label strip.
if (px < layout.axes.x || px >= layout.axes.right()) continue;
// The two orientations differ in exactly two things: which coordinate places the
// stroke (and which axis maps the span it is drawn along), and where the label is
// written. Everything else -- the skip at the far edge of the axes, the highlight, the
// text path -- is this one loop's, which is the point of one type rather than two.
const bool horizontal = cursor.orientation == CursorOrientation::Horizontal;
const f32 at = plot.cursor_screen_coordinate(i);
// A cursor at the *far* edge of its own range is off the plot rather than on it: its
// stroke straddles that edge by construction, so drawing it would put half a marker
// over the label strip beside it. The rule reads the same on both axes -- a marker
// scrolled past the right of its x range, a ruler dragged to the bottom of its y range
// -- and the axes clip would hold the ink in anyway, so this is about not drawing
// something that is a half-stroke and a half-nothing.
if (horizontal ? (at < layout.axes.y || at >= layout.axes.bottom())
: (at < layout.axes.x || at >= layout.axes.right())) {
continue;
}
stage_segment(cursor.x, y_range.min, cursor.x, y_range.max, plot.x_axis(cursor.axis_x).log,
log_y);
flush_lines(mapping(plot, cursor.axis_x, 0),
if (horizontal) {
stage_segment(x_range.min, cursor.y, x_range.max, cursor.y, log_x,
plot.y_axis(cursor.axis_y).log);
} else {
stage_segment(cursor.x, y_range.min, cursor.x, y_range.max, plot.x_axis(cursor.axis_x).log,
log_y);
}
flush_lines(horizontal ? mapping(plot, 0, cursor.axis_y) : mapping(plot, cursor.axis_x, 0),
on ? style_.cursor : cursor.color,
style_.device(on ? style_.handle_width_px : style_.line_width_px));
if (!cursor.label.empty()) {
stage_text({px + style_.device(3.0f), layout.axes.y + style_.device(2.0f)}, cursor.label.c_str());
draw_cursor_label(plot, layout, cursor, at);
}
}
void Renderer::draw_cursor_label(const Plot& plot, const PlotLayout& layout, const Cursor& cursor,
f32 at) {
const f32 gap = style_.device(3.0f);
if (cursor.orientation == CursorOrientation::Horizontal) {
// A ruler's text is the value it is *for*, written the way the y axis writes its own
// tick labels, so a value read off a ruler reads exactly like the tick beside it. The
// caller's caption, when there is one, is staged first on the same line, so neither the
// caption nor the value has to be dropped -- and both stay one text run's worth of
// glyphs, since the pen advances per glyph either way.
const Vec2 pen{layout.axes.x + gap, at + style_.device(2.0f)};
char value[32];
plot.y_axis(cursor.axis_y).format_value(cursor.y, value, sizeof value);
if (cursor.label.empty()) {
stage_text(pen, value);
return;
}
stage_text(pen, cursor.label.c_str());
stage_text({pen.x + font_backend_.measure(cursor.label.c_str()) + gap, pen.y}, value);
return;
}
if (!cursor.label.empty()) {
stage_text({at + gap, layout.axes.y + style_.device(2.0f)}, cursor.label.c_str());
}
}