Open and Place: a file read in the background waits for a busy tool only during a drag

A file opened or placed off the UI thread waited while the active tool
reported busy. The Dimension tool also reports busy between its clicks,
so after one click a ready file stayed queued with its spinner until
the dimension was finished or canceled. Files also waited between the
clicks of the Blend tool.

The session now records whether the pointer button is held. A file
waits for a busy tool only while the button is held. A file that
opens between a tool's clicks resets the tool, as a switch of
documents does; a file placed in the active document leaves the
tool's clicks in place. Files still wait while a dialog is open, a
drag is under way or a preview is open, and a tool's preview between
its clicks counts as one (the Flare tool after its center drag, the
Dimension tool's line once the pointer moves after its second click).
This commit is contained in:
dotCooCoo
2026-10-24 02:05:19 +07:00
committed by bflatastic
parent 78213cdf00
commit 2db1ada660
5 changed files with 166 additions and 10 deletions
+4
View File
@@ -1425,6 +1425,9 @@ pub struct Session {
pub(crate) liquify_stroke: Option<Box<cmd::distortcmds::LiquifyStroke>>,
/// A press on the Plane Switching Widget is under way: its drag and release are the widget's.
pub(crate) plane_widget_press: bool,
/// The pointer button is held: the last pointer event was a press or a drag, not a release or
/// a move ([`Session::pointer_held`]).
pub(crate) pointer_held: bool,
/// A guide being dragged out of a ruler ([`Session::ruler_guide`]).
pub(crate) ruler_guide: Option<vectorcraft_tools::rulerguide::NewGuide>,
/// Which selected type overflows its frame, for the markers every tool but the Type tools shows.
@@ -1520,6 +1523,7 @@ impl Session {
envelope_defaults: None,
liquify_stroke: None,
plane_widget_press: false,
pointer_held: false,
ruler_guide: None,
overflow_marks: Default::default(),
font_search: None,
+20
View File
@@ -825,6 +825,26 @@ fn selection_tool_drag_is_one_undo_step() {
assert_eq!(s.doc().unwrap().doc.node(a).unwrap().geometric_bounds().unwrap().x0, 100.0);
}
/// The pointer button is held from a press or a drag until a release or a move. The Dimension
/// tool stays busy after its first click with the button released, waiting for the second one.
#[test]
fn the_pointer_is_held_from_a_press_until_a_release_or_a_move() {
let mut s = session();
let v = ViewInfo::default();
s.select_tool("dimension", v).unwrap();
let mut held = vec![];
for (kind, x) in [(PointerKind::Down, 100.0), (PointerKind::Drag, 100.0), (PointerKind::Up, 100.0)] {
s.pointer(&PointerEvent::new(kind, x, 100.0), v).unwrap();
held.push(s.pointer_held());
}
assert!(s.tool_busy(), "the first point waits for the second");
for (kind, x) in [(PointerKind::Move, 200.0), (PointerKind::Down, 300.0), (PointerKind::Drag, 310.0), (PointerKind::Move, 320.0)] {
s.pointer(&PointerEvent::new(kind, x, 100.0), v).unwrap();
held.push(s.pointer_held());
}
assert_eq!(held, [true, true, false, false, true, true, false]);
}
/// How many steps there are to undo and to redo, and whether the document has unsaved changes.
pub(crate) fn history(s: &Session) -> (usize, usize, bool) {
let st = s.doc().unwrap();
+12
View File
@@ -313,6 +313,11 @@ impl Session {
/// Feed a pointer event to the active tool. Returns requests for the UI.
pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<Vec<UiRequest>> {
self.last_view = view;
match ev.kind {
PointerKind::Down | PointerKind::Drag => self.pointer_held = true,
PointerKind::Up | PointerKind::Move => self.pointer_held = false,
PointerKind::DoubleClick => {}
}
// The Plane Switching Widget takes its clicks whatever the tool.
if let Some(r) = self.plane_widget_pointer(ev, view) {
return r;
@@ -437,6 +442,13 @@ impl Session {
self.tool.busy()
}
/// Is the pointer button held: was the last pointer event a press or a drag, not a release or a
/// move? A tool that is busy while the button is not held is waiting between its clicks (the
/// Dimension tool after its first point).
pub fn pointer_held(&self) -> bool {
self.pointer_held
}
/// Is the active tool moving, scaling or rotating the selection with a drag?
pub fn tool_transforming(&self) -> bool {
self.tool.transforming()
+95 -3
View File
@@ -82,10 +82,14 @@ pub fn start(app: &mut VectorcraftApp, path: &str, bytes: Vec<u8>) -> Result<Val
/// becomes the active one, and the dialog's OK acts on the active document (#1694: Save As
/// options confirmed after another file had finished opening saved that file's art under the
/// first one's name). The files wait, their status line too, until the dialog is closed. Nor while
/// a drag (a tool's interaction, or a tool busy with a drag of its own) is under way: a document
/// that opens cancels it.
/// an interaction is open (a drag, a slider's preview, or a tool's preview between its clicks) or
/// a tool is busy with a drag of its own while the pointer button is held: a document that opens
/// cancels it. A tool that is busy while the button is not held and no interaction is open is
/// waiting between its clicks (the Dimension tool after its first point): the file opens, and the
/// tool starts afresh, as when the user switches documents.
pub fn poll(app: &mut VectorcraftApp) {
if app.ui.dialog.is_some() || app.session.in_interaction() || app.session.tool_busy() {
let s = &app.session;
if app.ui.dialog.is_some() || s.in_interaction() || (s.tool_busy() && s.pointer_held()) {
return;
}
let mut i = 0;
@@ -253,6 +257,94 @@ mod tests {
assert_eq!(names, ["a.png"], "the drag placed its file");
}
/// A file that finishes opening while a preview runs waits for the preview to end, so the
/// preview is not canceled: a slider's preview with the tool idle, a slider's preview while the
/// Dimension tool waits after its first point, and the Dimension tool's line following the
/// pointer after its second point.
#[test]
fn a_file_ready_during_a_preview_waits_for_it_to_end() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let view = vectorcraft_engine::ViewInfo::default();
let click = |app: &mut VectorcraftApp, x: f64| {
for kind in [PointerKind::Down, PointerKind::Up] {
crate::canvas::dispatch(app, &PointerEvent::new(kind, x, 20.0), view);
}
};
for case in ["slider", "slider between clicks", "line"] {
let mut app = VectorcraftApp::new(Session::new(), Services::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
if case != "slider" {
app.select_tool("dimension");
click(&mut app, 20.0);
}
if case == "line" {
click(&mut app, 80.0);
crate::canvas::dispatch(&mut app, &PointerEvent::new(PointerKind::Move, 50.0, 60.0), view);
} else {
app.session.begin_interaction("Preview").unwrap();
}
assert_eq!(app.session.tool_busy(), case != "slider", "{case}");
start(&mut app, "/art/Second.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(120.0, 80.0), false)).unwrap();
let t0 = std::time::Instant::now();
while t0.elapsed().as_millis() < 400 {
poll(&mut app);
std::thread::yield_now();
}
assert!(app.session.documents().len() == 1 && app.session.in_interaction(), "the file waits for the preview: {case}");
if case == "line" {
click(&mut app, 50.0);
} else {
app.session.cancel_interaction().unwrap();
}
let t0 = std::time::Instant::now();
while !app.opening.pending.is_empty() && t0.elapsed().as_secs() < 30 {
poll(&mut app);
std::thread::yield_now();
}
assert_eq!(app.session.documents().len(), 2, "{case}");
}
}
/// A file that finishes opening while a tool waits between its clicks with no preview open (the
/// Dimension tool after its first point, then after its second before the pointer moves) opens
/// without waiting for the next click, and the tool starts afresh in it, as when the user
/// switches documents.
#[test]
fn a_file_ready_between_a_tools_clicks_opens() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let mut app = VectorcraftApp::new(Session::new(), Services::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.select_tool("dimension");
let view = vectorcraft_engine::ViewInfo::default();
let click = |app: &mut VectorcraftApp, x: f64| {
for kind in [PointerKind::Down, PointerKind::Up] {
crate::canvas::dispatch(app, &PointerEvent::new(kind, x, 20.0), view);
}
};
let opened = |app: &mut VectorcraftApp| {
let t0 = std::time::Instant::now();
while !app.opening.pending.is_empty() && t0.elapsed().as_secs() < 30 {
poll(app);
std::thread::yield_now();
}
};
click(&mut app, 20.0);
assert!(app.session.tool_busy() && !app.session.in_interaction(), "the first point waits for the second");
start(&mut app, "/art/Second.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(400.0, 300.0), false)).unwrap();
opened(&mut app);
assert_eq!(app.session.documents().len(), 2, "the file opens after one click");
assert!(!app.session.tool_busy(), "the tool starts afresh");
click(&mut app, 20.0);
click(&mut app, 80.0);
assert!(app.session.tool_busy() && !app.session.in_interaction(), "the second point waits for the line's place");
start(&mut app, "/art/Third.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(120.0, 80.0), false)).unwrap();
opened(&mut app);
assert_eq!(app.session.documents().len(), 3, "the file opens after two clicks");
assert!(!app.session.tool_busy() && !app.session.in_interaction(), "the tool starts afresh");
let second = &app.session.documents()[1].doc;
assert!(second.layers.iter().all(|l| l.children().is_none_or(|c| c.is_empty())), "the unfinished dimension is not made");
}
#[test]
fn a_file_read_off_the_ui_thread_opens_as_one_read_at_once() {
let mut app = VectorcraftApp::new(Session::new(), Services::default());
+35 -7
View File
@@ -153,16 +153,18 @@ fn spawn(
}
/// Place the files that are ready (each frame), in the order they were started: a file read
/// sooner than one started before it waits for that one. Not while a dialog is open or a drag (a
/// tool's interaction, or a tool busy with a drag of its own, such as the place cursor's) is under
/// way, as [`crate::opening::poll`]: a placed file's document becomes the active one and the
/// dialog's OK acts on the active document; a drag in another document is canceled when the
/// active document changes, and a drag in the same one previews from the document as it was
/// before the drag, without the placed art.
/// sooner than one started before it waits for that one. Not while a dialog is open, an
/// interaction is open, or a tool is busy with a drag of its own (the place cursor's) while the
/// pointer button is held, as [`crate::opening::poll`]: a placed file's document becomes the
/// active one and the dialog's OK acts on the active document; a drag in another document is
/// canceled when the active document changes, and a drag or preview in the same one previews from
/// the document as it was before it began, without the placed art. A tool that is busy while the
/// button is not held is waiting between its clicks (the Dimension tool after its first point):
/// the file is placed unless the tool has a preview open (the Flare tool after its center drag).
pub fn poll(app: &mut VectorcraftApp) {
while app.ui.dialog.is_none()
&& !app.session.in_interaction()
&& !app.session.tool_busy()
&& !(app.session.tool_busy() && app.session.pointer_held())
&& let Some(p) = app.placing.pending.first()
{
let r = match p.done.try_recv() {
@@ -606,4 +608,30 @@ mod tests {
assert_eq!(placed_in(0).iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), ["photo.tif"]);
assert_eq!(placed_in(1), [("a.png".to_string(), Some(vectorcraft_geom::Rect::new(20.0, 20.0, 60.0, 60.0)))], "the drag placed its file");
}
/// A file read while a tool waits between its clicks (the Dimension tool after its first point),
/// in the file's document or in another one, is placed without waiting for the next click. In
/// the same document the tool keeps its point; in another one it starts afresh, as when the user
/// switches documents.
#[test]
fn a_file_read_between_a_tools_clicks_is_placed() {
use vectorcraft_tools::{PointerEvent, PointerKind};
let path = saved("clicks", "photo.tif", &picture(Tiff, 60, 40));
for other_document in [false, true] {
let mut app = app(true);
start(&mut app, &json!({"path": path, "at": [300, 200]}), false).unwrap();
if other_document {
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
}
app.select_tool("dimension");
let view = vectorcraft_engine::ViewInfo::default();
for kind in [PointerKind::Down, PointerKind::Up] {
crate::canvas::dispatch(&mut app, &PointerEvent::new(kind, 20.0, 20.0), view);
}
assert!(app.session.tool_busy() && !app.session.in_interaction(), "other document: {other_document}");
placed(&mut app);
assert_eq!(names(&app), ["photo.tif"], "other document: {other_document}");
assert_eq!(app.session.tool_busy(), !other_document, "other document: {other_document}");
}
}
}