mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 20:03:56 +00:00
Merge pull request #6291 from dotCooCoo/fix/update-dialogs-block-input
Updates: the update dialogs hold the keyboard and floating windows, and updates wait for running work
This commit is contained in:
@@ -340,6 +340,14 @@ fn native(app: &VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Gives the main window the keyboard when a floating document's OS window has it.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn focus_main_window(app: &VectorcraftApp, ctx: &egui::Context) {
|
||||
if native(app, ctx) && app.ui.floating.iter().any(|w| w.focused) {
|
||||
ctx.send_viewport_cmd_to(egui::ViewportId::ROOT, egui::ViewportCommand::Focus);
|
||||
}
|
||||
}
|
||||
|
||||
/// The OS window of document window `w`.
|
||||
fn viewport_id(w: &DocWindow) -> egui::ViewportId {
|
||||
egui::ViewportId::from_hash_of(("vectorcraft-document-window", w.uid, w.number))
|
||||
|
||||
@@ -451,6 +451,41 @@ fn closing_an_os_window_keeps_it_while_its_unsaved_document_stays_open() {
|
||||
);
|
||||
}
|
||||
|
||||
/// The update prompt, opening on its own, takes the keyboard once from a document's OS window that
|
||||
/// has it, and leaves the keyboard alone when none of those windows has it.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn the_update_prompt_takes_the_keyboard_from_a_document_window() {
|
||||
let _host = native_host();
|
||||
let ctx = egui::Context::default();
|
||||
ctx.set_embed_viewports(false);
|
||||
let mut app = app_with(2);
|
||||
if !native(&app, &ctx) {
|
||||
// Wayland: the windows are inside the main window.
|
||||
return;
|
||||
}
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
app.run("window.arrange.floatInWindow", json!({})).unwrap();
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
let focus_commands = |app: &mut VectorcraftApp| {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| crate::updates_ui::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
out.viewport_output.get(&egui::ViewportId::ROOT).map_or(0, |v| v.commands.iter().filter(|c| **c == egui::ViewportCommand::Focus).count())
|
||||
};
|
||||
|
||||
app.ui.floating[0].focused = true;
|
||||
assert_eq!(focus_commands(&mut app), 0, "no prompt is open");
|
||||
crate::updates_ui::open_prompt_for_test(&mut app);
|
||||
assert_eq!(focus_commands(&mut app), 1, "the main window gets the keyboard");
|
||||
assert_eq!(focus_commands(&mut app), 0, "once");
|
||||
|
||||
crate::updates_ui::close_dialogs_for_test(&mut app);
|
||||
assert_eq!(focus_commands(&mut app), 0, "the prompt closed");
|
||||
app.ui.floating[0].focused = false;
|
||||
crate::updates_ui::open_prompt_for_test(&mut app);
|
||||
assert_eq!(focus_commands(&mut app), 0, "no document window has the keyboard");
|
||||
}
|
||||
|
||||
/// Where the last frame drew the title of `title`'s tab in the tab strip.
|
||||
fn tab_at(shapes: &[Shape], title: &str) -> Pos2 {
|
||||
shapes
|
||||
|
||||
@@ -109,7 +109,7 @@ pub(crate) fn install_hooks(ctx: &Context) {
|
||||
}
|
||||
|
||||
/// The base eligibility every custom-descriptor consumer shares: a hidden viewport, the Home
|
||||
/// screen, a modal dialog, the palette, a file sheet, an unfocused window, another egui keyboard
|
||||
/// screen, a modal dialog, an update dialog, the palette, a file sheet, an unfocused window, another egui keyboard
|
||||
/// owner or no Type-tool text edit all mean this pass publishes a stock descriptor or none. One
|
||||
/// helper so the pre-UI route test, the post-UI finalizer and the end-of-pass observation cannot
|
||||
/// drift apart; Home in particular is an ineligible route even though the Type tool keeps its
|
||||
@@ -118,6 +118,7 @@ fn pass_eligible(app: &VectorcraftApp, ctx: &Context) -> bool {
|
||||
!app.viewport_hidden(ctx)
|
||||
&& !crate::menus::home_showing(app)
|
||||
&& app.ui.dialog.is_none()
|
||||
&& !crate::updates_ui::blocks_input(app)
|
||||
&& !app.ui.palette_open
|
||||
&& !app.file_sheet_open()
|
||||
&& ctx.input(|input| input.focused)
|
||||
@@ -801,6 +802,31 @@ pub(crate) fn write_output(ctx: &Context, update: Option<ImeContextUpdate>, rect
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
/// The update prompt ends the Type tool's IME route, as a dialog does.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn the_update_prompt_ends_the_type_tools_route() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 80, "text": "Text", "size": 12, "color": "#000000"})).unwrap()["id"].as_u64().unwrap();
|
||||
let view = app.view_info();
|
||||
app.session.select_tool("type", view).unwrap();
|
||||
app.session.set_tool_option("textSelection", &json!({"id": id, "start": 4, "end": 4}));
|
||||
assert!(app.session.tool_wants_text());
|
||||
let ctx = Context::default();
|
||||
let eligible = |app: &VectorcraftApp| {
|
||||
let mut eligible = false;
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| eligible = pass_eligible(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
eligible
|
||||
};
|
||||
assert!(eligible(&app));
|
||||
crate::updates_ui::open_prompt_for_test(&mut app);
|
||||
assert!(!eligible(&app), "no canvas descriptor behind the prompt");
|
||||
}
|
||||
|
||||
/// A changed context generation or IME purpose for the same owner requires a replacement reset,
|
||||
/// while an unchanged descriptor keeps the existing request.
|
||||
|
||||
@@ -1204,8 +1204,8 @@ pub(crate) fn show(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether a modal is open: a dialog, About, the command palette, a native file dialog, or a
|
||||
/// window behind the modal backdrop last frame (export progress, plug-in runs). Floating panels
|
||||
/// Whether a modal is open: a dialog, About, the command palette, a native file dialog, an update
|
||||
/// dialog, or a window behind the modal backdrop last frame (export progress, plug-in runs). Floating panels
|
||||
/// in OS windows of their own take no input meanwhile, as the main window behind it takes none.
|
||||
/// The window level and visibility a palette window takes: hidden under a modal dialog (a click
|
||||
/// must not bring it back over the dialog), at the normal level under a menu or another popup drawn
|
||||
@@ -1260,6 +1260,7 @@ pub(crate) fn modal_open(app: &VectorcraftApp, ctx: &egui::Context) -> bool {
|
||||
|| app.ui.about
|
||||
|| app.ui.palette_open
|
||||
|| app.file_dialog_open()
|
||||
|| crate::updates_ui::blocks_input(app)
|
||||
|| ctx.memory(|m| m.areas().is_visible(&crate::dialogs::modal::backdrop()))
|
||||
}
|
||||
|
||||
|
||||
@@ -642,6 +642,33 @@ fn a_modal_blocks_native_windows_and_takes_the_keyboard_from_them() {
|
||||
assert_eq!(h.app.session.tool_id(), "pen", "the window takes keys again once the modal closes");
|
||||
}
|
||||
|
||||
/// The update prompt and the result window of a check block a floating panel's OS window as any
|
||||
/// modal does.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn the_update_dialogs_block_native_windows_and_take_the_keyboard_from_them() {
|
||||
type Open = fn(&mut Harness);
|
||||
let opens: [(&str, Open); 2] = [
|
||||
("the prompt", |h| crate::updates_ui::open_prompt_for_test(&mut h.app)),
|
||||
("the result window", |h| crate::updates_ui::open_result_window_for_test(&mut h.app, &h.ctx)),
|
||||
];
|
||||
for (name, open) in opens {
|
||||
let mut h = Harness::new();
|
||||
let viewport = h.float("swatches");
|
||||
open(&mut h);
|
||||
h.frames(6);
|
||||
let focus_commands = h.host.borrow().commands.iter().filter(|(id, c)| *id == ViewportId::ROOT && *c == ViewportCommand::Focus).count();
|
||||
assert_eq!(focus_commands, 1, "{name}: the main window gets the keyboard");
|
||||
h.key_in(viewport, Key::P);
|
||||
assert_eq!(h.app.session.tool_id(), "selection", "{name}: a key typed in the blocked window does nothing");
|
||||
|
||||
crate::updates_ui::close_dialogs_for_test(&mut h.app);
|
||||
h.frames(3);
|
||||
h.key_in(viewport, Key::P);
|
||||
assert_eq!(h.app.session.tool_id(), "pen", "{name}: the window takes keys again once the dialog closes");
|
||||
}
|
||||
}
|
||||
|
||||
/// The level and visibility a palette window takes under a dialog or a menu, and otherwise: a
|
||||
/// palette stays out of the way of anything drawn in the main window.
|
||||
#[test]
|
||||
|
||||
@@ -210,6 +210,11 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
crate::ime::controls_without_editor(app, ctx);
|
||||
return;
|
||||
}
|
||||
// The update prompt and the modal result window draw over everything and read Escape themselves.
|
||||
if crate::updates_ui::blocks_input(app) {
|
||||
crate::ime::controls_without_editor(app, ctx);
|
||||
return;
|
||||
}
|
||||
if app.ui.dialog.is_some() || app.ui.palette_open {
|
||||
crate::ime::controls_without_editor(app, ctx);
|
||||
if app.ui.dialog.as_ref().is_some_and(crate::dialogs::revolve_gizmo::active) && crate::dialogs::revolve_gizmo::dragging(ctx) {
|
||||
|
||||
@@ -91,9 +91,9 @@ pub(super) fn follow(app: &mut VectorcraftApp, ctx: &egui::Context, focused: boo
|
||||
// Each pass looks at the key held last; one that goes back hands over to the key under it.
|
||||
loop {
|
||||
let gesture = app.session.tool_busy() || app.session.pointer_held();
|
||||
// A dialog, or a canvas drag that does not reach the tool (a pan, zoom or turn of the view),
|
||||
// defers the decision.
|
||||
let dialog = app.ui.dialog.is_some() || crate::canvas::dragging(ctx);
|
||||
// A dialog, an update dialog, or a canvas drag that does not reach the tool (a pan, zoom or
|
||||
// turn of the view), defers the decision.
|
||||
let dialog = app.ui.dialog.is_some() || crate::updates_ui::blocks_input(app) || crate::canvas::dragging(ctx);
|
||||
let active = app.session.tool_id();
|
||||
let on = app.session.prefs.hold_tool_keys;
|
||||
let Some(held) = app.held_tool.last_mut() else { return };
|
||||
|
||||
@@ -75,6 +75,21 @@ fn pen_app() -> VectorcraftApp {
|
||||
app
|
||||
}
|
||||
|
||||
/// S let go while the update prompt is open leaves Scale chosen until the prompt closes.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn a_tool_key_let_go_behind_the_update_prompt_returns_after_it() {
|
||||
let mut app = pen_app();
|
||||
let keys = Keyboard::new();
|
||||
keys.press_s(&mut app, 0.0);
|
||||
crate::updates_ui::open_prompt_for_test(&mut app);
|
||||
keys.release_s(&mut app, 1.0);
|
||||
assert_eq!(app.session.tool_id(), "scale", "Scale stays behind the prompt");
|
||||
crate::updates_ui::close_dialogs_for_test(&mut app);
|
||||
keys.frame(&mut app, 1.1, vec![], None);
|
||||
assert_eq!(app.session.tool_id(), "pen", "the Pen is back after the prompt");
|
||||
}
|
||||
|
||||
/// S held from 0 s, its repeats at `repeats`, let go at `release`.
|
||||
fn hold_s(app: &mut VectorcraftApp, keys: &Keyboard, repeats: &[f64], release: f64) {
|
||||
keys.press_s(app, 0.0);
|
||||
|
||||
@@ -20,6 +20,9 @@ pub(crate) struct Runtime {
|
||||
/// The context of the last frame, to wake the UI when a check is queued from a menu callback.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
repaint: Option<egui::Context>,
|
||||
/// Whether an update dialog held the input in the last frame.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
was_blocking: bool,
|
||||
/// The web build has no updater; its window only says so.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) manual_window_open: bool,
|
||||
@@ -38,6 +41,8 @@ impl Default for Runtime {
|
||||
check_requested: false,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
repaint: None,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
was_blocking: false,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
manual_window_open: false,
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -103,12 +108,52 @@ fn result_window_shows(status: &Status, prompt_open: bool) -> bool {
|
||||
!prompt_open && !matches!(status, Status::Idle)
|
||||
}
|
||||
|
||||
/// Whether the update prompt or the modal result window is open: keys, tool keys and clipboard
|
||||
/// events then stay out of the documents behind it, and the app's other windows take no input.
|
||||
pub(crate) fn blocks_input(app: &VectorcraftApp) -> bool {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let updater = &app.updates_rt.updater;
|
||||
holds_keyboard(updater.status(), updater.prompt_open())
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
let _ = app;
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The prompt is modal, and so is the result window for every status but [`Status::Updated`], a
|
||||
/// notice that leaves the app usable behind it.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn holds_keyboard(status: &Status, prompt_open: bool) -> bool {
|
||||
prompt_open || !matches!(status, Status::Idle | Status::Updated(_))
|
||||
}
|
||||
|
||||
/// Whether an update may install and close the app: no document holds unsaved work, no save,
|
||||
/// export, file opening, plug-in filter or Image Trace is running, no action is playing, no dialog
|
||||
/// or file dialog is open, and no tool is in the middle of a drag or an edit it computes on
|
||||
/// another thread.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn safe_to_close(app: &VectorcraftApp) -> bool {
|
||||
let session = &app.session;
|
||||
!crate::unsaved::any_dirty(app)
|
||||
&& app.background.jobs.is_empty()
|
||||
&& !crate::opening::long_operation_busy(app)
|
||||
&& app.ui.action_playing.is_none()
|
||||
&& app.ui.dialog.is_none()
|
||||
&& !app.file_dialog_open()
|
||||
&& !session.in_interaction()
|
||||
&& !session.tool_busy()
|
||||
&& !session.tool_editing()
|
||||
}
|
||||
|
||||
pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
// The close for a ready update waits until no document holds unsaved work.
|
||||
let clean = !crate::unsaved::any_dirty(app);
|
||||
match app.updates_rt.updater.show_if(ctx, clean) {
|
||||
// An installation, and the close for a ready update, wait for unsaved and running work.
|
||||
let safe = safe_to_close(app);
|
||||
match app.updates_rt.updater.show_if(ctx, safe) {
|
||||
Action::OpenUrl(url) => app.open_url(&url),
|
||||
Action::Close => ctx.send_viewport_cmd(egui::ViewportCommand::Close),
|
||||
Action::None => {}
|
||||
@@ -124,6 +169,12 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
CheckResultResponse::RestartToUpdate => rt.updater.restart_to_update(),
|
||||
}
|
||||
}
|
||||
// A check at startup can open the prompt while a floating document's window covers it.
|
||||
let blocking = blocks_input(app);
|
||||
if blocking && !app.updates_rt.was_blocking {
|
||||
crate::doc_windows::focus_main_window(app, ctx);
|
||||
}
|
||||
app.updates_rt.was_blocking = blocking;
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
@@ -150,10 +201,109 @@ pub(crate) fn show(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the update prompt for a test in another module.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn open_prompt_for_test(app: &mut VectorcraftApp) {
|
||||
tests::offer(app);
|
||||
}
|
||||
|
||||
/// Opens the result window of a running check for a test in another module.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn open_result_window_for_test(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
app.updates_rt.updater = tests::isolated_updater(env!("CARGO_PKG_VERSION"), "result");
|
||||
app.updates_rt.updater.check_now(ctx);
|
||||
}
|
||||
|
||||
/// Closes the update prompt and the result window for a test in another module.
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn close_dialogs_for_test(app: &mut VectorcraftApp) {
|
||||
app.updates_rt.updater = tests::isolated_updater(env!("CARGO_PKG_VERSION"), "closed");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_tools::{PointerEvent, PointerKind};
|
||||
|
||||
/// The folder that holds `name`'s update records.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn update_root(name: &str) -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!("vectorcraft-updates-{}-{name}", std::process::id()))
|
||||
}
|
||||
|
||||
/// An updater that reads its records and the launcher's pins only under [`update_root`].
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn isolated_updater(version: &str, name: &str) -> Updater {
|
||||
let root = update_root(name);
|
||||
Updater::new(&crate::UPDATE_APP, version)
|
||||
.with_source(std::sync::Arc::new(|| Ok(None)))
|
||||
.with_paths(craft_update::Paths::under(&root))
|
||||
.with_pins_path(root.join("pins.json"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(super) fn offer(app: &mut VectorcraftApp) {
|
||||
app.updates_rt.updater = isolated_updater(env!("CARGO_PKG_VERSION"), "prompt");
|
||||
app.updates_rt.updater.offer(craft_update::Release {
|
||||
tag: "v99.0.0".into(),
|
||||
version: craft_update::parse_version("99.0.0"),
|
||||
name: "VectorCraft 99.0.0".into(),
|
||||
html_url: "https://github.com/storytold/vectorcraft/releases/tag/v99.0.0".into(),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
/// An app with one saved document holding a selected rectangle.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn app_with_saved_object() -> VectorcraftApp {
|
||||
let services = crate::Services { write: Some(Box::new(|_: &str, _: &[u8]| Ok(()))), ..Default::default() };
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("file.new", json!({"width": 200, "height": 200})).unwrap();
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
app.run("file.save", json!({"path": "/docs/saved.vectorcraft"})).unwrap();
|
||||
assert!(!crate::unsaved::any_dirty(&app));
|
||||
app
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn press(key: egui::Key) -> egui::Event {
|
||||
egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }
|
||||
}
|
||||
|
||||
/// One frame in the app's order: the shortcuts read the input, then the update dialogs draw.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec<egui::Event>) {
|
||||
let mut output = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
crate::shortcuts::handle(app, ui.ctx());
|
||||
show(app, ui.ctx());
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// A Copy, the Pen tool's key and Delete, in one frame each: none of them may act.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn assert_keys_do_nothing(app: &mut VectorcraftApp, ctx: &egui::Context) {
|
||||
let tool = app.session.tool_id();
|
||||
let mut reached = Vec::new();
|
||||
frame(app, ctx, vec![egui::Event::Copy]);
|
||||
if !app.session.clipboard.is_empty() {
|
||||
reached.push("Copy");
|
||||
}
|
||||
let tool_key =
|
||||
egui::Event::Key { key: egui::Key::P, physical_key: Some(egui::Key::P), pressed: true, repeat: false, modifiers: egui::Modifiers::NONE };
|
||||
frame(app, ctx, vec![tool_key, egui::Event::Text("p".into())]);
|
||||
if app.session.tool_id() != tool {
|
||||
reached.push("the tool key");
|
||||
}
|
||||
frame(app, ctx, vec![press(egui::Key::Delete)]);
|
||||
if app.session.doc().unwrap().selection.is_empty() {
|
||||
reached.push("Delete");
|
||||
}
|
||||
assert_eq!(reached, Vec::<&str>::new(), "these reached the document");
|
||||
assert!(!crate::unsaved::any_dirty(app), "a key edited the document");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_runtime_is_idle() {
|
||||
@@ -208,4 +358,164 @@ mod tests {
|
||||
output.textures_delta.clear();
|
||||
assert!(app.updates_rt.updater.prompt_open());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn the_prompt_and_the_result_window_hold_the_keyboard_but_the_updated_notice_does_not() {
|
||||
for status in [
|
||||
Status::Checking,
|
||||
Status::UpToDate,
|
||||
Status::Failed("offline".into()),
|
||||
Status::Installing,
|
||||
Status::Ready,
|
||||
Status::Managed,
|
||||
Status::InstallFailed("locked".into()),
|
||||
] {
|
||||
assert!(holds_keyboard(&status, false), "{status:?}");
|
||||
}
|
||||
assert!(!holds_keyboard(&Status::Idle, false));
|
||||
assert!(!holds_keyboard(&Status::Updated("99.0.0".into()), false));
|
||||
assert!(holds_keyboard(&Status::Idle, true), "the prompt");
|
||||
}
|
||||
|
||||
/// The update prompt, drawn: keys stop at it, Escape closes it, and the next key acts again.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn keys_do_not_reach_the_document_behind_the_update_prompt() {
|
||||
let mut app = app_with_saved_object();
|
||||
let ctx = egui::Context::default();
|
||||
offer(&mut app);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert_keys_do_nothing(&mut app, &ctx);
|
||||
assert!(app.updates_rt.updater.prompt_open());
|
||||
|
||||
frame(&mut app, &ctx, vec![press(egui::Key::Escape)]);
|
||||
assert!(!app.updates_rt.updater.prompt_open(), "Escape closes the prompt");
|
||||
assert!(!app.session.doc().unwrap().selection.is_empty(), "Escape stops at the prompt");
|
||||
frame(&mut app, &ctx, vec![press(egui::Key::Delete)]);
|
||||
assert!(crate::unsaved::any_dirty(&app), "Delete acts once the prompt has closed");
|
||||
}
|
||||
|
||||
/// The result window of a check that is still running, drawn: keys stop at it too.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn keys_do_not_reach_the_document_behind_the_check_result_window() {
|
||||
let mut app = app_with_saved_object();
|
||||
let ctx = egui::Context::default();
|
||||
app.updates_rt.updater = isolated_updater("0.0.1", "checking");
|
||||
app.updates_rt.updater.check_now(&ctx);
|
||||
assert_eq!(app.updates_rt.updater.status(), &Status::Checking);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert_keys_do_nothing(&mut app, &ctx);
|
||||
}
|
||||
|
||||
/// The notice of a finished update, drawn: the keys act behind it.
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn keys_reach_the_document_behind_the_updated_notice() {
|
||||
let mut app = app_with_saved_object();
|
||||
let ctx = egui::Context::default();
|
||||
let root = update_root("updated");
|
||||
let records = craft_update::Paths::under(&root).data;
|
||||
std::fs::create_dir_all(&records).unwrap();
|
||||
std::fs::write(records.join("self-update-done.txt"), "v99.0.0").unwrap();
|
||||
app.updates_rt.updater = isolated_updater("99.0.0", "updated");
|
||||
app.updates_rt.updater.check_at_start(&ctx);
|
||||
assert_eq!(app.updates_rt.updater.status(), &Status::Updated("99.0.0".into()));
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, vec![press(egui::Key::Delete)]);
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
assert!(crate::unsaved::any_dirty(&app), "Delete acts behind the notice");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn unsaved_changes_dialogs_and_running_work_hold_an_update_back() {
|
||||
let mut app = app_with_saved_object();
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
app.run("shape.rectangle", json!({"x": 70, "y": 10, "width": 50, "height": 50})).unwrap();
|
||||
assert!(!safe_to_close(&app), "unsaved changes");
|
||||
app.run("file.save", json!({"path": "/docs/saved.vectorcraft"})).unwrap();
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
app.ui.dialog = Some(crate::state::Dialog::new("about", json!({})));
|
||||
assert!(!safe_to_close(&app), "a dialog");
|
||||
app.ui.dialog = None;
|
||||
|
||||
let (shown, _) = crate::tests_picks::off_the_ui_thread(&mut app.services);
|
||||
// The command asks again with the answer: until then it reports that it has none.
|
||||
assert!(app.run("file.open", json!({})).is_err());
|
||||
assert!(app.file_dialog_open());
|
||||
assert!(!safe_to_close(&app), "a file dialog");
|
||||
shown.answer(&[]);
|
||||
crate::picks::poll(&mut app, &egui::Context::default());
|
||||
assert!(!app.file_dialog_open());
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
app.services.write_shared = Some(std::sync::Arc::new(|_: &str, _: &[u8]| Ok(())));
|
||||
let (release, held) = std::sync::mpsc::channel::<()>();
|
||||
let work = move |_: crate::background::Writer| {
|
||||
let _ = held.recv();
|
||||
Ok(Value::Null)
|
||||
};
|
||||
// No label: a job the status bar does not list holds the close back too.
|
||||
crate::background::run(&mut app, true, String::new(), None, work, |_, r| r).unwrap();
|
||||
assert!(!safe_to_close(&app), "a background save");
|
||||
let _ = release.send(());
|
||||
crate::background::wait_all(&mut app);
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
crate::opening::start(&mut app, "/art/Wait.vectorcraft", vectorcraft_format::save(&vectorcraft_doc::Document::new(10.0, 10.0), false))
|
||||
.unwrap();
|
||||
assert!(!safe_to_close(&app), "a file opening");
|
||||
app.opening.pending.clear();
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
use crate::panels::actions::{Action, ActionSet, PlaybackSpeed, play};
|
||||
let step = ("object.move".to_owned(), json!({"dx": 1, "dy": 0}));
|
||||
app.ui.action_sets = vec![ActionSet::new("Test", vec![Action::new("Move", vec![step.clone(), step])])];
|
||||
app.ui.action_playback.speed = PlaybackSpeed::Step;
|
||||
assert_eq!(play(&mut app, 0, 0).unwrap()["pending"], json!(1));
|
||||
app.run("file.save", json!({"path": "/docs/saved.vectorcraft"})).unwrap();
|
||||
crate::background::wait_all(&mut app);
|
||||
assert!(!safe_to_close(&app), "an action playing");
|
||||
app.ui.action_playing = None;
|
||||
assert!(safe_to_close(&app));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn a_gesture_or_a_tools_edit_holds_an_update_back() {
|
||||
let mut app = app_with_saved_object();
|
||||
let view = app.view_info();
|
||||
app.session.begin_interaction("Move").unwrap();
|
||||
assert!(!safe_to_close(&app), "an interaction");
|
||||
app.session.cancel_interaction().unwrap();
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
app.select_tool("selection");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, 150.0, 150.0), view).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Drag, 180.0, 180.0), view).unwrap();
|
||||
assert!(app.session.tool_busy() && !app.session.in_interaction());
|
||||
assert!(!safe_to_close(&app), "a drag with a tool");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, 180.0, 180.0), view).unwrap();
|
||||
assert!(!app.session.tool_busy());
|
||||
assert!(safe_to_close(&app));
|
||||
|
||||
// Enough segments for the merge to run on another thread.
|
||||
for k in 0..40 {
|
||||
app.run("shape.rectangle", json!({"x": 10 * k, "y": 100, "width": 20, "height": 20})).unwrap();
|
||||
}
|
||||
app.run("file.save", json!({"path": "/docs/saved.vectorcraft"})).unwrap();
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
app.select_tool("shapeBuilder");
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Move, 15.0, 110.0), view).unwrap();
|
||||
app.session.tool_poll(view, true).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Down, 15.0, 110.0), view).unwrap();
|
||||
app.session.pointer(&PointerEvent::new(PointerKind::Up, 15.0, 110.0), view).unwrap();
|
||||
assert!(app.session.tool_editing() && !crate::unsaved::any_dirty(&app), "the click starts a merge on another thread");
|
||||
assert!(!safe_to_close(&app), "a tool's edit");
|
||||
crate::unsaved::finish_pending(&mut app);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user