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

+58
View File
@@ -7461,3 +7461,61 @@ PLOT_TEST(a_repaired_invalid_span_is_fainter_than_a_missing_one) {
missing_ink, m_lo, m_hi, repaired_ink, r_lo, r_hi);
ctx.destroy();
}
PLOT_TEST(the_box_zoom_frame_lands_exactly_where_it_says) {
// The frame a host sets has to reach the framebuffer and cover exactly the rect it names. Both halves
// are read back rather than trusted, and the two renders are compared: what is under the frame is a
// trace, the grid and the axis chrome, so the assertions are differential -- a predicate guessing
// those colours would be testing the guess.
plottest::HeadlessContext ctx(kW, kH);
plot::Status st;
PLOT_CHECK(ctx.create(&st));
PLOT_CHECK(plot::gl::load(nullptr));
plot::Renderer r;
PLOT_CHECK(r.init(&st));
plot::data::ArraySeries s(sine_samples(), 0.0, 1.0);
plot::Plot p;
p.set_viewport({0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
const plot::u32 ax = p.add_x_axis({"t", s.x_bounds(), true, plot::AxisPlace::Bottom, false});
const plot::u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
p.add_trace({&s, plot::Color::hex(0x40C0FF), 1.5f, ax, ay});
// The *floor* of a pixel rather than its luminance: the fill is the axis furniture's light neutral at
// a low alpha, so over a saturated trace it can lower a channel while it raises the floor, and the
// floor is what says "something light was laid over this".
const auto floor_of = [&](plot::u32 x, plot::u32 y) {
plot::u8 px[4] = {};
PLOT_CHECK(ctx.read_pixel(x, ctx.height - 1u - y, px));
return static_cast<int>(std::min(px[0], std::min(px[1], px[2])));
};
// The renderer does not clear -- that is the host's -- and a second draw over the first would double
// every trace and change every pixel for reasons that have nothing to do with the frame.
const auto draw = [&](bool framed) {
if (framed) p.set_selection_rect({200.0f, 100.0f, 200.0f, 120.0f});
plot::gl::glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
plot::gl::glClear(plot::gl::GL_COLOR_BUFFER_BIT);
r.begin_frame(kW, kH, {0, 0, static_cast<plot::f32>(kW), static_cast<plot::f32>(kH)});
r.draw(p);
r.end_frame();
};
draw(false);
const int border_before = floor_of(200, 160); // the frame's left border, mid-height
const int inside_before = floor_of(300, 160); // the middle of the framed area
const int outside_before = floor_of(500, 160); // to the right of it
draw(true);
const int border_after = floor_of(200, 160);
const int inside_after = floor_of(300, 160);
const int outside_after = floor_of(500, 160);
// A frame, not a wash: the border is nearly opaque, the fill is not.
PLOT_CHECK(border_after > inside_after);
PLOT_CHECK(border_after > border_before + 60);
// A fill, not a panel: it lightens what it covers and leaves it readable.
PLOT_CHECK(inside_after > inside_before + 10);
// Exactly the rect, and not a pixel beyond it: this is the assertion that the frame matches the area
// the gesture is choosing rather than merely being somewhere on screen.
PLOT_CHECK(outside_after == outside_before);
p.clear_selection_rect();
}