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:
Martino Ferrari committed 2026-09-28 00:54:25 +02:00
1 parent 15931a2405
commit b5b9b42be1
8 files changed
+697 -58

No files matched your search

+42 -2
View File
@@ -39,6 +39,44 @@ f64 from_space(f64 v, bool log) noexcept {
/// key) zoom in, so the span shrinks by `exp(-notches * kZoomPerNotch)`.
f64 zoom_factor(f64 notches) noexcept { return std::exp(-notches * kZoomPerNotch); }
/// True when a coordinate is one arithmetic can work with. Every event here comes
/// from a host, and a host can hand over anything: a toolkit's "no pointer"
/// sentinel, a NaN from a degenerate mapping of its own, an infinity from a lost
/// device. A non-finite value rides the pan and zoom arithmetic into a range that
/// is no longer a range, and since a gesture that sets a range also clears auto
/// scale, `f` would be the only way back -- so it must not get in.
bool finite(f32 v) noexcept { return std::isfinite(v); }
/// `e`, with an unusable position replaced by the last known one and an unusable
/// delta by no movement. Sanitising rather than rejecting keeps the buttons in the
/// same event alive: a drag whose release arrived with a garbage position must
/// still end.
PointerEvent sanitized(const PointerEvent& e, bool have_pointer, f32 last_x,
f32 last_y) noexcept {
PointerEvent in = e;
if (!finite(in.x) || !finite(in.y)) {
in.x = have_pointer ? last_x : 0.0f;
in.y = have_pointer ? last_y : 0.0f;
in.dx = 0.0f;
in.dy = 0.0f;
return in;
}
if (!finite(in.dx)) in.dx = 0.0f;
if (!finite(in.dy)) in.dy = 0.0f;
return in;
}
WheelEvent sanitized(const WheelEvent& e, bool have_pointer, f32 last_x, f32 last_y) noexcept {
WheelEvent in = e;
if (!finite(in.x) || !finite(in.y)) {
in.x = have_pointer ? last_x : 0.0f;
in.y = have_pointer ? last_y : 0.0f;
}
if (!finite(in.dx)) in.dx = 0.0f;
if (!finite(in.dy)) in.dy = 0.0f;
return in;
}
} // namespace
void Controller::set_viewport(Rect px) {
@@ -49,7 +87,8 @@ void Controller::set_viewport(Rect px) {
plot_->layout();
}
void Controller::on_pointer(const PointerEvent& e) {
void Controller::on_pointer(const PointerEvent& raw) {
const PointerEvent e = sanitized(raw, has_pointer_, pointer_x_, pointer_y_);
has_pointer_ = true;
pointer_x_ = e.x;
pointer_y_ = e.y;
@@ -66,7 +105,8 @@ void Controller::on_pointer(const PointerEvent& e) {
if (e.down && mode_ != DragMode::None) update_drag(e);
}
void Controller::on_wheel(const WheelEvent& e) {
void Controller::on_wheel(const WheelEvent& raw) {
const WheelEvent e = sanitized(raw, has_pointer_, pointer_x_, pointer_y_);
plot_->layout();
const f64 factor = zoom_factor(static_cast<f64>(e.dy));
if (!(factor > 0.0)) return;