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:
1 parent
1fc97d818f
commit
be1f31a04f
14 files changed
+276
-1
No files matched your search
@@ -2403,3 +2403,54 @@ PLOT_TEST(an_unselectable_cursor_is_neither_hovered_nor_grabbed) {
|
||||
PLOT_CHECK_NEAR(p.cursor(readout).x, before, 1e-12);
|
||||
PLOT_CHECK(c.grabbed_cursor() == normal);
|
||||
}
|
||||
|
||||
PLOT_TEST(a_ctrl_box_zoom_previews_the_band_it_will_zoom) {
|
||||
// What a host draws for an open box zoom is not always the area the pointer swept. Ctrl frames the
|
||||
// x range only, so what its release will change is a *band* across the plot -- and a pointer dragged
|
||||
// straight across sweeps a sliver a couple of pixels tall, which would be the frame showing the
|
||||
// gesture instead of its effect. Shift frames both axes and gets the swept box itself; Alt is the
|
||||
// other single-axis mask, and gets the other frame.
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
p.add_x_axis({"t", {0.0, 800.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const plot::Rect axes = p.layout().axes;
|
||||
constexpr plot::u32 kAlt = static_cast<plot::u32>(plot::input::Mod::Alt);
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0, 0, 800, 400});
|
||||
|
||||
c.on_pointer({100, 200, 0, 0, plot::input::Button::Left, kCtrl, true, true, false});
|
||||
// Nothing framed until something was swept: the press alone is an empty rect at the pointer, and a
|
||||
// full-width band derived from it would claim a range that does not exist yet.
|
||||
PLOT_CHECK_NEAR(c.selection_preview().w, 0.0, 1e-6);
|
||||
c.on_pointer({300, 202, 200, 2, plot::input::Button::Left, kCtrl, true, false, false});
|
||||
const plot::Rect ctrl = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(ctrl.x, 100.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.w, 200.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.y, axes.y, 1e-6);
|
||||
PLOT_CHECK_NEAR(ctrl.h, axes.h, 1e-6);
|
||||
// The gesture's own geometry is untouched: the widening happens in the preview, so the range the
|
||||
// release computes is still the one the pointer described.
|
||||
PLOT_CHECK_NEAR(c.selection_rect().h, 2.0, 1e-6);
|
||||
c.on_pointer({300, 202, 0, 0, plot::input::Button::Left, kCtrl, false, false, true});
|
||||
|
||||
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kShift, true, true, false});
|
||||
c.on_pointer({400, 150, 300, 100, plot::input::Button::Left, kShift, true, false, false});
|
||||
const plot::Rect both = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(both.x, 100.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.y, 50.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.w, 300.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(both.h, 100.0, 1e-6);
|
||||
c.on_pointer({400, 150, 0, 0, plot::input::Button::Left, kShift, false, false, true});
|
||||
|
||||
c.on_pointer({100, 50, 0, 0, plot::input::Button::Left, kAlt, true, true, false});
|
||||
c.on_pointer({110, 150, 10, 100, plot::input::Button::Left, kAlt, true, false, false});
|
||||
const plot::Rect alt = c.selection_preview();
|
||||
PLOT_CHECK_NEAR(alt.x, axes.x, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.w, axes.w, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.y, 50.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(alt.h, 100.0, 1e-6);
|
||||
c.on_pointer({110, 150, 0, 0, plot::input::Button::Left, kAlt, false, false, true});
|
||||
// Closed: a frame on a finished gesture would claim the old selection is still open.
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
||||
PLOT_CHECK_NEAR(c.selection_preview().w, 0.0, 1e-6);
|
||||
}
|
||||
@@ -2789,3 +2789,22 @@ PLOT_TEST(an_annotation_can_be_moved_by_id) {
|
||||
p.set_annotation(42u, other);
|
||||
PLOT_CHECK_NEAR(p.annotation(id).x, 3.5, 1e-12);
|
||||
}
|
||||
|
||||
PLOT_TEST(the_selection_frame_is_carried_and_taken_off) {
|
||||
// The box-zoom frame is the host's to set -- the plot has no controller and learns nothing about
|
||||
// pointers -- so what it owes is to carry it: out through `layout()` unchanged, because it is
|
||||
// already in viewport pixels and a second derivation would be a second rounding of the same
|
||||
// rectangle, and gone when the host takes it off, because a frame left behind claims the selection
|
||||
// is still open.
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
PLOT_CHECK(p.layout().selection.w == 0.0f && p.layout().selection.h == 0.0f); // nothing swept
|
||||
p.set_selection_rect({10.0f, 20.0f, 30.0f, 40.0f});
|
||||
PLOT_CHECK_NEAR(p.selection_rect().x, 10.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.y, 20.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.w, 30.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.h, 40.0, 1e-6);
|
||||
p.clear_selection_rect();
|
||||
PLOT_CHECK_NEAR(p.selection_rect().w, 0.0, 1e-6);
|
||||
PLOT_CHECK_NEAR(p.layout().selection.h, 0.0, 1e-6);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in new issue
Block a user