mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
# Conflicts: # Cargo.toml # crates/ui-egui/src/lib.rs
This commit is contained in:
commit
e04ec3da1a
7 files changed
+118
-8
No files matched your search
Generated
+12
@@ -1049,6 +1049,17 @@ dependencies = [
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-quit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=1e6f127844ec61b3b8afdcc2bb76e7cfe470153f#1e6f127844ec61b3b8afdcc2bb76e7cfe470153f"
|
||||
dependencies = [
|
||||
"dispatch2",
|
||||
"egui",
|
||||
"objc2 0.6.5",
|
||||
"objc2-app-kit 0.3.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "craft-ui"
|
||||
version = "0.1.0"
|
||||
@@ -1777,6 +1788,7 @@ name = "effectcraft-ui-egui"
|
||||
version = "0.17.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"craft-quit",
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
"effectcraft-color",
|
||||
|
||||
@@ -14,6 +14,7 @@ repository = "https://github.com/storytold/effectcraft"
|
||||
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", version = "=0.1.0", default-features = false, features = ["read"] }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f", features = ["egui"] }
|
||||
craft-quit = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "1e6f127844ec61b3b8afdcc2bb76e7cfe470153f" }
|
||||
# internal
|
||||
effectcraft-time = { path = "crates/time" }
|
||||
effectcraft-geom = { path = "crates/geom" }
|
||||
|
||||
@@ -361,14 +361,11 @@ fn main() -> eframe::Result {
|
||||
}))
|
||||
}),
|
||||
);
|
||||
match &result {
|
||||
Ok(()) => log::info!("the window closed"),
|
||||
Err(e) => {
|
||||
if on_gl {
|
||||
launch_guard::opengl_failed(effectcraft_host::config_dir().as_deref());
|
||||
}
|
||||
startup_failed(e, !on_gl && cfg!(not(target_os = "macos")));
|
||||
if let Err(e) = &result {
|
||||
if on_gl {
|
||||
launch_guard::opengl_failed(effectcraft_host::config_dir().as_deref());
|
||||
}
|
||||
startup_failed(e, !on_gl && cfg!(not(target_os = "macos")));
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -479,6 +476,9 @@ impl eframe::App for Desktop {
|
||||
}
|
||||
|
||||
fn on_exit(&mut self) {
|
||||
// On macOS the app quits through `-[NSApplication terminate:]`, and the process ends
|
||||
// before `eframe::run_native` returns (#341).
|
||||
log::info!("the window is closing");
|
||||
self.launch.exited();
|
||||
self.window.save(self.app.session.config.as_deref());
|
||||
self.app.on_exit();
|
||||
|
||||
@@ -11,6 +11,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-ui = { workspace = true }
|
||||
craft-quit = { workspace = true, features = ["egui"] }
|
||||
effectcraft-time = { workspace = true }
|
||||
effectcraft-geom = { workspace = true }
|
||||
effectcraft-color = { workspace = true }
|
||||
|
||||
@@ -2219,6 +2219,9 @@ impl EffectcraftApp {
|
||||
self.apply_gpu_failure(ctx);
|
||||
if self.gpu_failure_applied == Some(true) {
|
||||
self.close_after_device_loss(ctx);
|
||||
// On macOS the app quits through `-[NSApplication terminate:]` instead of closing the
|
||||
// window while AppKit's run loop is running (#341).
|
||||
craft_quit::on_close_request(ctx);
|
||||
return;
|
||||
}
|
||||
// Focus comes from the fresh viewport copy; a full pass's `InputState::focused` is only a
|
||||
@@ -2286,7 +2289,9 @@ impl EffectcraftApp {
|
||||
self.command_inbox = Some(rx);
|
||||
}
|
||||
// A close request (the window's button, Quit, `app.quit`) must be answered while hidden.
|
||||
panels::unsaved::on_close_requested(self, ctx);
|
||||
// On macOS a close that nothing held open quits through `-[NSApplication terminate:]`
|
||||
// (#341).
|
||||
panels::unsaved::close_or_quit(self, ctx);
|
||||
let title = panels::unsaved::window_title(self);
|
||||
if title != self.window_title {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone()));
|
||||
|
||||
@@ -58,6 +58,15 @@ pub fn on_close_requested(app: &mut EffectcraftApp, ctx: &egui::Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/// [`on_close_requested`], then on macOS a close that nothing held open quits through
|
||||
/// `-[NSApplication terminate:]` instead: closing the window while AppKit's run loop is running
|
||||
/// crashes on a Mac with a Touch Bar (#341). `on_exit` still runs. Returns whether that quit was
|
||||
/// scheduled; other platforms close the window as before.
|
||||
pub fn close_or_quit(app: &mut EffectcraftApp, ctx: &egui::Context) -> bool {
|
||||
on_close_requested(app, ctx);
|
||||
craft_quit::on_close_request(ctx)
|
||||
}
|
||||
|
||||
/// The project's display name: its file name, or `Untitled Project.ecproj`.
|
||||
pub fn project_name(app: &EffectcraftApp) -> String {
|
||||
app.session
|
||||
@@ -207,6 +216,23 @@ mod tests {
|
||||
out.viewport_output.get(&egui::ViewportId::ROOT).is_some_and(|v| v.commands.contains(&egui::ViewportCommand::CancelClose))
|
||||
}
|
||||
|
||||
/// One frame of the dialogs with the window's close button pressed, through [`close_or_quit`]
|
||||
/// as the app runs it; returns whether the close was canceled and whether the platform quit
|
||||
/// was scheduled.
|
||||
fn close_frame(app: &mut EffectcraftApp, ctx: &egui::Context) -> (bool, bool) {
|
||||
let mut raw = egui::RawInput::default();
|
||||
raw.viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
|
||||
let mut quits = false;
|
||||
let mut out = ctx.run_ui(raw, |ui| {
|
||||
app.auto.begin_frame();
|
||||
quits = close_or_quit(app, ui.ctx());
|
||||
crate::panels::dialogs::show(app, ui.ctx());
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
let canceled = out.viewport_output.get(&egui::ViewportId::ROOT).is_some_and(|v| v.commands.contains(&egui::ViewportCommand::CancelClose));
|
||||
(canceled, quits)
|
||||
}
|
||||
|
||||
fn modified(app: &mut EffectcraftApp) {
|
||||
app.session.execute("comp.new", json!({"name": "Main", "width": 64, "height": 64})).unwrap();
|
||||
assert!(app.session.is_dirty());
|
||||
@@ -340,4 +366,62 @@ mod tests {
|
||||
assert!(!app.session.is_dirty());
|
||||
assert!(!frame(&mut app, &ctx, true));
|
||||
}
|
||||
|
||||
/// Closing the window while AppKit's run loop is running crashes on a Mac with a Touch Bar
|
||||
/// (#341). On macOS the close is canceled and the app quits through
|
||||
/// `-[NSApplication terminate:]`. Other platforms close the window.
|
||||
#[test]
|
||||
fn closing_a_saved_project_quits_through_the_platform_on_macos() {
|
||||
let (mut app, ctx) = app();
|
||||
let macos = cfg!(target_os = "macos");
|
||||
assert_eq!(close_frame(&mut app, &ctx), (macos, macos));
|
||||
assert!(app.dialog.is_none(), "nothing to ask");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn closing_a_modified_project_asks_before_the_platform_quit() {
|
||||
let (mut app, ctx) = app();
|
||||
modified(&mut app);
|
||||
assert_eq!(close_frame(&mut app, &ctx), (true, false), "the prompt holds the window and nothing quits");
|
||||
assert_eq!(app.dialog, Some(Dialog::UnsavedChanges));
|
||||
answer_with(&mut app, &ctx, Answer::Cancel);
|
||||
assert!(app.session.is_dirty() && !app.dialog_state.unsaved.quitting);
|
||||
assert_eq!(close_frame(&mut app, &ctx), (true, false), "asked again after Cancel");
|
||||
answer_with(&mut app, &ctx, Answer::Discard);
|
||||
let macos = cfg!(target_os = "macos");
|
||||
assert_eq!(close_frame(&mut app, &ctx), (macos, macos), "Don't Save quits");
|
||||
}
|
||||
|
||||
/// While the window is minimized or hidden, eframe runs `App::logic` without `App::ui` and
|
||||
/// still handles a close: `logic` asks about a modified project.
|
||||
#[test]
|
||||
fn a_close_while_only_logic_runs_asks_about_a_modified_project() {
|
||||
let (mut app, ctx) = app();
|
||||
modified(&mut app);
|
||||
let mut raw = egui::RawInput::default();
|
||||
raw.viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
|
||||
let mut frame = eframe::Frame::_new_kittest();
|
||||
let out = ctx.run_logic(&raw, |ctx| eframe::App::logic(&mut app, ctx, &mut frame));
|
||||
let canceled = out.viewport_commands.get(&egui::ViewportId::ROOT).is_some_and(|c| c.contains(&egui::ViewportCommand::CancelClose));
|
||||
assert!(canceled, "the close is canceled");
|
||||
assert_eq!(app.dialog, Some(Dialog::UnsavedChanges));
|
||||
}
|
||||
|
||||
/// A file dialog answered in the frame that closes the window runs its command before the
|
||||
/// close decision, so a change it makes is asked about.
|
||||
#[test]
|
||||
fn a_dialog_answer_runs_before_the_close_decision() {
|
||||
let (mut app, ctx) = app();
|
||||
assert!(!app.session.is_dirty());
|
||||
let answer = crate::DialogAnswer { command: "comp.new".into(), key: "name", params: json!({"width": 64, "height": 64}), paths: vec!["Main".into()] };
|
||||
app.dialog_answers.0.send(answer).unwrap();
|
||||
let mut raw = egui::RawInput::default();
|
||||
raw.viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
|
||||
let mut frame = eframe::Frame::_new_kittest();
|
||||
let out = ctx.run_logic(&raw, |ctx| eframe::App::logic(&mut app, ctx, &mut frame));
|
||||
let canceled = out.viewport_commands.get(&egui::ViewportId::ROOT).is_some_and(|c| c.contains(&egui::ViewportCommand::CancelClose));
|
||||
assert!(app.session.is_dirty(), "the answer ran");
|
||||
assert!(canceled, "the close is canceled");
|
||||
assert_eq!(app.dialog, Some(Dialog::UnsavedChanges));
|
||||
}
|
||||
}
|
||||
@@ -115,5 +115,12 @@ fn a_lost_device_draws_nothing_autosaves_and_closes() {
|
||||
let saved = h.state().session.autosave.last_path.clone().expect("unsaved changes auto-saved");
|
||||
assert!(std::path::Path::new(&saved).is_file(), "{saved}");
|
||||
assert_eq!(*told.borrow(), vec![Some(Ok(saved))], "the user is told once, with the recovery copy");
|
||||
// The close arrives: on macOS the app quits through `-[NSApplication terminate:]` instead of
|
||||
// closing the window while AppKit's run loop is running (#341).
|
||||
h.input_mut().viewports.entry(egui::ViewportId::ROOT).or_default().events.push(egui::ViewportEvent::Close);
|
||||
h.step();
|
||||
let kept = h.output().viewport_output.get(&egui::ViewportId::ROOT).is_some_and(|v| v.commands.contains(&egui::ViewportCommand::CancelClose));
|
||||
assert_eq!(kept, cfg!(target_os = "macos"), "the platform quit closes the window on macOS");
|
||||
assert_eq!(told.borrow().len(), 1);
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user