Merge pull request #5753 from dotCooCoo/fix/pointer-waits-for-queued-keys

MCP: pointer_gesture waits for a key sent before it
This commit is contained in:
BFlat
2026-11-05 13:35:07 +07:00
committed by GitHub
6 changed files with 51 additions and 3 deletions
+1
View File
@@ -104,6 +104,7 @@ impl VectorcraftApp {
clipboard_publishing: Default::default(),
pending_url: None,
control_rx: None,
deferred_control: None,
pending_screenshots: vec![],
queued_screenshots: vec![],
screenshot_token: 0,
+9 -1
View File
@@ -9,7 +9,15 @@ impl VectorcraftApp {
let Some(rx) = self.control_rx.take() else { return };
let mut handled = 0;
while handled < control::MAX_CONTROL_REQUESTS_PER_FRAME {
let Ok(req) = rx.try_recv() else { break };
let Some(req) = self.deferred_control.take().or_else(|| rx.try_recv().ok()) else { break };
// `ui.pointer` drives the tool at once, while `ui.key` and the other synthetic input
// reach it in a later UI pass: a pointer request waits until the input sent before it
// was applied, so a key pressed during a drag changes the drag before its release.
if req.method == "ui.pointer" && (!self.synthetic.is_empty() || self.synthetic_injected) {
self.deferred_control = Some(req);
ctx.request_repaint();
break;
}
// Its caller stopped waiting (the app was busy or its window not drawn for too long)
// and was told it won't run: an edit applied now would come from nowhere (#1725).
if !req.claim() {
+2 -1
View File
@@ -10,7 +10,8 @@
//! - `ui.contextMenu.list`: the canvas context menu for the current selection, flattened
//! - `ui.tool.select {tool}`, `ui.tool.list`
//! - `ui.pointer {events:[{kind: down|drag|up|move|doubleclick, x, y, backquote?, space?: "doc"|"screen"}], mods?, backquote?}`:
//! drive the active tool through the same path as the mouse
//! drive the active tool through the same path as the mouse, after the synthetic input sent
//! before it was applied
//! - `ui.key {key, shift?, alt?, cmd?}` / `ui.text {text}`: synthetic keyboard input
//! - `ui.move {x, y}` / `ui.click {x, y, button?, count?, shift?…}` / `ui.drag {x, y, toX, toY, steps?}`:
//! real egui pointer input in screen points (reaches every widget: panels, flyouts, dialogs)
+3
View File
@@ -151,6 +151,9 @@ pub struct VectorcraftApp {
/// moves ([`native_menu::sync`]).
run_count: u64,
control_rx: Option<Receiver<ControlRequest>>,
/// A `ui.pointer` request taken from the channel while synthetic input sent before it was not
/// yet applied: it runs, ahead of the channel, on the first frame after that input is applied.
deferred_control: Option<ControlRequest>,
/// Window captures asked for and not answered yet.
pending_screenshots: Vec<PendingScreenshot>,
/// Admitted captures whose settled frame has not come yet (issued one at a time).
+35
View File
@@ -82,6 +82,41 @@ fn a_typed_key_holds_its_modifiers_for_its_text() {
assert_eq!(app.session.tool_id(), "blobBrush", "Shift+B, not B");
}
/// A `ui.pointer` release sent right after a `ui.key` waits until the key reached the busy tool:
/// C pressed while an arc is dragged out closes the arc drawn on release.
#[test]
fn a_pointer_release_after_a_key_waits_for_the_key() {
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();
app.run("tool.select", json!({"tool": "arc"})).unwrap();
let ctx = egui::Context::default();
control(&mut app, &ctx, "ui.pointer", json!({"events": [{"kind": "down", "x": 100, "y": 100}, {"kind": "drag", "x": 200, "y": 180}]}));
let (key, _key_reply) = ControlRequest::new("ui.key", json!({"key": "c"}));
let (up, up_reply) = ControlRequest::new("ui.pointer", json!({"events": [{"kind": "up", "x": 200, "y": 180}]}));
tx.send(key).unwrap();
tx.send(up).unwrap();
// Each frame as the host runs it: the input hook, the control requests, then the UI pass.
let mut released = None;
for _ in 0..8 {
let mut raw = egui::RawInput::default();
app.raw_input_hook(&ctx, &mut raw);
app.drain_control(&ctx);
if let Ok(r) = up_reply.try_recv() {
released = Some(r);
break;
}
let mut out = ctx.run_ui(raw, |ui| crate::shortcuts::handle(&mut app, ui.ctx()));
out.textures_delta.clear();
}
let r = released.expect("the release ran");
assert_eq!(r["ok"], true, "{r}");
let document = app.session.active().unwrap();
let arc = *document.selection.objects.last().expect("the arc drawn on release");
let closed = document.doc.node(arc).unwrap().path_data().unwrap().subpaths[0].closed;
assert!(closed, "C reached the Arc tool before the release");
}
#[test]
fn a_drag_holds_its_modifiers_from_the_press_to_the_release() {
let (mut app, _) = one_square();
+1 -1
View File
@@ -426,7 +426,7 @@ opens the picker through `run_command` with the remote backend.
| `inspect_document` | `{depth?, childLimit?, ids?, kind?, limit?, offset?}` | Artboards (with `color`, `#rrggbb`, for one with a background color), layer tree (ids, kinds, bounds, paint), selection, history, tool. A type node's `fill`, `stroke` and `strokeWidth` are the paint its characters show (its first run's); fills and strokes of the type object itself come as `objectFill`, `objectStroke` and `objectStrokeWidth`. Paint comes as a label (`"#ff0000"`, `"None"`, `"G1 40% (#ff9999)"` for a swatch at a tint); `paint.proxies` gives it as an object. |
| `inspect_ui` | `{}` | UI state. Remote mode only. |
| `select_tool` | `{tool}` | `selection`, `directSelection`, `pen`, `rectangle`, `ellipse`, `polygon`, `star`, `lineSegment`, … |
| `pointer_gesture` | `{events:[{kind,x,y,mods?,backquote?,pressure?,tiltX?,tiltY?,rotation?,holdMs?}], tool?, mods?, backquote?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. `backquote` is the held state for each event; the top-level value supplies its default. Events go through the same path as the mouse. Headless, a `move` replies after the tool's highlight under the pointer is ready, including the Shape Builder's and the Live Paint Bucket's regions of a large selection, which the desktop app finds off the UI thread and highlights after the reply. |
| `pointer_gesture` | `{events:[{kind,x,y,mods?,backquote?,pressure?,tiltX?,tiltY?,rotation?,holdMs?}], tool?, mods?, backquote?}` | `kind` is one of `down`, `drag`, `up`, `move`, `doubleclick`. `backquote` is the held state for each event; the top-level value supplies its default. Events go through the same path as the mouse. Headless, a `move` replies after the tool's highlight under the pointer is ready, including the Shape Builder's and the Live Paint Bucket's regions of a large selection, which the desktop app finds off the UI thread and highlights after the reply. Remote, the events wait until the keys and other input sent before them (`press_key`, `type_text`) have reached the app, so a key pressed between two calls of one drag applies before the release. |
| `draw_path` | `{points \| d, closed?, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `points` is `[[x,y],…]` or `[{x,y,in?,out?,smooth?},…]`. `d` is SVG path data. The options are below. |
| `draw_shape` | `{shape, …geometry, fill?, stroke?, strokeWidth?, dash?, cap?, join?, opacity?, name?, effects?}` | `rectangle`/`ellipse`: `x,y,width,height` (plus `radius` for corners). `polygon`: `cx,cy,radius,sides`. `star`: `cx,cy,radius1,radius2,points`. `line`: `x1,y1,x2,y2`. |
| `set_paint` | `{fill?, stroke?, strokeWidth?, ids?}` | Applies to the selection (or `ids`) and becomes the default for new art. Returns `{applied, objects}`: `objects` is the number of objects painted, as the `objects` of the `paint.setFill`, `paint.setStroke` and `stroke.set` replies (the largest of them): a selected group counts the objects inside it. When it is 0, a `note` says that only the paint for new art changed. |