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 })); +}