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

+27
View File
@@ -144,6 +144,13 @@ struct PlotLayout {
/// clamped against the rect it was measured for, so nothing of the readout can be drawn
/// over a label strip or into a figure's neighbouring row.
CrosshairLayout crosshair;
/// The box-zoom frame, when a modified drag has one open: the area the gesture is framing, in
/// viewport pixels, already widened along whichever axis the modifier will *not* zoom (see
/// `input::Controller::selection_preview()`, which is where that is decided). Here rather than
/// derived by the renderer, for the regions' reason -- the frame draws exactly these numbers -- and
/// left as the zero rect whenever no gesture is open, which is what both the fill and the four
/// border segments read as "draw nothing".
Rect selection;
};
/// What a pointer is on, as of the last pointer event: the cursor marker under it, the
@@ -753,6 +760,21 @@ class Plot {
/// calls this when the pointer leaves the axes.
void clear_crosshair() noexcept { crosshair_.active = false; }
/// The box-zoom frame the host is showing, in **viewport pixels**, or the zero rect for none.
///
/// Set by the host from `input::Controller::selection_preview()` while a modified left drag is
/// choosing a range, and taken off when it ends. It is the *host's* job and not the plot's because
/// the plot has no controller and learns nothing about pointer, button or modifier (see
/// `plot::input`): this is the receiving end of that boundary, exactly as `set_hot_handles()` is.
/// Drawn as a frame -- a slightly opaque fill under a light border -- over everything else inside
/// the axes, so the area being framed stays legible against the traces it covers.
void set_selection_rect(Rect rect) noexcept { selection_ = rect; }
/// Takes the frame off, leaving the same state as before any drag: the next one starts from nothing.
void clear_selection_rect() noexcept { selection_ = Rect{}; }
/// The frame as set -- what `layout()` measures into `PlotLayout::selection`, and what a host or a
/// test reads back without paying a layout.
Rect selection_rect() const noexcept { return selection_; }
/// Switches the readout panel off -- or back on -- without moving the crosshair: the lines and
/// the per-trace markers stay, the panel is not measured and no row is listed. A host that wants
/// the pointer's column marked but not a panel over its data sets this once; it is deliberately
@@ -969,6 +991,11 @@ class Plot {
/// The crosshair's state (see Crosshair): at most one per plot, placed by
/// `set_crosshair()` and read back by `crosshair()` and `layout()`.
Crosshair crosshair_{};
/// The host's box-zoom frame (`set_selection_rect()`): viewport pixels, the zero rect when no
/// modified drag is choosing a range. State rather than a `PlotLayout` field alone because a host
/// sets it on every drag motion and reads it back; `layout()` only carries it through.
Rect selection_{};
/// The samples `crosshair_value()` reads a span through: `Series::gather()` appends
/// `(x, y)` pairs and never clears, so the lookup clears this and refills it with the at
/// most two samples a span covers. Sized to hold them from construction -- four floats is