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

+18
View File
@@ -344,6 +344,21 @@ u32 PlotArea::pointer_row() const noexcept {
return row_in(row_rects_, pointer_.y);
}
void PlotArea::sync_selection_frames() noexcept {
const auto frame_for = [](input::Controller& c) -> Rect {
return c.drag_mode() == input::DragMode::BoxZoom ? c.selection_preview() : Rect{};
};
if (figure_ != nullptr) {
for (u32 i = 0; i < row_controllers_.size(); ++i) {
// An empty rect on the rows that are not choosing: `Rect` has no equality to compare
// with, and a store of the same value is what keeps this path free of anything else.
figure_->plot(i).set_selection_rect(frame_for(*row_controllers_[i]));
}
return;
}
plot_->set_selection_rect(frame_for(controller_));
}
void PlotArea::sync_figure_crosshair() noexcept {
if (row_rects_.empty() || row_controllers_.empty()) return;
const u32 row_count = static_cast<u32>(row_rects_.size());
@@ -534,6 +549,9 @@ void PlotArea::render() noexcept {
// `::resize` ratio, not `gtk_widget_get_scale_factor()` -- the latter is the
// ceiling under fractional scaling and would over-scale the chrome by up to 60%.
renderer_->style().ui_scale = mapping_.factor;
// After the scale and before the draw: the preview reads the row's layout, and a layout measured
// with the previous frame's scale is not the one being drawn.
sync_selection_frames();
renderer_->begin_frame(static_cast<u32>(viewport.w), static_cast<u32>(viewport.h), viewport);
if (figure_ != nullptr) {
renderer_->draw(*figure_);
+11
View File
@@ -397,6 +397,17 @@ class PlotArea {
/// Spreads one row's crosshair over the whole figure (see "Figure mode"). Run once
/// per frame, from the render callback, after this frame's row rects are cached.
void sync_figure_crosshair() noexcept;
/// Hands each row's plot the box-zoom frame its controller is showing, or takes it off
/// the ones whose gesture is over. Run once per frame from the render callback, like
/// the crosshair's spread and for a related reason: a frame has to go away when a drag
/// ends by *any* route -- the release, Escape, a row rebuilt mid-gesture -- and a
/// per-event hook would have to be repeated at every one of them, which is how a frame
/// ends up stuck on screen after a cancelled zoom.
///
/// Reads `selection_preview()`, which needs the row's layout, only for a controller
/// actually in `BoxZoom`: the cost is paid while a range is being chosen and never
/// otherwise.
void sync_selection_frames() noexcept;
/// `figure_->set_viewport(px)`, one `layout()` into the rect cache, and each row's
/// own rect to its controller. The figure-mode half of `refresh_viewport()` and of
/// `render()`, which must not drift apart.