From 595026aa69872c41352bc8e4c8a2209685ab3f36 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo <103679419+marcoarnulfo@users.noreply.github.com> Date: Tue, 13 Oct 2026 06:36:25 +0200 Subject: [PATCH] fix(ui): ui.click and ui.drag refuse coordinates that overflow (#1306) (#1447) ui.move refuses a coordinate that isn't finite once narrowed to a pointer position; ui.click {x, y} and ui.drag did not, so 1e39 became an infinite position and was queued. With debug assertions on, the next idle frame panicked in emath comparing NaN vectors. Both now refuse such a coordinate with an error, as ui.move does, and ui.drag also refuses ends too far apart for the way between them to stay finite. Adds click_and_drag_refuse_coordinates_that_overflow_like_move in tests/control.rs, which panics before this change. --- crates/ui-egui/src/control.rs | 12 ++++++++++-- crates/ui-egui/tests/control.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 1dcdc3295..eaa102b56 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -397,11 +397,16 @@ impl Control { fn drag(&mut self, p: &Value) -> Result { let point = |k: &str| -> Result { match p.get(k).and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect::>()).as_deref() { - Some([x, y]) => Ok(egui::pos2(*x as f32, *y as f32)), + Some([x, y]) => Some(egui::pos2(*x as f32, *y as f32)) + .filter(|pos| pos.is_finite()) + .ok_or_else(|| format!("ui.drag: {k} must be [x, y], finite coordinates in points")), _ => Err(format!("ui.drag: {k} must be [x, y] in points")), } }; let (from, to) = (point("from")?, point("to")?); + if !(to - from).is_finite() { + return Err("ui.drag: from and to must be a finite distance apart, in points".into()); + } let steps = p.get("steps").and_then(Value::as_u64).unwrap_or(8).clamp(1, 200) as usize; let modifiers = modifiers(p.get("modifiers"))?; let which = pointer_button(p, "ui.drag")?; @@ -417,7 +422,10 @@ impl Control { fn click(&mut self, p: &Value) -> Result { if let (Some(x), Some(y)) = (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) { - let pos = egui::pos2(x as f32, y as f32); + let point = |key: &str, n: f64| { + Some(n as f32).filter(|n| n.is_finite()).ok_or_else(|| format!("ui.click: {key} must be a finite coordinate in points")) + }; + let pos = egui::pos2(point("x", x)?, point("y", y)?); let which = pointer_button(p, "ui.click")?; let button = |pressed| egui::Event::PointerButton { pos, button: which, pressed, modifiers: egui::Modifiers::NONE }; let frames = self.inject(vec![vec![egui::Event::PointerMoved(pos)], vec![button(true)], vec![button(false)]]); diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index 4233089ec..a73b3073b 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -1550,3 +1550,29 @@ fn initial_zoom_option_is_applied_before_the_first_viewport_layout() { assert_eq!(h.state().views[0].zoom, percent as f32 / 100.0); } } + +/// #1306: a coordinate that only overflows once narrowed to a pointer position is refused by +/// click and drag, as move refuses it, before any pointer input is queued; the frames that follow +/// run, and the channel keeps working. +#[test] +fn click_and_drag_refuse_coordinates_that_overflow_like_move() { + let (mut h, c) = harness(); + for (method, params) in [ + ("ui.move", json!({ "x": 1e39, "y": -1e39 })), + ("ui.click", json!({ "x": 1e39, "y": -1e39 })), + ("ui.click", json!({ "x": 40.0, "y": 1e39 })), + ("ui.drag", json!({ "from": [1e39, -1e39], "to": [40.0, 40.0], "steps": 1 })), + ("ui.drag", json!({ "from": [40.0, 40.0], "to": [40.0, -1e39], "steps": 1 })), + // Each end fits, but not the way from one to the other. + ("ui.drag", json!({ "from": [-3e38, 40.0], "to": [3e38, 40.0], "steps": 1 })), + ] { + let e = call(&mut h, &c, method, params.clone()).expect_err(&format!("{method} {params} is refused")); + assert!(e.starts_with(&format!("{method}: ")) && e.contains("finite"), "{e}"); + h.run_steps(4); + assert_eq!(ok(&mut h, &c, "ui.inspect", json!({ "query": "Read", "role": "Button" }))["count"], 1); + } + let read = ok(&mut h, &c, "ui.inspect", json!({ "query": "Read", "role": "Button" }))["widgets"][0]["rect"].clone(); + let (x, y) = (read[0].as_f64().unwrap() + 4.0, read[1].as_f64().unwrap() + 4.0); + assert_eq!(ok(&mut h, &c, "ui.click", json!({ "x": x, "y": y }))["clicked"], json!([x, y])); + ok(&mut h, &c, "ui.drag", json!({ "from": [x, y], "to": [x + 10.0, y], "steps": 2 })); +}