mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 16:52:47 +00:00
Merge pull request #5762 from dotCooCoo/fix/pointer-backquote-boolean
MCP: pointer_gesture rejects a backquote that is not a boolean
This commit is contained in:
@@ -181,9 +181,11 @@ impl Headless {
|
||||
fn pointer(&mut self, p: &Value) -> Result<Value, String> {
|
||||
let events = p.get("events").and_then(Value::as_array).ok_or("missing `events`")?;
|
||||
let base: Mods = p.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or_default();
|
||||
let default = PointerEvent::json_backquote(p, false)?;
|
||||
let held = events.iter().map(|e| PointerEvent::json_backquote(e, default)).collect::<Result<Vec<_>, _>>()?;
|
||||
self.ensure_document();
|
||||
let mut requests = vec![];
|
||||
for e in events {
|
||||
for (e, backquote) in events.iter().zip(held) {
|
||||
let kind = s(e, "kind").unwrap_or("");
|
||||
let kind = pointer_kind(kind).ok_or_else(|| format!("unknown pointer kind `{kind}` (down|drag|up|move|doubleclick)"))?;
|
||||
if s(e, "space") == Some("screen") {
|
||||
@@ -192,8 +194,6 @@ impl Headless {
|
||||
let x = e.get("x").and_then(Value::as_f64).ok_or("pointer event needs numeric `x`")?;
|
||||
let y = e.get("y").and_then(Value::as_f64).ok_or("pointer event needs numeric `y`")?;
|
||||
let mods = e.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or(base);
|
||||
let backquote =
|
||||
e.get("backquote").and_then(Value::as_bool).unwrap_or_else(|| p.get("backquote").and_then(Value::as_bool).unwrap_or(false));
|
||||
let ev = PointerEvent {
|
||||
kind,
|
||||
pos: Point::new(x, y),
|
||||
|
||||
@@ -1431,6 +1431,34 @@ fn pointer_gesture_forwards_backquote_on_each_event() {
|
||||
assert_eq!(rect["bounds"]["x"], 100.0, "{doc}");
|
||||
}
|
||||
|
||||
/// A `backquote` that is not a boolean is an error, and none of the gesture's events runs.
|
||||
#[test]
|
||||
fn pointer_gesture_rejects_a_backquote_that_is_not_a_boolean() {
|
||||
let mut s = server();
|
||||
let made = call(&mut s, 1, "draw_shape", json!({"shape": "rectangle", "x": 100, "y": 100, "width": 50, "height": 50, "fill": "#ff0000"}));
|
||||
let id = serde_json::from_str::<Value>(&text_of(&made)).unwrap()["id"].as_u64().unwrap();
|
||||
let drag = |top: Option<Value>, first: Option<Value>| {
|
||||
let mut down = json!({"kind": "down", "x": 300, "y": 300});
|
||||
if let Some(b) = first {
|
||||
down["backquote"] = b;
|
||||
}
|
||||
let mut a = json!({"tool": "rectangle", "events": [down, {"kind": "drag", "x": 350, "y": 350}, {"kind": "up", "x": 350, "y": 350}]});
|
||||
if let Some(b) = top {
|
||||
a["backquote"] = b;
|
||||
}
|
||||
a
|
||||
};
|
||||
for (n, args) in [drag(Some(json!("true")), None), drag(None, Some(json!(1)))].into_iter().enumerate() {
|
||||
let r = call(&mut s, 2 + n as u64, "pointer_gesture", args);
|
||||
assert_eq!(r["isError"], true, "{r}");
|
||||
assert!(text_of(&r).contains("`backquote` must be a boolean"), "{r}");
|
||||
}
|
||||
let inspected = call(&mut s, 4, "inspect_document", json!({}));
|
||||
let doc = serde_json::from_str::<Value>(&text_of(&inspected)).unwrap();
|
||||
let children = doc["layers"][0]["children"].as_array().unwrap();
|
||||
assert_eq!(children.iter().map(|n| n["id"].as_u64()).collect::<Vec<_>>(), vec![Some(id)], "{doc}");
|
||||
}
|
||||
|
||||
/// A Shift-drag marquee reaches agents (#483): `pointer_gesture` with `mods.shift` toggles the
|
||||
/// objects it reaches, so a selected one leaves the selection and an unselected one joins it.
|
||||
#[test]
|
||||
|
||||
@@ -125,6 +125,12 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Args) -> Result<ToolResult, Str
|
||||
if !e.get("x").is_some_and(Value::is_number) || !e.get("y").is_some_and(Value::is_number) {
|
||||
return Err(format!("event {i}: numeric `x` and `y` required"));
|
||||
}
|
||||
if e.get("backquote").is_some_and(|v| !v.is_boolean()) {
|
||||
return Err(format!("event {i}: `backquote` must be a boolean"));
|
||||
}
|
||||
}
|
||||
if a.get("backquote").is_some_and(|v| !v.is_boolean()) {
|
||||
return Err("`backquote` must be a boolean".into());
|
||||
}
|
||||
if let Some(t) = a.get("tool").and_then(Value::as_str) {
|
||||
b.call("ui.tool.select", json!({"tool": t}))?;
|
||||
|
||||
@@ -104,6 +104,11 @@ impl PointerEvent {
|
||||
pub fn json_hold(e: &Value) -> f64 {
|
||||
e.get("holdMs").and_then(Value::as_f64).filter(|f| f.is_finite()).map_or(0.0, |ms| ms.clamp(0.0, 60_000.0) / 1000.0)
|
||||
}
|
||||
/// Whether backquote is held, from the `backquote` of a JSON pointer event or request (control
|
||||
/// channel, MCP): `default` when the key is absent, an error when it is not a boolean.
|
||||
pub fn json_backquote(v: &Value, default: bool) -> Result<bool, String> {
|
||||
v.get("backquote").map_or(Ok(default), |b| b.as_bool().ok_or_else(|| "`backquote` must be a boolean".to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -119,6 +124,17 @@ mod tests {
|
||||
value["backquote"] = serde_json::json!(true);
|
||||
assert!(serde_json::from_value::<PointerEvent>(value).unwrap().backquote);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_backquote_takes_the_default_only_when_absent() {
|
||||
use serde_json::json;
|
||||
assert_eq!(PointerEvent::json_backquote(&json!({}), true), Ok(true));
|
||||
assert_eq!(PointerEvent::json_backquote(&json!({"backquote": false}), true), Ok(false));
|
||||
assert_eq!(PointerEvent::json_backquote(&json!({"backquote": true}), false), Ok(true));
|
||||
for bad in [json!("true"), json!(1), json!(null)] {
|
||||
assert!(PointerEvent::json_backquote(&json!({"backquote": bad}), false).is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Keys tools care about.
|
||||
|
||||
@@ -540,7 +540,13 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe
|
||||
let base_mods: Mods = p.get("mods").and_then(|m| serde_json::from_value(m.clone()).ok()).unwrap_or_default();
|
||||
let view = app.view_info();
|
||||
let xf = app.canvas_rect.zip(app.view().copied()).map(|(rect, v)| Xf::new(rect, &v));
|
||||
for e in events {
|
||||
let held =
|
||||
PointerEvent::json_backquote(p, false).and_then(|d| events.iter().map(|e| PointerEvent::json_backquote(e, d)).collect::<Result<Vec<_>, _>>());
|
||||
let held = match held {
|
||||
Ok(held) => held,
|
||||
Err(e) => return err(e),
|
||||
};
|
||||
for (e, backquote) in events.iter().zip(held) {
|
||||
let kind = match e.get("kind").and_then(Value::as_str).unwrap_or("") {
|
||||
"down" => PointerKind::Down,
|
||||
"drag" => PointerKind::Drag,
|
||||
@@ -566,7 +572,6 @@ fn pointer_events(app: &mut VectorcraftApp, ctx: &egui::Context, req: &ControlRe
|
||||
Some(xf) if screen => crate::canvas::pose_on_page(q, &xf),
|
||||
_ => q,
|
||||
});
|
||||
let backquote = e.get("backquote").and_then(Value::as_bool).unwrap_or_else(|| p.get("backquote").and_then(Value::as_bool).unwrap_or(false));
|
||||
crate::canvas::dispatch(app, &PointerEvent { kind, pos, mods, backquote, pressure: PointerEvent::json_pressure(e), pen }, view);
|
||||
let hold = PointerEvent::json_hold(e);
|
||||
if hold > 0.0 {
|
||||
|
||||
@@ -293,6 +293,29 @@ fn synthetic_pointer_backquote_keeps_solid_object_in_place() {
|
||||
assert_eq!(xf.as_coeffs()[4], 20.0, "synthetic backquote drag changes pattern paint");
|
||||
}
|
||||
|
||||
/// `ui.pointer` rejects a `backquote` that is not a boolean, given for the request or for any
|
||||
/// event, before any of the events runs.
|
||||
#[test]
|
||||
fn synthetic_pointer_rejects_a_backquote_that_is_not_a_boolean() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Services::default());
|
||||
app.run("file.new", json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 50, "height": 50})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
assert_eq!(input_request(&mut app, "ui.tool.select", json!({"tool": "selection"}))["ok"], true);
|
||||
let before = app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().clone();
|
||||
let down = json!({"kind": "down", "x": 125, "y": 125});
|
||||
let drag = json!({"kind": "drag", "x": 145, "y": 125});
|
||||
for params in [
|
||||
json!({"events": [down, drag, {"kind": "up", "x": 145, "y": 125}], "backquote": "true"}),
|
||||
json!({"events": [down, drag, {"kind": "up", "x": 145, "y": 125, "backquote": 1}]}),
|
||||
] {
|
||||
let r = input_request(&mut app, "ui.pointer", params.clone());
|
||||
assert_eq!(r["ok"], false, "{params}: {r}");
|
||||
assert!(r["error"].as_str().is_some_and(|e| e.contains("`backquote` must be a boolean")), "{r}");
|
||||
assert_eq!(app.session.doc().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap(), &before, "{params}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `ui.pointer` replies with the key object: a click on one of two selected objects makes it
|
||||
/// the key, and a click on the key again lets it go.
|
||||
#[test]
|
||||
|
||||
@@ -426,6 +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, and a `backquote` that is not a boolean is an error. 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`. |
|
||||
|
||||
Reference in New Issue
Block a user