feat: an open box zoom draws the frame the release will apply

The controller has always tracked its selection; nothing drew it, so a Ctrl or
Shift drag framed a range with no feedback until the release.

The preview is the area the release will change, not what the pointer swept:
Ctrl frames the x range only, so a band across the plot is what it will zoom,
where the sliver a straight horizontal drag sweeps is only the gesture. The
plot carries the rect a host hands it -- it learns nothing about pointers --
and the renderer draws a low-alpha fill under a light border over everything
else inside the axes.
This commit is contained in:
Martino Ferrari committed 2026-10-11 11:07:55 +02:00
1 parent 1fc97d818f
commit be1f31a04f
14 files changed
+276 -1

No files matched your search

+25
View File
@@ -751,6 +751,31 @@ void Controller::pan(const PointerEvent& e) {
}
}
Rect Controller::selection_preview() const noexcept {
// Nothing to preview off a box zoom: the mask bits outlive the gesture -- `end_drag()` clears the
// selection, not them -- so widening without this guard would hand out a full-band rect for a drag
// that finished minutes ago.
if (mode_ != DragMode::BoxZoom) return Rect{};
// And nothing has been framed until the drag has swept every axis the release will apply: at the
// press instant the selection is an empty rect at the pointer, and the *widened* form of that is a
// band the width of the plot, which is a claim about a range that does not exist yet. Ctrl's other
// axis is deliberately not required: a drag with no vertical movement at all is a band across the
// plot, which is exactly what Ctrl is for.
if ((box_zoom_x_ && !(selection_.w > 0.0f)) || (box_zoom_y_ && !(selection_.h > 0.0f))) return Rect{};
Rect preview = selection_;
if (box_zoom_x_ && box_zoom_y_) return preview;
const Rect& axes = plot_->layout().axes;
if (!box_zoom_x_) {
preview.x = axes.x;
preview.w = axes.w;
}
if (!box_zoom_y_) {
preview.y = axes.y;
preview.h = axes.h;
}
return preview;
}
void Controller::box_zoom_update(const PointerEvent& e) {
const f32 x = std::min(origin_x_, e.x);
const f32 y = std::min(origin_y_, e.y);
+4
View File
@@ -1101,6 +1101,10 @@ const PlotLayout& Plot::layout() {
// its rows are the sample values nearest a data x -- so it wants the transforms the
// settled layout pins, exactly as the legend and the regions do.
build_crosshair();
// The box-zoom frame is not measured, only carried: the host already has it in viewport pixels from
// the controller that produced it, and re-deriving it here would be a second rounding of the same
// rectangle -- and a second place for it to disagree with what the gesture is doing.
layout_.selection = selection_;
return layout_;
}
+24
View File
@@ -545,6 +545,9 @@ void Renderer::draw_plot(Plot& plot, const PlotLayout& layout) {
// panel is drawn over the legend and every other overlay rather than under them, and its
// text joins the frame's over flush with the tick labels and titles.
draw_crosshair_readout(plot, layout);
// The selection frame last of all: it is the gesture still in progress, and nothing inside the axes
// should be able to sit on top of the area being chosen.
draw_selection_frame(layout);
clear_clip();
draw_tick_labels(plot, layout);
draw_titles(plot, layout);
@@ -1960,6 +1963,27 @@ void Renderer::stage_marker(plot::Marker marker, Vec2 centre_px, f32 size_px) {
}
}
void Renderer::draw_selection_frame(const PlotLayout& layout) {
const Rect& frame = layout.selection;
// Two guards, not one: a drag swept flat in one direction has a real extent on one axis and none on
// the other, and a border on that is still the gesture's own answer -- the fill is what a zero
// extent has nothing of.
if (!(frame.w > 0.0f) && !(frame.h > 0.0f)) return;
if (frame.w > 0.0f && frame.h > 0.0f) fill_pixels(frame, style_.selection_fill);
// The border frames the pixels the fill covers and not the measured rect's own fractional edges:
// the readout panel's rounding and the readout panel's reason, since a hairline straddling a
// fractional edge is a frame bright on two sides and dim on the others.
const f32 left = std::ceil(frame.x - 0.5f) + 0.5f;
const f32 right = std::ceil(frame.right() - 0.5f) - 0.5f;
const f32 top = std::ceil(frame.y - 0.5f) + 0.5f;
const f32 bottom = std::ceil(frame.bottom() - 0.5f) - 0.5f;
lines_.add_segment({left, top}, {right, top});
lines_.add_segment({left, bottom}, {right, bottom});
lines_.add_segment({left, top}, {left, bottom});
lines_.add_segment({right, top}, {right, bottom});
flush_lines(pixel_mapping(), style_.selection_edge, style_.device(1.0f));
}
void Renderer::draw_readout_panel(const Rect& panel, Color border) {
if (!(panel.w > 0.0f) || !(panel.h > 0.0f)) return;
// The frame's own background at a partial alpha, which dims whatever it covers instead of erasing