Shape Builder: a deferred pointer reply describes the gesture's document

A ui.pointer reply that waits for the merge now records the document and tool of the gesture and reads the selection from that document when it is sent, so a command that switches documents first (such as file.new) no longer makes the reply report the other document.
This commit is contained in:
dotCooCoo
2026-10-29 10:06:08 -07:00
parent 993566a476
commit 061b519e20
5 changed files with 62 additions and 11 deletions
+7 -4
View File
@@ -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 });
}
}
}
+26 -4
View File
@@ -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<u64>,
pub tool: String,
pub reply: Sender<ControlResponse>,
}
pub struct ControlRequest {
pub method: String,
pub params: Value,
@@ -151,14 +159,28 @@ pub enum Outcome {
AfterToolEdit {
from: u64,
to: u64,
document: Option<u64>,
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<u64>, 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::<Vec<_>>(),
"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))
}
+25
View File
@@ -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());
+2 -2
View File
@@ -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<PendingScreenshot>,
/// `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<ControlResponse>)>,
pointer_replies: Vec<control::PendingPointerReply>,
/// Admitted captures whose settled frame has not come yet (issued one at a time).
queued_screenshots: Vec<QueuedScreenshot>,
screenshot_token: u64,
+2 -1
View File
@@ -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.