Shape Builder: a press dropped while a merge is computed is not made after it

A long touch, a canceled touch or a second finger drops the press in
progress. When that press was held because a merge was being computed on
another thread, the tool had not seen it, yet it stayed in the held input
and was made once the merge was applied: the Shape Builder stayed in its
drag, or a Cmd-press kept the selection tool lent. Dropping such a press
now removes it, and what was held after it, from the held input, and
leaves the tool as it is.
This commit is contained in:
dotCooCoo
2026-11-08 14:20:56 -08:00
parent 58d5d76d3f
commit 02f13ce619
2 changed files with 87 additions and 1 deletions
+72
View File
@@ -939,6 +939,78 @@ fn shape_builder_holds_gestures_made_during_a_merge() {
}
}
/// A press held while a merge is computed on another thread, then dropped (a long touch, a
/// canceled touch, a second finger), is never made: once the merge is applied, the tool is at
/// rest and the document is the one the input before the press makes. A held click before the
/// press is still made.
#[test]
fn shape_builder_drops_a_press_held_during_a_merge() {
type Gesture = fn(&mut Session, ViewInfo);
fn press(s: &mut Session, v: ViewInfo, x: f64, mods: Mods) {
s.pointer(&PointerEvent::new(PointerKind::Down, x, 10.0).with_mods(mods), v).unwrap();
}
fn click(s: &mut Session, v: ViewInfo) {
for kind in [PointerKind::Down, PointerKind::Up] {
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
}
}
let cases: [(&str, bool, Gesture); 5] = [
("a press", false, |s, v| press(s, v, 75.0, Mods::default())),
("a drag", false, |s, v| {
press(s, v, 75.0, Mods::default());
s.pointer(&PointerEvent::new(PointerKind::Drag, 95.0, 10.0), v).unwrap();
}),
("a Cmd-press", false, |s, v| press(s, v, 305.0, Mods { cmd: true, ..Default::default() })),
("a press and a key", false, |s, v| {
press(s, v, 75.0, Mods::default());
s.tool_key(ToolKey::Escape, Mods::default(), v).unwrap();
}),
("a click, then a press", true, |s, v| {
click(s, v);
press(s, v, 135.0, Mods::default());
}),
];
let v = ViewInfo::default();
let start = || {
let mut s = session();
square_row(&mut s);
click_row(&mut s, v);
s
};
// A tool left in the middle of a gesture keeps waiting: the polls stop after a few.
let settle = |s: &mut Session| {
for _ in 0..8 {
if !s.tool_waiting() {
return;
}
s.tool_poll(v, true).unwrap();
}
};
let labels = |s: &Session| s.doc().unwrap().history.undo.iter().map(|e| e.label.clone()).collect::<Vec<_>>();
for (what, clicked, send) in cases {
// The input before the press, one after the other.
let mut one = start();
settle(&mut one);
if clicked {
click(&mut one, v);
settle(&mut one);
}
let mut s = start();
let from = s.input_count();
send(&mut s, v);
let to = s.input_count();
assert!(s.tool_editing() && !s.input_applied(from, to), "{what}: held");
s.drop_tool_press(v).unwrap();
settle(&mut s);
assert!(!s.tool_waiting() && !s.tool_editing() && s.input_applied(from, to), "{what}");
assert!(!s.tool_busy(), "{what}: the tool is at rest");
assert_eq!(s.tool_id(), "shapeBuilder", "{what}: no tool is lent");
assert!(undated(&s) == undated(&one), "{what}: the document the input before the press makes");
assert_eq!(labels(&s), labels(&one), "{what}");
assert_eq!(s.doc().unwrap().selection, one.doc().unwrap().selection, "{what}");
}
}
/// A new revision of the document made outside a command while a merge is computed on another
/// thread keeps the merge when the selected art is as it was (the next poll applies it, as the
/// command computes it), and drops it when the art changed.
+15 -1
View File
@@ -511,8 +511,14 @@ impl Session {
/// nothing and a click opens no size dialog. The tool gets back the persistent options it had
/// when the press began, and none of the press's changes to them are stored. A dropped Type
/// press restores the prior selection. The freeform gradient point selected once the tool
/// took the press is selected again.
/// took the press is selected again. A press held while the tool computes an edit on another
/// thread has not reached the tool: it is removed from the held input with everything held
/// after it, and the tool is left as it is.
pub fn drop_tool_press(&mut self, view: ViewInfo) -> Result<()> {
if let Some(i) = self.held_press() {
self.held_input.truncate(i);
return Ok(());
}
// Nothing to roll back without a document; the tool still drops the press and gets the release.
let _ = self.cancel_interaction();
let before = self.press_options.take();
@@ -554,6 +560,14 @@ impl Session {
r.map(drop)
}
/// Where the press in progress is in the held input: the last held press, when no release is
/// held after it.
fn held_press(&self) -> Option<usize> {
let ends = |kind: PointerKind| matches!(kind, PointerKind::Down | PointerKind::Up);
let i = self.held_input.iter().rposition(|(_, input, _)| matches!(input, HeldInput::Pointer(ev) if ends(ev.kind)))?;
matches!(self.held_input.get(i), Some((_, HeldInput::Pointer(ev), _)) if ev.kind == PointerKind::Down).then_some(i)
}
/// The options of `t` that [`Self::store_tool_settings`] stores.
fn persistent_options(t: &dyn Tool) -> Map<String, Value> {
let opts = t.options();