fix(imgui): keep the plot inside the child's clip rect and reject unusable input
The widget drew a region derived from the child's content rect alone, so a child the hosting window cut off cleared and painted over the host's pixels; it also scaled ImGui's (-FLT_MAX, -FLT_MAX) no-pointer sentinel and differenced it against the last real position, turning a drag out of the window into a ~1e35 pan, and it forgot the host's button levels across a re-bind, handing the next plot a press that never happened. The region is now the content rect intersected with the child's clip rect (a no-op when nothing clips it), an invalid position is no position update, and the button levels survive a re-bind. The controller sanitises non-finite coordinates and deltas for any host, and the widget's input translation now has a committed test on the headless EGL context.
This commit is contained in:
1 parent
15931a2405
commit
b5b9b42be1
8 files changed
+697
-58
No files matched your search
@@ -5,6 +5,7 @@
|
||||
#include <plot/data/array_series.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -248,6 +249,89 @@ PLOT_TEST(key_minus_zooms_out) {
|
||||
PLOT_CHECK(f.plot.x_range(f.ax).span() > 100.0);
|
||||
}
|
||||
|
||||
/// A host can hand the controller a coordinate that is not a number: ImGui reports
|
||||
/// (-FLT_MAX, -FLT_MAX) for "no mouse position", and any host can produce a NaN out
|
||||
/// of a degenerate mapping of its own. The pan arithmetic runs through the axis
|
||||
/// transforms, so such a value would leave a range that is not a range -- and
|
||||
/// because a gesture that sets a range also clears auto scale, panning and zooming
|
||||
/// could not bring it back.
|
||||
PLOT_TEST(non_finite_pointer_does_not_move_a_range) {
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0, 0, 800, 400});
|
||||
const plot::Range xr = p.x_range(ax);
|
||||
const plot::Range yr = p.y_range(ay);
|
||||
|
||||
c.on_pointer({100, 100, 0, 0, plot::input::Button::Left, 0, true, true, false});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::Pan);
|
||||
|
||||
const float inf = std::numeric_limits<float>::infinity();
|
||||
const float nan = std::numeric_limits<float>::quiet_NaN();
|
||||
const float sentinel = -std::numeric_limits<float>::max(); // ImGui's "no pointer"
|
||||
|
||||
// The pointer-left-the-window sentinel arrives at the unrepresentable end of the
|
||||
// axis, so scaling and differencing it is a 1e35 pan. It must read as no
|
||||
// movement at all.
|
||||
c.on_pointer({sentinel, sentinel, inf, -inf, plot::input::Button::Left, 0, true, false, false});
|
||||
PLOT_CHECK(std::isfinite(p.x_range(ax).min) && std::isfinite(p.x_range(ax).max));
|
||||
PLOT_CHECK(std::isfinite(p.y_range(ay).min) && std::isfinite(p.y_range(ay).max));
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-9);
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-9);
|
||||
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-9);
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::Pan); // still the same drag
|
||||
|
||||
// A usable position with an unusable delta is no movement either, and a later
|
||||
// usable position is not differenced against the sentinel.
|
||||
c.on_pointer({180, 130, inf, nan, plot::input::Button::Left, 0, true, false, false});
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-9);
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-9);
|
||||
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-9);
|
||||
c.on_pointer({220, 160, 40, 30, plot::input::Button::Left, 0, true, false, false});
|
||||
PLOT_CHECK(std::isfinite(p.x_range(ax).min)); // a real move still pans
|
||||
PLOT_CHECK(p.x_range(ax).min < xr.min);
|
||||
|
||||
// The buttons in a sanitised event still act: the release ends the drag.
|
||||
c.on_pointer({220, 160, 0, 0, plot::input::Button::Left, 0, false, false, true});
|
||||
PLOT_CHECK(c.drag_mode() == plot::input::DragMode::None);
|
||||
}
|
||||
|
||||
PLOT_TEST(non_finite_wheel_does_not_break_a_range) {
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
const u32 ax = p.add_x_axis({"t", {0.0, 100.0}, true, plot::AxisPlace::Bottom, false});
|
||||
const u32 ay = p.add_y_axis({"v", {-1.0, 1.0}, true, plot::AxisPlace::Left, false});
|
||||
plot::input::Controller c(p);
|
||||
c.set_viewport({0, 0, 800, 400});
|
||||
|
||||
const float inf = std::numeric_limits<float>::infinity();
|
||||
const float nan = std::numeric_limits<float>::quiet_NaN();
|
||||
|
||||
// A zoom is anchored at the data coordinate under the pointer, so a position that
|
||||
// is not a number must not become the anchor: the axis would be re-centred on
|
||||
// NaN, which no gesture could undo. The fallback is a real coordinate and the
|
||||
// zoom still happens. (A *finite* sentinel -- ImGui's `-FLT_MAX` for "no pointer"
|
||||
// -- is not the controller's to interpret: finite coordinates are legal input.
|
||||
// The adapter translates that one; see tests/test_imgui_widget.cpp.)
|
||||
const double span_before = p.x_range(ax).span();
|
||||
c.on_wheel({nan, nan, 0.0f, 1.0f, 0});
|
||||
PLOT_CHECK(std::isfinite(p.x_range(ax).min) && std::isfinite(p.x_range(ax).max));
|
||||
PLOT_CHECK(std::isfinite(p.y_range(ay).min) && std::isfinite(p.y_range(ay).max));
|
||||
PLOT_CHECK(std::fabs(p.x_range(ax).min) < 1e6 && std::fabs(p.x_range(ax).max) < 1e6);
|
||||
PLOT_CHECK(p.x_range(ax).span() < span_before);
|
||||
|
||||
// A non-finite amount is no zoom at all.
|
||||
const plot::Range xr = p.x_range(ax);
|
||||
const plot::Range yr = p.y_range(ay);
|
||||
c.on_wheel({400.0f, 200.0f, 0.0f, nan, 0});
|
||||
c.on_wheel({400.0f, 200.0f, 0.0f, inf, 0});
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).min, xr.min, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.x_range(ax).max, xr.max, 1e-12);
|
||||
PLOT_CHECK_NEAR(p.y_range(ay).min, yr.min, 1e-12);
|
||||
}
|
||||
|
||||
PLOT_TEST(key_f_auto_fits_traces) {
|
||||
plot::Plot p;
|
||||
p.set_viewport({0, 0, 800, 400});
|
||||
|
||||
Reference in new issue
Block a user