diff --git a/crates/ui-egui/src/app/control.rs b/crates/ui-egui/src/app/control.rs index 6d095d4842..f2a35b16c2 100644 --- a/crates/ui-egui/src/app/control.rs +++ b/crates/ui-egui/src/app/control.rs @@ -88,7 +88,9 @@ impl VectorcraftApp { } } } - control::Outcome::AfterToolEdit { from, to } => self.pointer_replies.push((from, to, reply)), + control::Outcome::AfterToolEdit { from, to, document, tool } => { + self.pointer_replies.push(control::PendingPointerReply { from, to, document, tool, reply }); + } } } if handled == control::MAX_CONTROL_REQUESTS_PER_FRAME { @@ -120,11 +122,12 @@ impl VectorcraftApp { /// Sends the waiting `ui.pointer` replies whose events have been made. fn send_pointer_replies(&mut self) { - for (from, to, reply) in std::mem::take(&mut self.pointer_replies) { + for pending in std::mem::take(&mut self.pointer_replies) { + let control::PendingPointerReply { from, to, document, tool, reply } = pending; if self.session.input_applied(from, to) { - let _ = reply.send(control::pointer_reply(self)); + let _ = reply.send(control::pointer_reply_for(self, document, &tool)); } else { - self.pointer_replies.push((from, to, reply)); + self.pointer_replies.push(control::PendingPointerReply { from, to, document, tool, reply }); } } } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index d5a0e2ef61..2e632616c4 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -95,6 +95,14 @@ fn reserve_synthetic(app: &mut VectorcraftApp, additional: usize) -> Result<(), pub type ControlResponse = Value; +pub(crate) struct PendingPointerReply { + pub from: u64, + pub to: u64, + pub document: Option, + pub tool: String, + pub reply: Sender, +} + pub struct ControlRequest { pub method: String, pub params: Value, @@ -151,14 +159,28 @@ pub enum Outcome { AfterToolEdit { from: u64, to: u64, + document: Option, + tool: String, }, } /// The reply to `ui.pointer`: the selection, the key object and the active tool. pub(crate) fn pointer_reply(app: &mut VectorcraftApp) -> Value { - match app.run("document.inspect", json!({})) { - Ok(d) => json!({"ok": true, "result": {"selection": d["selection"], "key": d["key"], "tool": app.session.tool_id()}}), - Err(e) => json!({"ok": false, "error": e.to_string()}), + pointer_reply_for(app, app.session.active().map(|d| d.uid), app.session.tool_id()) +} + +pub(crate) fn pointer_reply_for(app: &VectorcraftApp, document: Option, tool: &str) -> Value { + let state = document.and_then(|uid| app.session.documents().iter().find(|d| d.uid == uid)); + match state { + Some(state) => json!({ + "ok": true, + "result": { + "selection": state.selection.objects.iter().map(|id| id.0).collect::>(), + "key": state.selection.key.map(|id| id.0), + "tool": tool, + } + }), + None => json!({"ok": false, "error": "the gesture's document is no longer open"}), } } @@ -558,7 +580,7 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe // a hover is answered at once. let to = app.session.input_count(); if !app.session.input_applied(from, to) { - return Outcome::AfterToolEdit { from, to }; + return Outcome::AfterToolEdit { from, to, document: app.session.active().map(|d| d.uid), tool: app.session.tool_id().to_string() }; } Outcome::Done(pointer_reply(app)) } diff --git a/crates/ui-egui/src/control/tests.rs b/crates/ui-egui/src/control/tests.rs index d0decf73a6..fc2ef0100b 100644 --- a/crates/ui-egui/src/control/tests.rs +++ b/crates/ui-egui/src/control/tests.rs @@ -12,6 +12,31 @@ fn input_request(app: &mut VectorcraftApp, method: &str, params: Value) -> Value r } +#[test] +fn deferred_pointer_reply_uses_the_gesture_document_after_file_new() { + let mut app = VectorcraftApp::new(Session::new(), Services::default()); + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + let id = app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 40, "height": 40})).unwrap()["id"].as_u64().unwrap(); + app.run("select.set", json!({"ids": [id]})).unwrap(); + let (reply, received) = std::sync::mpsc::channel(); + let at = app.session.input_count(); + app.pointer_replies.push(super::PendingPointerReply { + from: at, + to: at, + document: app.session.active().map(|d| d.uid), + tool: app.session.tool_id().to_string(), + reply, + }); + + app.run("file.new", json!({"width": 200, "height": 200})).unwrap(); + app.answer_pointer_replies(&egui::Context::default()); + + let result = received.try_recv().unwrap(); + assert_eq!(result["ok"], true, "{result}"); + assert_eq!(result["result"]["selection"], json!([id])); + assert_eq!(result["result"]["key"], Value::Null); +} + #[test] fn ui_set_turns_the_image_trace_preview_off_and_on() { let mut app = VectorcraftApp::new(Session::new(), Services::default()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index cf01a3e3d5..f76ec238c0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -81,7 +81,7 @@ use tests::{ mod tests; use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::mpsc::{Receiver, Sender}; +use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use serde_json::{Value, json}; @@ -136,7 +136,7 @@ pub struct VectorcraftApp { pending_screenshots: Vec, /// `ui.pointer` replies waiting for their events and the edit the tool computes on another /// thread ([`control::Outcome::AfterToolEdit`]), with the numbers of those events. - pointer_replies: Vec<(u64, u64, Sender)>, + pointer_replies: Vec, /// Admitted captures whose settled frame has not come yet (issued one at a time). queued_screenshots: Vec, screenshot_token: u64, diff --git a/crates/ui-egui/src/state.rs b/crates/ui-egui/src/state.rs index e7460b7e8d..1d53724c98 100644 --- a/crates/ui-egui/src/state.rs +++ b/crates/ui-egui/src/state.rs @@ -66,8 +66,9 @@ pub const ROTATE_VIEW_ANGLES: [(&str, f64); 12] = [ ("-90°", -90.0), ]; -use super::{ControlResponse, Sender}; +use super::ControlResponse; use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Sender; /// Zoom stop stepping reads this (set each UI frame and before commands) so canvas wheel zoom /// matches the status bar when Display Print Size at 100% Zoom is on.