mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 13:23:22 +00:00
MCP: pointer_gesture waits for a key sent before it
In remote mode, press_key queues its key for the next UI pass, while pointer_gesture drives the tool as soon as the app reads the request. A release sent right after a key could commit the drag before the key reached the tool, so C pressed while dragging out an arc left it open. A ui.pointer request now waits, ahead of the requests after it, until the synthetic input sent before it has been applied.
This commit is contained in:
@@ -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,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() {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user