mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 04:14:26 +00:00
Merge pull request #4379 from dotCooCoo/fix/shape-builder-merge-off-ui-thread
Shape Builder: a large selection merges on another thread
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
use super::{
|
||||
Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, SubPath, Touched, Value, b, bad, check_numbers,
|
||||
Arc, Beside, BuilderMap, EngineError, FillRule, Node, NodeId, PathData, Point, Rect, Result, Session, Touched, Value, b, bad, check_numbers,
|
||||
f64_or, fill_param, finite, gap_param, ids_json, json, marquee_param, po, points_param, remnant, replace_leaves, replace_with_pieces, root_ids,
|
||||
shape_node, switch_or,
|
||||
};
|
||||
@@ -41,6 +41,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let gap = gap_param(p);
|
||||
// The map the Shape Builder tool already has, which a click or drag of it is made over.
|
||||
let shared = s.tool.shape_builder_map().filter(|(_, g)| *g == gap);
|
||||
let prepared = s.tool.shape_builder_merge();
|
||||
let label = if erase { "Shape Builder Delete" } else { "Shape Builder" };
|
||||
let (ids, merged, touched, split) = s.edit(label, |d, sel| {
|
||||
let (shapes, leaves) = b::shape_builder_shapes(d, &roots, open_filled_as_closed);
|
||||
@@ -89,16 +90,28 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
touched.add(map.hit(q, Some(tol)));
|
||||
return Ok((ids, None, touched, Some(split)));
|
||||
}
|
||||
let mut touched = Touched { regions: marquee.map(|(a, b)| map.regions_in_marquee(a, b)).unwrap_or_default(), ..Default::default() };
|
||||
touched.extend(&map.builder_hits_along(&pts, piece_tol));
|
||||
touched.edges = map.erased_edges(&touched);
|
||||
let touched = map.merge_touched(&pts, marquee, piece_tol);
|
||||
if touched.is_empty() {
|
||||
return Err(EngineError::Other("Shape Builder: no region under the pointer".into()));
|
||||
}
|
||||
let cuts: Vec<&SubPath> = touched.edges.iter().filter_map(|&k| map.edges.get(k)?.0.path.subpaths.first()).collect();
|
||||
let inputs = b::merge_inputs(&leaves.iter().collect::<Vec<_>>(), open_filled_as_closed);
|
||||
// The tool computed this merge on another thread when it would come out the same.
|
||||
let ready = prepared.as_ref().filter(|m| {
|
||||
shared.as_ref().is_some_and(|(s, _)| Arc::ptr_eq(s, &m.map) && std::ptr::eq(s.as_ref(), map))
|
||||
&& m.gap == gap
|
||||
&& m.touched == touched
|
||||
&& m.inputs == inputs
|
||||
});
|
||||
let computed;
|
||||
let geometry = match ready {
|
||||
Some(m) => &m.geometry,
|
||||
None => {
|
||||
computed = b::merge_geometry(map, &touched, gap, &inputs);
|
||||
&computed
|
||||
}
|
||||
};
|
||||
let picked: Vec<&po::Region> = touched.regions.iter().filter_map(|&i| map.regions.get(i).map(|r| &r.0)).collect();
|
||||
let faces = po::FaceMerger::with_gap(&map.shapes, gap_param(p));
|
||||
let union = if picked.is_empty() { PathData::default() } else { faces.merge(&picked) };
|
||||
let union = &geometry.union;
|
||||
// Pick Color From Artwork: the merged shape takes the look of the front-most object under the
|
||||
// drag's first point, else under its last point, else of the front-most object. Color
|
||||
// Swatches: the current fill, with the rest of the look of the front-most object over the
|
||||
@@ -115,7 +128,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// What goes in place of each object the map was made from (none: it stays as it is).
|
||||
let mut outs: Vec<Option<Vec<Node>>> = vec![];
|
||||
let mut merged_id = None;
|
||||
for (i, (sh, leaf)) in map.shapes.iter().zip(&leaves).enumerate() {
|
||||
for (i, leaf) in leaves.iter().take(map.shapes.len()).enumerate() {
|
||||
let mut out = None;
|
||||
let pieces: Vec<usize> = map.lines.iter().enumerate().filter(|(_, (l, _))| l.key == i as u64).map(|(k, _)| k).collect();
|
||||
if !pieces.is_empty() {
|
||||
@@ -131,41 +144,15 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
out = Some(kept);
|
||||
}
|
||||
} else {
|
||||
} else if let Some(Some(rest)) = geometry.rests.get(i) {
|
||||
// A shape loses the regions deleted or merged, then the edges deleted (each one on
|
||||
// its outline, shared edges from every shape along them).
|
||||
let in_union = picked.iter().any(|r| r.sources.contains(&i));
|
||||
let mut rest = if in_union {
|
||||
// What is left of it: its faces not merged or deleted, united. They share their
|
||||
// edges with the merged faces exactly, where subtracting the merged shape left
|
||||
// slivers of the outline along them (#569). A sliver goes in only where the
|
||||
// shape covers it and the merged shape does not take it in.
|
||||
let kept: Vec<&po::Region> = map
|
||||
.regions
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(k, (r, ..))| r.sources.contains(&i) && !touched.regions.contains(k))
|
||||
.map(|(_, (r, ..))| r)
|
||||
.collect();
|
||||
if kept.is_empty() { PathData::default() } else { faces.merge_rest(i, &kept, &picked) }
|
||||
// its outline, shared edges from every shape along them). What is left of its own
|
||||
// outline keeps its fill rule; united faces fill with non-zero.
|
||||
out = Some(if rest.path.is_empty() {
|
||||
vec![]
|
||||
} else {
|
||||
b::node_outline(leaf).map_or_else(|| sh.path.clone(), |o| o.0)
|
||||
};
|
||||
let fill_closes = open_filled_as_closed && !leaf.appearance.fill_paint().is_none();
|
||||
// Edges come back from the planar map within a few hundredths of the outlines they
|
||||
// follow, whatever else is selected.
|
||||
let cut_tol = b::outline_snap(&sh.path);
|
||||
let mut cut = false;
|
||||
for piece in &cuts {
|
||||
if let Some(r) = po::cut_out(&rest, piece, fill_closes, cut_tol) {
|
||||
rest = r;
|
||||
cut = true;
|
||||
}
|
||||
}
|
||||
if in_union || cut {
|
||||
// What is left of its own outline keeps its fill rule; united faces fill with non-zero.
|
||||
out = Some(if rest.is_empty() { vec![] } else { vec![remnant(d, leaf, rest, in_union.then_some(FillRule::NonZero))] });
|
||||
}
|
||||
vec![remnant(d, leaf, rest.path.clone(), rest.united.then_some(FillRule::NonZero))]
|
||||
});
|
||||
}
|
||||
// The merged shape goes in place of the object it takes its look from, so it stays in
|
||||
// that object's group (a clip group too).
|
||||
|
||||
@@ -752,6 +752,8 @@ pub(super) fn encode_ai(cmd: &str, f: &Format, doc: &Document, p: &Value) -> Res
|
||||
/// file becomes the document's own; the live document gets them in [`SaveJob::finish`]). Bad
|
||||
/// options fail here, before anything is encoded.
|
||||
pub fn save_job(s: &mut Session, plan: SavePlan) -> Result<SaveJob> {
|
||||
// A Shape Builder merge still being computed for a click is saved with the document.
|
||||
s.finish_tool_edit_later()?;
|
||||
// The web build, without a clock, leaves the dates unless given one.
|
||||
let at = if plan.path.is_some() && plan.retargets() { clock_date(s, "modified", plan.modified) } else { None };
|
||||
job_for_stamped(s, s.doc()?, plan, at)
|
||||
|
||||
@@ -678,6 +678,12 @@ impl Session {
|
||||
for e in self.prefs.keep_folders(&mut p) {
|
||||
log::warn!("{e}");
|
||||
}
|
||||
// The edit the active tool computes on another thread is applied first (the Preferences
|
||||
// dialog applies its settings here, outside a command). A failure to apply it was the
|
||||
// edit's own, and the preferences apply all the same.
|
||||
if self.depth == 0 {
|
||||
let _ = self.finish_tool_edit_later();
|
||||
}
|
||||
let grid_changed = p.gridline_every != self.prefs.gridline_every || p.grid_subdivisions != self.prefs.grid_subdivisions;
|
||||
let history_changed = p.history_states != self.prefs.history_states;
|
||||
let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{NodeId, Value, cmd};
|
||||
use super::{NodeId, Value, cmd, find_command};
|
||||
|
||||
/// The commands that act on the active artboard (#693), which the app passes as their `artboard`:
|
||||
/// Paste in Place, in Front and in Back paste onto it, Align to Artboard aligns to it, Distribute
|
||||
@@ -78,6 +78,34 @@ pub(crate) fn strip_replay_only(id: &str, p: &mut Value) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Queries that apply the edit the active tool is computing before they run, as edits do
|
||||
/// ([`Session::finish_tool_edit`]): a batch runs edits, Find Objects with `select`, Select All
|
||||
/// Instances and Lock Guides change the selection, and the proof views, the preferences and added
|
||||
/// fonts give the documents new revisions (they draw again, and type lays out again).
|
||||
const WAIT_FOR_TOOL_EDIT: &[&str] = &[
|
||||
"command.batch",
|
||||
"document.find",
|
||||
"symbol.selectInstances",
|
||||
"view.guides.lock",
|
||||
"view.proofSetup",
|
||||
"view.proofColors",
|
||||
"view.overprintPreview",
|
||||
"view.separationsPreview",
|
||||
"prefs.set",
|
||||
"prefs.reset",
|
||||
"text.addFontFiles",
|
||||
];
|
||||
|
||||
/// Does command `id`, run on its own, first apply the edit the active tool is computing on
|
||||
/// another thread and the input held for it ([`Session::finish_tool_edit`])? Every command that
|
||||
/// can edit, select, save or give a document a new revision does; queries that only read do not
|
||||
/// (`commands_that_do_not_wait_for_a_tool_edit_change_no_document` in the command sweep checks
|
||||
/// them). A frontend that adds params from
|
||||
/// its own state (the active artboard) applies them first itself.
|
||||
pub fn waits_for_tool_edit(id: &str) -> bool {
|
||||
find_command(id).is_some_and(|spec| spec.journal || WAIT_FOR_TOOL_EDIT.contains(&id))
|
||||
}
|
||||
|
||||
/// Journaled commands that write a file and change nothing in the document: with an object of a
|
||||
/// dynamic symbol instance's art selected, they read the instance as queries do
|
||||
/// ([`crate::Session::execute`]).
|
||||
|
||||
@@ -62,6 +62,19 @@ pub struct Session {
|
||||
/// While Cmd lends `tool` (a selection tool) for a drag: the tool it was lent to, which comes
|
||||
/// back as it was at the release ([`Session::pointer`]).
|
||||
pub(crate) lender: Option<Box<dyn Tool>>,
|
||||
/// Pointer and key input that came while the tool computed an edit on another thread, each with
|
||||
/// its number and view, made in order once the edit is applied.
|
||||
held_input: std::collections::VecDeque<(u64, tooling::HeldInput, ViewInfo)>,
|
||||
/// The number the next pointer or key event handed to the session gets.
|
||||
input_count: u64,
|
||||
/// The number of the event whose handling started the edit the tool is computing.
|
||||
edit_started_by: Option<u64>,
|
||||
/// Set while the tool's edit and the input held for it are applied: the commands that run then
|
||||
/// do not start applying them again.
|
||||
settling_tool_edit: bool,
|
||||
/// Requests for the UI from making held input where nothing could take them (a command, a tool
|
||||
/// switch), which the next [`Session::tool_poll`] returns.
|
||||
held_ui: Vec<UiRequest>,
|
||||
/// The active document (its uid) and its [`History::step`] when the tool last handled a pointer
|
||||
/// event or drew its overlays: when they differ at the next one, the tool is told first
|
||||
/// ([`vectorcraft_tools::Tool::history_moved`]). None until the first.
|
||||
@@ -308,7 +321,7 @@ mod session;
|
||||
pub(crate) use command_sets::SELECTS_GIVEN_IDS;
|
||||
#[cfg(test)]
|
||||
use command_sets::given_ids;
|
||||
pub use command_sets::{FIT_ACTIVE_ARTBOARD, ON_ACTIVE_ARTBOARD};
|
||||
pub use command_sets::{FIT_ACTIVE_ARTBOARD, ON_ACTIVE_ARTBOARD, waits_for_tool_edit};
|
||||
pub use doc_state::{DocState, OpenRows, RecalledSelection};
|
||||
pub use error::{EngineError, Result};
|
||||
pub use history::{History, HistoryEntry, NamedStep, default_history_budget};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::command_sets::{JOURNALED_READS, SELECTS_GIVEN_IDS, given_ids, strip_replay_only};
|
||||
use super::command_sets::{JOURNALED_READS, SELECTS_GIVEN_IDS, given_ids, strip_replay_only, waits_for_tool_edit};
|
||||
use super::doc_state::DocState;
|
||||
use super::error::{EngineError, Result};
|
||||
use super::history::NamedStep;
|
||||
|
||||
@@ -46,6 +46,11 @@ impl Session {
|
||||
journal: Journal::default(),
|
||||
tool: vectorcraft_tools::create("selection"),
|
||||
lender: None,
|
||||
held_input: Default::default(),
|
||||
input_count: 0,
|
||||
edit_started_by: None,
|
||||
settling_tool_edit: false,
|
||||
held_ui: vec![],
|
||||
tool_step: None,
|
||||
selection_at_lend: None,
|
||||
lent_selection: None,
|
||||
@@ -160,6 +165,9 @@ impl Session {
|
||||
}
|
||||
self.press_type_selection = None;
|
||||
let view = self.last_view;
|
||||
// The edit the tool computes on another thread and the input held for it belong to the
|
||||
// document being left. The switch goes on whatever they did, as for the deactivation.
|
||||
let _ = self.finish_tool_edit_later();
|
||||
// The switch goes on whatever the lent tool's last actions did, as for the deactivation.
|
||||
let _ = self.give_back_tool(view);
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.deactivate(cx));
|
||||
|
||||
@@ -230,6 +230,11 @@ impl Session {
|
||||
if label != "Typing" && cmd::typing_in_progress(self) {
|
||||
cmd::settle_interaction(self)?;
|
||||
}
|
||||
// An edit the tool computes on another thread (a Shape Builder merge) is applied first too,
|
||||
// so the snapshot the interaction previews from and goes back to holds it.
|
||||
if !self.in_interaction() {
|
||||
self.finish_tool_edit_later()?;
|
||||
}
|
||||
self.clear_deferred_overlay_cache();
|
||||
let paint = self.paint.clone();
|
||||
let st = self.doc_mut()?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::{
|
||||
CommandSpec, EngineError, JOURNALED_READS, NodeId, Result, SELECTS_GIVEN_IDS, Selection, Session, Value, cmd, find_command, given_ids, guard,
|
||||
strip_replay_only,
|
||||
strip_replay_only, waits_for_tool_edit,
|
||||
};
|
||||
|
||||
impl Session {
|
||||
@@ -39,6 +39,12 @@ impl Session {
|
||||
/// Execute a command by id. This is THE entry point for every frontend.
|
||||
pub fn execute(&mut self, id: &str, params: &Value) -> Result<Value> {
|
||||
let spec = find_command(id).ok_or_else(|| cmd::params::unknown_command(id))?;
|
||||
// An edit the active tool is computing on another thread (a Shape Builder merge) is
|
||||
// applied before a command that may edit, select, save or give a document a new revision,
|
||||
// as the click came first. Queries that only read do not wait for it.
|
||||
if self.depth == 0 && waits_for_tool_edit(id) {
|
||||
self.finish_tool_edit_later()?;
|
||||
}
|
||||
let outer = std::mem::replace(&mut self.reading, !spec.journal || JOURNALED_READS.contains(&id));
|
||||
let r = self.execute_spec(spec, id, params);
|
||||
self.reading = outer;
|
||||
|
||||
@@ -207,6 +207,10 @@ impl Session {
|
||||
/// [`Session::end_undo_scope`]: open an undo group there or, when one is open already, join it,
|
||||
/// so that the edits are part of its step. `None` without a document.
|
||||
pub fn begin_undo_scope(&mut self) -> Option<UndoScope> {
|
||||
// An edit the tool computes on another thread is a step before the scope, so the scope's
|
||||
// snapshot holds it. A failure to apply it was the edit's own, and the scope begins all the
|
||||
// same.
|
||||
let _ = self.finish_tool_edit_later();
|
||||
let journal = self.journal.len();
|
||||
let st = self.active()?;
|
||||
let uid = st.uid;
|
||||
|
||||
@@ -607,6 +607,486 @@ fn shape_builder_merge_uses_the_map_the_tool_has() {
|
||||
assert_eq!(maps_built(), built + 1, "another gap");
|
||||
}
|
||||
|
||||
/// 40 squares 20 pt wide in a row, each 10 pt right of the one before (160 segments, which the
|
||||
/// Shape Builder merges on another thread), all selected.
|
||||
fn square_row(s: &mut Session) -> Vec<NodeId> {
|
||||
let ids: Vec<NodeId> = (0..40).map(|k| rect(s, f64::from(k) * 10.0, 0.0, 20.0, 20.0)).collect();
|
||||
s.execute("select.set", &json!({"ids": ids.iter().map(|i| i.0).collect::<Vec<_>>()})).unwrap();
|
||||
ids
|
||||
}
|
||||
|
||||
/// The active document of `s` without its dates, so documents made in two sessions at different
|
||||
/// times compare equal when their art is.
|
||||
fn undated(s: &Session) -> vectorcraft_doc::Document {
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
(d.metadata.created, d.metadata.modified) = (None, None);
|
||||
d
|
||||
}
|
||||
|
||||
/// The Shape Builder over [`square_row`], its map built, after a click at (15, 10): the merge is
|
||||
/// being computed on another thread, and the document is as it was.
|
||||
fn click_row(s: &mut Session, v: ViewInfo) {
|
||||
s.select_tool("shapeBuilder", v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Move, 15.0, 10.0), v).unwrap();
|
||||
s.tool_poll(v, true).unwrap();
|
||||
let revision = s.doc().unwrap().revision;
|
||||
s.pointer(&PointerEvent::new(PointerKind::Down, 15.0, 10.0), v).unwrap();
|
||||
s.pointer(&PointerEvent::new(PointerKind::Up, 15.0, 10.0), v).unwrap();
|
||||
assert!(s.tool_editing() && s.tool_waiting());
|
||||
assert_eq!(s.doc().unwrap().revision, revision, "the release returns before the merge is applied");
|
||||
}
|
||||
|
||||
/// A click of the Shape Builder on a large selection merges on another thread, and the merge the
|
||||
/// next poll applies is the command's own, as one undo step.
|
||||
#[test]
|
||||
fn shape_builder_merges_a_large_selection_on_another_thread() {
|
||||
use vectorcraft_tools::builder::merges_computed;
|
||||
let v = ViewInfo::default();
|
||||
let mut s = session();
|
||||
square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
let computed = merges_computed();
|
||||
s.tool_poll(v, true).unwrap();
|
||||
assert!(!s.tool_editing());
|
||||
assert_eq!(merges_computed(), computed, "the command takes the merge computed on another thread");
|
||||
assert_eq!(s.journal.last().unwrap().0, "shapeBuilder.merge");
|
||||
assert_eq!(layer_children(&s), 41, "the overlap and what is left of the two squares over it");
|
||||
let mut direct = session();
|
||||
square_row(&mut direct);
|
||||
direct.execute("shapeBuilder.merge", &json!({"points": [[15, 10]]})).unwrap();
|
||||
assert!(undated(&s) == undated(&direct), "the same document as the command gives");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(layer_children(&s), 40, "one undo step");
|
||||
}
|
||||
|
||||
/// What comes after a click whose merge is being computed on another thread waits for the merge
|
||||
/// and runs on the merged art, as when the merge ran on the calling thread: a command (an edit,
|
||||
/// an undo, a selection, a save, a batch, Lock Guides, the proof views, a preference, Add Font
|
||||
/// Files), an option change, another tool, a save made outside a command, preferences applied
|
||||
/// outside a command, a dialog's preview, a scrubbed field's undo scope, a Cmd-click or Cmd-drag
|
||||
/// with the selection tool. The merge is the one computed on the other thread.
|
||||
#[test]
|
||||
fn shape_builder_applies_a_merge_before_what_comes_next() {
|
||||
use vectorcraft_tools::builder::merges_computed;
|
||||
type Next = (&'static str, fn(&mut Session, &[NodeId], &std::path::Path));
|
||||
let v = ViewInfo::default();
|
||||
let next: Vec<Next> = vec![
|
||||
("Select All Instances", |s, _, _| {
|
||||
s.execute("symbol.selectInstances", &json!({"name": "S"})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects.len(), 1);
|
||||
}),
|
||||
("an edit", |s, _, _| {
|
||||
s.execute("object.move", &json!({"dx": 0, "dy": 100})).unwrap();
|
||||
}),
|
||||
("an undo", |s, _, _| {
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(layer_children(s), 40, "the undo takes the merge back");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
}),
|
||||
("another selection", |s, _, _| {
|
||||
s.execute("select.inverse", &json!({})).unwrap();
|
||||
}),
|
||||
("Select All", |s, _, _| {
|
||||
s.execute("select.all", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().selection.objects.len(), 41);
|
||||
}),
|
||||
("a save", |s, _, dir| {
|
||||
s.execute("file.saveAs", &json!({"path": dir.join("a.vectorcraft").to_string_lossy()})).unwrap();
|
||||
assert!(!s.doc().unwrap().is_dirty(), "the file holds the merge");
|
||||
}),
|
||||
("a save outside a command", |s, _, dir| {
|
||||
let p = json!({"path": dir.join("b.vectorcraft").to_string_lossy()});
|
||||
let plan = cmd::fileio::save_plan(s, cmd::fileio::SaveMode::SaveAs, &p).unwrap();
|
||||
assert_eq!(layer_children(s), 40, "planning a save leaves the document");
|
||||
let job = cmd::fileio::save_job(s, plan).unwrap();
|
||||
assert_eq!(layer_children(s), 41, "the snapshot is taken after the merge");
|
||||
drop(job);
|
||||
}),
|
||||
("a batch", |s, _, _| {
|
||||
s.execute("command.batch", &json!({"commands": [{"command": "object.move", "params": {"dx": 0, "dy": 50}}]})).unwrap();
|
||||
}),
|
||||
("an option change", |s, _, _| {
|
||||
s.set_tool_option_cmd(&json!({"key": "gapDetection", "value": true})).unwrap();
|
||||
}),
|
||||
("another tool", |s, _, _| {
|
||||
s.select_tool("selection", ViewInfo::default()).unwrap();
|
||||
}),
|
||||
("a closed document", |s, _, _| {
|
||||
s.execute("file.close", &json!({})).unwrap();
|
||||
}),
|
||||
("Lock Guides", |s, _, _| {
|
||||
s.execute("view.guides.lock", &json!({"locked": true})).unwrap();
|
||||
}),
|
||||
("an option set from a panel", |s, _, _| {
|
||||
s.set_tool_option("gapDetection", &json!(true));
|
||||
}),
|
||||
("a dialog's preview, canceled", |s, _, _| {
|
||||
s.begin_interaction("Move").unwrap();
|
||||
s.preview("object.move", &json!({"dx": 7, "dy": 0})).unwrap();
|
||||
s.preview("object.move", &json!({"dx": 9, "dy": 0})).unwrap();
|
||||
s.cancel_interaction().unwrap();
|
||||
assert_eq!(layer_children(s), 41, "canceling the preview keeps the merge");
|
||||
}),
|
||||
("a dialog's preview, kept", |s, _, _| {
|
||||
s.begin_interaction("Move").unwrap();
|
||||
s.preview("object.move", &json!({"dx": 7, "dy": 0})).unwrap();
|
||||
s.commit_interaction().unwrap();
|
||||
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(labels, ["Move", "Shape Builder"]);
|
||||
}),
|
||||
("a scrubbed field, canceled", |s, _, _| {
|
||||
let scope = s.begin_undo_scope().unwrap();
|
||||
s.execute("object.move", &json!({"dx": 5, "dy": 0})).unwrap();
|
||||
s.end_undo_scope(scope, true);
|
||||
assert_eq!(
|
||||
s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()),
|
||||
Some("Shape Builder"),
|
||||
"the scrub's step is undone, not the merge"
|
||||
);
|
||||
}),
|
||||
("a held click, then another tool", |s, _, _| {
|
||||
let v = ViewInfo::default();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
|
||||
}
|
||||
assert!(s.tool_editing(), "the click is held");
|
||||
s.select_tool("selection", v).unwrap();
|
||||
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(labels, ["Shape Builder", "Shape Builder"], "the held click merges with the Shape Builder first");
|
||||
}),
|
||||
("a held click, then another document", |s, _, _| {
|
||||
let v = ViewInfo::default();
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
|
||||
}
|
||||
let first = s.active_index().unwrap();
|
||||
s.add_document(vectorcraft_doc::Document::new(100.0, 100.0), None);
|
||||
assert!(!s.tool_editing());
|
||||
let labels: Vec<&str> = s.documents()[first].history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(labels, ["Shape Builder", "Shape Builder"], "the held click merges in its own document first");
|
||||
s.set_active(first);
|
||||
}),
|
||||
("a Cmd-click", |s, _, _| {
|
||||
let v = ViewInfo::default();
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
assert_eq!(s.cursor(Point::new(500.0, 300.0), cmd, v), vectorcraft_tools::Cursor::Busy, "Cmd shows the merge is computed");
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 500.0, 300.0).with_mods(cmd), v).unwrap();
|
||||
}
|
||||
assert!(s.tool_editing(), "the click is held");
|
||||
while s.tool_waiting() {
|
||||
s.tool_poll(v, true).unwrap();
|
||||
}
|
||||
assert!(s.doc().unwrap().selection.is_empty(), "the lent selection tool deselects after the merge");
|
||||
assert_eq!(s.tool_id(), "shapeBuilder");
|
||||
}),
|
||||
("a Cmd-drag", |s, _, _| {
|
||||
let v = ViewInfo::default();
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
for (kind, y) in [(PointerKind::Down, 10.0), (PointerKind::Drag, 60.0), (PointerKind::Drag, 110.0), (PointerKind::Up, 110.0)] {
|
||||
s.pointer(&PointerEvent::new(kind, 305.0, y).with_mods(cmd), v).unwrap();
|
||||
}
|
||||
assert!(s.tool_editing(), "the drag is held");
|
||||
while s.tool_waiting() {
|
||||
s.tool_poll(v, true).unwrap();
|
||||
}
|
||||
let labels: Vec<&str> = s.doc().unwrap().history.undo.iter().rev().take(2).map(|e| e.label.as_str()).collect();
|
||||
assert_eq!(labels, ["Move", "Shape Builder"]);
|
||||
}),
|
||||
// The proof views redraw every document (a new revision); they are left off, as the
|
||||
// other tests of the process-wide proof view expect.
|
||||
("Separations Preview", |s, _, _| {
|
||||
s.execute("view.separationsPreview", &json!({"on": false})).unwrap();
|
||||
}),
|
||||
("Overprint Preview", |s, _, _| {
|
||||
s.execute("view.overprintPreview", &json!({"on": false})).unwrap();
|
||||
}),
|
||||
("Proof Colors", |s, _, _| {
|
||||
s.execute("view.proofColors", &json!({"on": false})).unwrap();
|
||||
}),
|
||||
("Proof Setup", |s, _, _| {
|
||||
s.execute("view.proofSetup", &json!({})).unwrap();
|
||||
}),
|
||||
("a preference", |s, _, _| {
|
||||
let every = s.prefs.gridline_every;
|
||||
s.execute("prefs.set", &json!({"key": "gridlineEvery", "value": every + 10.0})).unwrap();
|
||||
}),
|
||||
("Reset Preferences", |s, _, _| {
|
||||
s.execute("prefs.reset", &json!({})).unwrap();
|
||||
}),
|
||||
("preferences applied outside a command", |s, _, _| {
|
||||
let mut p = s.prefs.clone();
|
||||
p.gridline_every += 10.0;
|
||||
s.apply_prefs(p);
|
||||
}),
|
||||
("Add Font Files", |s, _, _| {
|
||||
// No files: the command fails after the merge.
|
||||
assert!(s.execute("text.addFontFiles", &json!({"files": []})).is_err());
|
||||
}),
|
||||
];
|
||||
let _proof = crate::tests_colormgmt::GLOBAL.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
let dir = vectorcraft_testkit::temp_dir("shape-builder-merge-first");
|
||||
for (what, then) in next {
|
||||
let mut s = session();
|
||||
if what == "Select All Instances" {
|
||||
let far = rect(&mut s, 500.0, 500.0, 20.0, 20.0);
|
||||
s.execute("symbol.new", &json!({"name": "S", "ids": [far.0]})).unwrap();
|
||||
}
|
||||
let ids = square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
let computed = merges_computed();
|
||||
let journal = s.journal.len();
|
||||
then(&mut s, &ids, &dir);
|
||||
assert!(!s.tool_editing(), "{what}");
|
||||
assert_eq!(merges_computed(), computed, "{what}: the merge computed on another thread");
|
||||
assert_eq!(s.journal.since(journal).first().map(|j| j.0.as_str()), Some("shapeBuilder.merge"), "{what}: the merge comes first");
|
||||
if what != "a closed document" {
|
||||
assert!(layer_children(&s) >= 41, "{what}: the merge stays");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gestures and arrow keys that come while a merge is computed on another thread are held and
|
||||
/// made after it, in order: the document is the one they make one after the other, and the
|
||||
/// events count as made once they are and the merge they start is applied.
|
||||
#[test]
|
||||
fn shape_builder_holds_gestures_made_during_a_merge() {
|
||||
type Gesture = fn(&mut Session, ViewInfo);
|
||||
let cases: [(&str, bool, Gesture); 6] = [
|
||||
("a second click", false, |s, v| {
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
|
||||
}
|
||||
}),
|
||||
("a drag", false, |s, v| {
|
||||
for (kind, x) in [(PointerKind::Down, 75.0), (PointerKind::Drag, 85.0), (PointerKind::Drag, 95.0), (PointerKind::Up, 95.0)] {
|
||||
s.pointer(&PointerEvent::new(kind, x, 10.0), v).unwrap();
|
||||
}
|
||||
}),
|
||||
("two clicks", false, |s, v| {
|
||||
for x in [55.0, 135.0] {
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, x, 10.0), v).unwrap();
|
||||
}
|
||||
}
|
||||
}),
|
||||
("a Cmd-click", false, |s, v| {
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 500.0, 300.0).with_mods(cmd), v).unwrap();
|
||||
}
|
||||
}),
|
||||
("a Cmd-drag and a click", false, |s, v| {
|
||||
let cmd = Mods { cmd: true, ..Default::default() };
|
||||
for (kind, y) in [(PointerKind::Down, 10.0), (PointerKind::Drag, 110.0), (PointerKind::Up, 110.0)] {
|
||||
s.pointer(&PointerEvent::new(kind, 305.0, y).with_mods(cmd), v).unwrap();
|
||||
}
|
||||
for kind in [PointerKind::Down, PointerKind::Up] {
|
||||
s.pointer(&PointerEvent::new(kind, 55.0, 10.0), v).unwrap();
|
||||
}
|
||||
}),
|
||||
("an arrow key", true, |s, v| {
|
||||
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
|
||||
}),
|
||||
];
|
||||
let v = ViewInfo::default();
|
||||
let start = |swatches: bool| {
|
||||
let mut s = session();
|
||||
if swatches {
|
||||
s.select_tool("shapeBuilder", v).unwrap();
|
||||
s.set_tool_option_cmd(&json!({"key": "pickColorFrom", "value": "swatches"})).unwrap();
|
||||
}
|
||||
square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
s
|
||||
};
|
||||
for (what, swatches, send) in cases {
|
||||
// One after the other: the merge applied before the gesture, and each merge after its own.
|
||||
let mut one = start(swatches);
|
||||
while one.tool_waiting() {
|
||||
one.tool_poll(v, true).unwrap();
|
||||
}
|
||||
send(&mut one, v);
|
||||
while one.tool_waiting() {
|
||||
one.tool_poll(v, true).unwrap();
|
||||
}
|
||||
// While the merge is computed.
|
||||
let mut s = start(swatches);
|
||||
let revision = s.doc().unwrap().revision;
|
||||
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");
|
||||
assert_eq!(s.doc().unwrap().revision, revision, "{what}: nothing made before the merge");
|
||||
while s.tool_waiting() {
|
||||
s.tool_poll(v, true).unwrap();
|
||||
}
|
||||
assert!(!s.tool_editing() && s.input_applied(from, to), "{what}");
|
||||
assert!(undated(&s) == undated(&one), "{what}: the document the gestures make one after the other");
|
||||
assert_eq!(s.doc().unwrap().selection, one.doc().unwrap().selection, "{what}");
|
||||
assert_eq!(s.paint.fill, one.paint.fill, "{what}");
|
||||
let labels = |s: &Session| s.doc().unwrap().history.undo.iter().map(|e| e.label.clone()).collect::<Vec<_>>();
|
||||
assert_eq!(labels(&s), labels(&one), "{what}");
|
||||
// The click's merge alone.
|
||||
let mut merged = start(swatches);
|
||||
while merged.tool_waiting() {
|
||||
merged.tool_poll(v, true).unwrap();
|
||||
}
|
||||
let changed = labels(&s).len() > labels(&merged).len()
|
||||
|| s.paint.fill != merged.paint.fill
|
||||
|| s.doc().unwrap().selection != merged.doc().unwrap().selection;
|
||||
assert!(changed, "{what}: the gesture did something");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 same input makes, dropped the same way, after the merge.
|
||||
/// A held click before the press is still made, and so is a perspective plane key held after it.
|
||||
#[test]
|
||||
fn shape_builder_drops_a_press_held_during_a_merge() {
|
||||
use vectorcraft_tools::distort::perspective::{PerspectiveGrid, Plane};
|
||||
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();
|
||||
}
|
||||
}
|
||||
// What a case sends, and whether the perspective grid is shown.
|
||||
let cases: [(&str, bool, Gesture); 7] = [
|
||||
("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 press and an arrow key", false, |s, v| {
|
||||
press(s, v, 75.0, Mods::default());
|
||||
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
|
||||
}),
|
||||
("a press and a plane key", true, |s, v| {
|
||||
press(s, v, 75.0, Mods::default());
|
||||
s.tool_key(ToolKey::Digit(3), Mods::default(), v).unwrap();
|
||||
}),
|
||||
("a click, then a press", false, |s, v| {
|
||||
click(s, v);
|
||||
press(s, v, 135.0, Mods::default());
|
||||
}),
|
||||
];
|
||||
let v = ViewInfo::default();
|
||||
let start = |grid: bool| {
|
||||
let mut s = session();
|
||||
s.select_tool("shapeBuilder", v).unwrap();
|
||||
s.set_tool_option_cmd(&json!({"key": "pickColorFrom", "value": "swatches"})).unwrap();
|
||||
if grid {
|
||||
s.execute("perspective.grid.preset", &json!({"kind": 2})).unwrap();
|
||||
}
|
||||
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<_>>();
|
||||
let plane = |s: &Session| PerspectiveGrid::from_doc(&s.doc().unwrap().doc).unwrap().plane;
|
||||
for (what, grid, send) in cases {
|
||||
// The same input after the merge, dropped the same way.
|
||||
let mut one = start(grid);
|
||||
settle(&mut one);
|
||||
send(&mut one, v);
|
||||
one.drop_tool_press(v).unwrap();
|
||||
settle(&mut one);
|
||||
let mut s = start(grid);
|
||||
if grid {
|
||||
assert_ne!(plane(&s), Plane::Right, "{what}: the plane before the key");
|
||||
}
|
||||
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 makes after the merge");
|
||||
assert_eq!(labels(&s), labels(&one), "{what}");
|
||||
assert_eq!(s.doc().unwrap().selection, one.doc().unwrap().selection, "{what}");
|
||||
assert_eq!(s.paint.fill, one.paint.fill, "{what}");
|
||||
if grid {
|
||||
assert_eq!(plane(&one), Plane::Right, "{what}: the plane key is made after the merge");
|
||||
assert_eq!(plane(&s), Plane::Right, "{what}: the plane key is made");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn shape_builder_keeps_a_merge_over_a_new_revision_of_the_same_art() {
|
||||
use vectorcraft_tools::builder::merges_computed;
|
||||
let v = ViewInfo::default();
|
||||
let mut s = session();
|
||||
square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
let computed = merges_computed();
|
||||
s.active_mut().unwrap().revision += 1;
|
||||
s.tool_poll(v, true).unwrap();
|
||||
assert!(!s.tool_editing());
|
||||
assert_eq!(merges_computed(), computed, "the merge computed on another thread");
|
||||
assert_eq!(s.journal.last().unwrap().0, "shapeBuilder.merge");
|
||||
let mut direct = session();
|
||||
square_row(&mut direct);
|
||||
direct.execute("shapeBuilder.merge", &json!({"points": [[15, 10]]})).unwrap();
|
||||
assert!(undated(&s) == undated(&direct), "the same document as the command gives");
|
||||
// The art changed: the merge is dropped.
|
||||
let mut s = session();
|
||||
let ids = square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
let journal = s.journal.len();
|
||||
let st = s.active_mut().unwrap();
|
||||
Arc::make_mut(&mut st.doc).node_mut(ids[1]).unwrap().transform(Affine::translate((0.0, 50.0)), false);
|
||||
st.revision += 1;
|
||||
s.tool_poll(v, true).unwrap();
|
||||
assert!(!s.tool_editing());
|
||||
assert_eq!(s.journal.len(), journal, "nothing merged");
|
||||
assert_eq!(layer_children(&s), 40);
|
||||
}
|
||||
|
||||
/// A merge computed for the tool Cmd lent the selection tool's drag away from is applied before
|
||||
/// a command too.
|
||||
#[test]
|
||||
fn shape_builder_merge_of_the_lending_tool_comes_first() {
|
||||
let v = ViewInfo::default();
|
||||
let mut s = session();
|
||||
square_row(&mut s);
|
||||
click_row(&mut s, v);
|
||||
let lent = s.make_tool("selection");
|
||||
s.lender = Some(std::mem::replace(&mut s.tool, lent));
|
||||
assert!(s.tool_editing());
|
||||
let journal = s.journal.len();
|
||||
s.execute("object.move", &json!({"dx": 0, "dy": 100})).unwrap();
|
||||
assert!(!s.tool_editing());
|
||||
assert_eq!(s.journal.since(journal).first().map(|j| j.0.as_str()), Some("shapeBuilder.merge"));
|
||||
assert_eq!(s.tool_id(), "selection", "the lent tool stays active until the release");
|
||||
assert_eq!(s.lender.as_ref().map(|t| t.id()), Some("shapeBuilder"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shape_builder_tool_merges_the_narrow_shape_it_highlights_in_a_wide_selection() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -83,6 +83,23 @@ pub enum UiRequest {
|
||||
/// Place a file with these `file.place` params: a click or drag of the place cursor, when
|
||||
/// [`Session::place_cursor_requests`] is set.
|
||||
Place(Value),
|
||||
/// A press held while the tool computed an edit on another thread was made now, at `pos` in the
|
||||
/// document with [`crate::DocState::uid`] `doc`, with tool `tool` active (the selection tool Cmd
|
||||
/// lent): the frontend does what it does after a press it hands over itself.
|
||||
HeldPress {
|
||||
doc: Option<u64>,
|
||||
pos: Point,
|
||||
tool: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
/// Pointer or key input that came while the active tool computed an edit on another thread
|
||||
/// ([`Tool::editing`]), made in order once the edit is applied ([`Session::tool_poll`],
|
||||
/// [`Session::finish_tool_edit`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) enum HeldInput {
|
||||
Pointer(PointerEvent),
|
||||
Key(ToolKey, Mods),
|
||||
}
|
||||
|
||||
impl Session {
|
||||
@@ -106,6 +123,7 @@ impl Session {
|
||||
|
||||
/// Choose a tool (the toolbar, its shortcut, MCP), finishing any pending tool work first.
|
||||
pub fn select_tool(&mut self, id: &str, view: ViewInfo) -> Result<()> {
|
||||
self.finish_tool_edit_later()?;
|
||||
// Choosing a perspective tool shows the document's perspective grid; Hide Grid hides it
|
||||
// again, the tool staying chosen.
|
||||
if matches!(id, "perspectiveGrid" | "perspectiveSelection")
|
||||
@@ -129,6 +147,8 @@ impl Session {
|
||||
/// Switch to tool `id` without choosing it afresh (back from a temporary tool: a hidden
|
||||
/// perspective grid stays hidden), finishing any pending tool work first.
|
||||
pub fn switch_tool(&mut self, id: &str, view: ViewInfo) -> Result<()> {
|
||||
// The edit the tool computes on another thread and the input held for it come first.
|
||||
self.finish_tool_edit_later()?;
|
||||
self.give_back_tool(view)?;
|
||||
if self.tool.id() == id {
|
||||
return Ok(());
|
||||
@@ -430,8 +450,85 @@ impl Session {
|
||||
self.tool_step = now;
|
||||
}
|
||||
|
||||
/// Feed a pointer event to the active tool. Returns requests for the UI.
|
||||
/// Feed a pointer event to the active tool. Returns requests for the UI. While the tool computes
|
||||
/// an edit on another thread, a press and what follows it are held and made once the edit is
|
||||
/// applied, in order ([`Session::tool_poll`]); moves of the pointer with no button held go to
|
||||
/// the tool at once.
|
||||
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
let n = self.next_input();
|
||||
if ev.kind != PointerKind::Move && (!self.held_input.is_empty() || self.tool_edit_pending()) {
|
||||
self.held_input.push_back((n, HeldInput::Pointer(*ev), view));
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let r = self.pointer_now(ev, view);
|
||||
self.note_edit_start(n);
|
||||
r
|
||||
}
|
||||
|
||||
/// The number of the next pointer or key event handed to the session.
|
||||
fn next_input(&mut self) -> u64 {
|
||||
let n = self.input_count;
|
||||
self.input_count = n.wrapping_add(1);
|
||||
n
|
||||
}
|
||||
|
||||
/// How many pointer and key events have been handed to the session ([`Session::pointer`],
|
||||
/// [`Session::tool_key`]); with [`Session::input_applied`], a frontend tells when the events it
|
||||
/// handed over have been made.
|
||||
pub fn input_count(&self) -> u64 {
|
||||
self.input_count
|
||||
}
|
||||
|
||||
/// Have the events numbered `from..to` ([`Session::input_count`] before and after handing them
|
||||
/// over) been made, and the edit they started on another thread applied?
|
||||
pub fn input_applied(&self, from: u64, to: u64) -> bool {
|
||||
let ours = |n: &u64| (from..to).contains(n);
|
||||
!self.held_input.iter().any(|(n, ..)| ours(n)) && !(self.tool_edit_pending() && self.edit_started_by.as_ref().is_some_and(ours))
|
||||
}
|
||||
|
||||
/// After event `n` was made: the event that started the edit being computed, if one is.
|
||||
fn note_edit_start(&mut self, n: u64) {
|
||||
if !self.tool_edit_pending() {
|
||||
self.edit_started_by = None;
|
||||
} else if self.edit_started_by.is_none() {
|
||||
self.edit_started_by = Some(n);
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the active tool, or the tool Cmd lent the selection tool's drag away from, computing an
|
||||
/// edit on another thread?
|
||||
fn tool_edit_pending(&self) -> bool {
|
||||
self.tool.editing() || self.lender.as_ref().is_some_and(|t| t.editing())
|
||||
}
|
||||
|
||||
/// Makes the input held while the tool computed an edit, in order, until a held gesture starts
|
||||
/// another such edit.
|
||||
fn replay_held_input(&mut self) -> Result<Vec<UiRequest>> {
|
||||
let mut ui = vec![];
|
||||
let mut failed = None;
|
||||
while !self.tool_edit_pending()
|
||||
&& let Some((n, input, view)) = self.held_input.pop_front()
|
||||
{
|
||||
let r = match input {
|
||||
HeldInput::Pointer(ev) => self.pointer_now(&ev, view).map(|mut r| {
|
||||
if ev.kind == PointerKind::Down {
|
||||
r.push(UiRequest::HeldPress { doc: self.active().map(|d| d.uid), pos: ev.pos, tool: self.tool.id() });
|
||||
}
|
||||
r
|
||||
}),
|
||||
HeldInput::Key(key, mods) => self.tool_key_now(key, mods, view),
|
||||
};
|
||||
self.note_edit_start(n);
|
||||
match r {
|
||||
Ok(r) => ui.extend(r),
|
||||
// The input after it is still made, as it was when each event came on its own.
|
||||
Err(e) => failed = failed.or(Some(e)),
|
||||
}
|
||||
}
|
||||
failed.map_or(Ok(ui), Err)
|
||||
}
|
||||
|
||||
fn pointer_now(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
self.pointer_event(ev, view, true)
|
||||
}
|
||||
|
||||
@@ -442,8 +539,25 @@ 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 the pointer events
|
||||
/// and tool keys held after it, and the tool is left as it is. A perspective plane key held
|
||||
/// after it stays held, since the plane it picks outlasts a dropped press. Pointer events held
|
||||
/// after the last held release belong to the press the tool has, and are removed with it.
|
||||
pub fn drop_tool_press(&mut self, view: ViewInfo) -> Result<()> {
|
||||
let (from, held) = self.held_press();
|
||||
let keep: Vec<bool> = (self.held_input.iter().enumerate())
|
||||
.map(|(j, (_, input, _))| match input {
|
||||
_ if j < from => true,
|
||||
HeldInput::Pointer(_) => false,
|
||||
HeldInput::Key(key, _) => !held || self.plane_key(*key).is_some(),
|
||||
})
|
||||
.collect();
|
||||
let mut keep = keep.into_iter();
|
||||
self.held_input.retain(|_| keep.next().unwrap_or(true));
|
||||
if held {
|
||||
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();
|
||||
@@ -485,6 +599,19 @@ impl Session {
|
||||
r.map(drop)
|
||||
}
|
||||
|
||||
/// Where the input of the press in progress starts in the held input, and whether the press
|
||||
/// itself is held: the last held press when no release is held after it; otherwise the input
|
||||
/// after the last held release (all of it when none is held), which follows a press the tool
|
||||
/// already has.
|
||||
fn held_press(&self) -> (usize, bool) {
|
||||
let ends = |kind: PointerKind| matches!(kind, PointerKind::Down | PointerKind::Up);
|
||||
match self.held_input.iter().rposition(|(_, input, _)| matches!(input, HeldInput::Pointer(ev) if ends(ev.kind))) {
|
||||
Some(i) if matches!(self.held_input.get(i), Some((_, HeldInput::Pointer(ev), _)) if ev.kind == PointerKind::Down) => (i, true),
|
||||
Some(i) => (i + 1, false),
|
||||
None => (0, false),
|
||||
}
|
||||
}
|
||||
|
||||
/// The options of `t` that [`Self::store_tool_settings`] stores.
|
||||
fn persistent_options(t: &dyn Tool) -> Map<String, Value> {
|
||||
let opts = t.options();
|
||||
@@ -619,19 +746,148 @@ impl Session {
|
||||
self.tool.wants_samples()
|
||||
}
|
||||
|
||||
/// Is the active tool waiting for work it runs on another thread ([`Tool::waiting`])?
|
||||
/// Is the active tool waiting for work it runs on another thread ([`Tool::waiting`]), is input
|
||||
/// held until an edit it computes is applied, or are requests for the UI left from making it?
|
||||
pub fn tool_waiting(&self) -> bool {
|
||||
self.tool.waiting()
|
||||
self.tool.waiting() || !self.held_input.is_empty() || !self.held_ui.is_empty()
|
||||
}
|
||||
|
||||
/// Lets the active tool check on the work [`Session::tool_waiting`] reports, and with `wait`
|
||||
/// wait for it to finish ([`Tool::poll`]).
|
||||
pub fn tool_poll(&mut self, view: ViewInfo, wait: bool) -> Result<()> {
|
||||
self.with_tool_cx(view, |t, cx| t.poll(cx, wait));
|
||||
self.take_tool_panic()
|
||||
/// wait for it to finish ([`Tool::poll`]); an edit the work made is applied, and then the input
|
||||
/// held for it is made. Returns requests for the UI, those left from making held input where
|
||||
/// nothing could take them first ([`Session::finish_tool_edit_later`]).
|
||||
pub fn tool_poll(&mut self, view: ViewInfo, wait: bool) -> Result<Vec<UiRequest>> {
|
||||
if self.settling_tool_edit {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
self.settling_tool_edit = true;
|
||||
let r = self.tool_poll_now(view, wait);
|
||||
self.settling_tool_edit = false;
|
||||
// On an error the requests left wait for the next poll.
|
||||
let r = r?;
|
||||
let mut ui = std::mem::take(&mut self.held_ui);
|
||||
ui.extend(r);
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
fn tool_poll_now(&mut self, view: ViewInfo, wait: bool) -> Result<Vec<UiRequest>> {
|
||||
let acts = self.with_tool_cx(view, |t, cx| t.poll(cx, wait));
|
||||
self.take_tool_panic()?;
|
||||
let applied = !acts.is_empty();
|
||||
let mut ui = self.apply_actions(acts)?;
|
||||
if !self.tool_edit_pending() {
|
||||
self.edit_started_by = None;
|
||||
// The input held for an edit applied now is made at the next poll, so what waits for the
|
||||
// edit alone (a `ui.pointer` reply) sees the document as the edit left it.
|
||||
if !applied {
|
||||
ui.extend(self.replay_held_input()?);
|
||||
}
|
||||
}
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
/// Is the active tool computing an edit on another thread ([`Tool::editing`]), or is input held
|
||||
/// until it is applied?
|
||||
pub fn tool_editing(&self) -> bool {
|
||||
self.tool_edit_pending() || !self.held_input.is_empty()
|
||||
}
|
||||
|
||||
/// Waits for the edit the active tool computes on another thread and applies it
|
||||
/// ([`Tool::finish_edit`]), then makes the input held for it, waiting in turn for an edit that
|
||||
/// starts, so what runs next sees the document as the gestures left it. Commands, saves, option
|
||||
/// changes, interactions, undo scopes and tool or document switches call it first. Returns
|
||||
/// requests for the UI, with those left from earlier ([`Session::finish_tool_edit_later`]); on
|
||||
/// an error, the next [`Session::tool_poll`] returns them.
|
||||
pub fn finish_tool_edit(&mut self) -> Result<Vec<UiRequest>> {
|
||||
if self.settling_tool_edit {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let mut ui = std::mem::take(&mut self.held_ui);
|
||||
if self.tool_editing() {
|
||||
self.settling_tool_edit = true;
|
||||
let r = self.finish_tool_edit_now();
|
||||
self.settling_tool_edit = false;
|
||||
match r {
|
||||
Ok(r) => ui.extend(r),
|
||||
Err(e) => {
|
||||
ui.append(&mut self.held_ui);
|
||||
self.held_ui = ui;
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ui)
|
||||
}
|
||||
|
||||
/// [`Session::finish_tool_edit`] for a caller that cannot hand requests to the UI (a command, a
|
||||
/// tool switch): the next [`Session::tool_poll`] returns them, and [`Session::tool_waiting`]
|
||||
/// holds until then.
|
||||
pub(crate) fn finish_tool_edit_later(&mut self) -> Result<()> {
|
||||
let ui = self.finish_tool_edit()?;
|
||||
self.held_ui.extend(ui);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finish_tool_edit_now(&mut self) -> Result<Vec<UiRequest>> {
|
||||
let mut ui = vec![];
|
||||
let mut failed = None;
|
||||
loop {
|
||||
match self.finish_pending_edit() {
|
||||
Ok(r) => ui.extend(r),
|
||||
Err(e) => failed = failed.or(Some(e)),
|
||||
}
|
||||
// A tool that cannot finish its edit keeps the input held.
|
||||
if self.tool_edit_pending() {
|
||||
break;
|
||||
}
|
||||
self.edit_started_by = None;
|
||||
if self.held_input.is_empty() {
|
||||
break;
|
||||
}
|
||||
match self.replay_held_input() {
|
||||
Ok(r) => ui.extend(r),
|
||||
Err(e) => failed = failed.or(Some(e)),
|
||||
}
|
||||
}
|
||||
match failed {
|
||||
None => Ok(ui),
|
||||
Some(e) => {
|
||||
self.held_ui.extend(ui);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Waits for the edit being computed and applies it: the active tool's, or that of the tool
|
||||
/// Cmd lent the selection tool's drag away from, which is active meanwhile.
|
||||
fn finish_pending_edit(&mut self) -> Result<Vec<UiRequest>> {
|
||||
if self.tool.editing() {
|
||||
let acts = self.with_tool_cx(self.last_view, |t, cx| t.finish_edit(cx));
|
||||
self.take_tool_panic()?;
|
||||
return self.apply_actions(acts);
|
||||
}
|
||||
let Some(lender) = self.lender.take_if(|t| t.editing()) else { return Ok(vec![]) };
|
||||
let lent = std::mem::replace(&mut self.tool, lender);
|
||||
let acts = self.with_tool_cx(self.last_view, |t, cx| t.finish_edit(cx));
|
||||
let r = self.take_tool_panic().and_then(|()| self.apply_actions(acts));
|
||||
self.lender = Some(std::mem::replace(&mut self.tool, lent));
|
||||
r
|
||||
}
|
||||
|
||||
/// A key for the active tool. While the tool computes an edit on another thread, the key is held
|
||||
/// and made once the edit is applied, after the input held before it.
|
||||
pub fn tool_key(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
let n = self.next_input();
|
||||
if self.tool_editing() {
|
||||
self.held_input.push_back((n, HeldInput::Key(key, mods), view));
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let r = self.tool_key_now(key, mods, view);
|
||||
self.note_edit_start(n);
|
||||
r
|
||||
}
|
||||
|
||||
fn tool_key_now(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result<Vec<UiRequest>> {
|
||||
// 1–4 pick the active perspective plane while the grid is shown.
|
||||
if let Some(plane) = self.plane_key(key) {
|
||||
self.execute("perspective.plane.set", &serde_json::json!({ "plane": plane.id() }))?;
|
||||
@@ -837,7 +1093,8 @@ impl Session {
|
||||
/// it is on, makes the drawing tools' a crosshair, [`Cursor::precise`]). With Cmd held, the pointer of the selection tool a
|
||||
/// press would borrow ([`Self::lend_selection_tool`]).
|
||||
pub fn cursor(&mut self, p: Point, mods: Mods, view: ViewInfo) -> Cursor {
|
||||
let lent = if mods.cmd && self.lender.is_none() { self.lent_tool(self.tool.id(), &mods) } else { None };
|
||||
// While an edit is computed, a Cmd press is held for the active tool, which shows it is busy.
|
||||
let lent = if mods.cmd && self.lender.is_none() && !self.tool_editing() { self.lent_tool(self.tool.id(), &mods) } else { None };
|
||||
let c = match lent {
|
||||
Some(id) => {
|
||||
let lent = self.make_tool(id);
|
||||
@@ -860,6 +1117,9 @@ impl Session {
|
||||
self.tool.options()
|
||||
}
|
||||
pub fn set_tool_option(&mut self, key: &str, v: &Value) {
|
||||
// The active tool's edit being computed is applied with the options it was started with. A
|
||||
// failure to apply it was the edit's own, and the option is set all the same.
|
||||
let _ = self.finish_tool_edit_later();
|
||||
if key == "queue" && self.tool.id() == "place" {
|
||||
self.note_place_queue_roots();
|
||||
}
|
||||
@@ -934,6 +1194,10 @@ impl Session {
|
||||
{
|
||||
return Err(format!("unknown tool `{t}`"));
|
||||
}
|
||||
// The active tool's merge being computed is applied with the options it was started with.
|
||||
if tool.is_none_or(|t| t == self.tool.id()) {
|
||||
self.finish_tool_edit_later().map_err(|e| e.to_string())?;
|
||||
}
|
||||
let mut values = p.get("values").and_then(Value::as_object).cloned().unwrap_or_default();
|
||||
if let Some(k) = p.get("key").and_then(Value::as_str) {
|
||||
values.insert(k.to_string(), p.get("value").cloned().unwrap_or(Value::Null));
|
||||
|
||||
Reference in New Issue
Block a user