diff --git a/crates/ui-egui/src/app/control.rs b/crates/ui-egui/src/app/control.rs index 5031234b8..039012f35 100644 --- a/crates/ui-egui/src/app/control.rs +++ b/crates/ui-egui/src/app/control.rs @@ -13,8 +13,9 @@ impl VectorcraftApp { // Synthetic input sent before this pointer request was dropped (see // `discard_stale_injected_batch`): its events would reach the tool without that input. if req.method == "ui.pointer" && req.made_before(self.synthetic_dropped_before) { - handled += 1; + // One its caller gave up is skipped without counting toward this frame's limit. if req.claim() { + handled += 1; let _ = req.reply.send(json!({ "ok": false, "error": "input sent before this pointer request was dropped because the document, dialog or focus changed" diff --git a/crates/ui-egui/src/tests/synthetic.rs b/crates/ui-egui/src/tests/synthetic.rs index acf2c55ca..1afa4da1e 100644 --- a/crates/ui-egui/src/tests/synthetic.rs +++ b/crates/ui-egui/src/tests/synthetic.rs @@ -142,6 +142,27 @@ fn a_given_up_pointer_does_not_hold_back_later_requests() { assert!(up_reply.try_recv().is_err(), "the given-up pointer request did not run"); } +/// Given-up `ui.pointer` requests made before synthetic input was dropped do not count toward a +/// frame's request limit: a request queued behind a frame's worth of them runs in that frame. +#[test] +fn given_up_stale_pointer_requests_do_not_use_up_the_frame() { + let (tx, rx) = std::sync::mpsc::channel(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()).with_control(rx); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + for _ in 0..crate::control::MAX_CONTROL_REQUESTS_PER_FRAME { + let (up, _up_reply) = ControlRequest::new("ui.pointer", json!({"events": [{"kind": "up", "x": 200, "y": 180}]})); + assert!(up.ticket().give_up(), "the pointer request had not started"); + tx.send(up).unwrap(); + } + app.synthetic_dropped_before = ControlRequest::next_seq(); + let (list, list_reply) = ControlRequest::new("ui.tool.list", json!({})); + tx.send(list).unwrap(); + app.drain_control(&ctx); + let r = list_reply.try_recv().expect("the list ran in the same frame"); + assert_eq!(r["ok"], true, "{r}"); +} + /// A `ui.pointer` release sent after a `ui.key` fails when the key's batch is discarded because a /// dialog opened before a UI pass took it, whether the release was waiting for the key /// (`waiting`) or still on the channel. A release made after the discard runs.