diff --git a/apps/figcraft-web/src/host.rs b/apps/figcraft-web/src/host.rs index 1000c1490..57b67ca85 100644 --- a/apps/figcraft-web/src/host.rs +++ b/apps/figcraft-web/src/host.rs @@ -91,6 +91,11 @@ impl Bridge { let _ = self.host.post_failed(error, None); } + /// The document file the host opened or the last Save As made, if any. + pub fn host_file(&self) -> Option { + self.host_file.borrow().clone() + } + /// Hand a file the app would otherwise download to the host; `false` when nothing was /// delivered. The `saveAs` rule is this module's documentation's. pub fn post_save(&self, path: &str, bytes: &[u8]) -> bool { diff --git a/apps/figcraft-web/src/web.rs b/apps/figcraft-web/src/web.rs index 69042ce06..7da9f8c93 100644 --- a/apps/figcraft-web/src/web.rs +++ b/apps/figcraft-web/src/web.rs @@ -279,6 +279,9 @@ impl Services for WebServices { } } } + fn host_file(&self) -> Option { + self.host.as_ref()?.host_file() + } fn clipboard_set_text(&mut self, s: &str) { figcraft_ui_egui::egui_copy_text(&self.ctx, s); } diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index 3351329fd..1ae5e5679 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -6,12 +6,7 @@ use super::{ impl FigApp { pub fn save_as_dialog(&mut self) { if self.services.downloads() { - if let Some(r) = self.run("file.saveAs", json!({"data": true})) - && self.download(r["name"].as_str().unwrap_or_default(), &r) - { - self.run("file.markSaved", json!({})); - dialogs::file_saved(self, &r); - } + self.save_download(None); return; } let Ok(name) = self.session.doc().map(|d| d.name.clone()) else { return }; @@ -43,6 +38,17 @@ impl FigApp { }); } + /// Save by handing the file over as a download. `keep`: the name a Save of a host-opened + /// document keeps; `None` is a Save As, under the document's own name. + fn save_download(&mut self, keep: Option<&str>) { + if let Some(r) = self.run("file.saveAs", json!({"data": true})) + && self.download(keep.unwrap_or_else(|| r["name"].as_str().unwrap_or_default()), &r) + { + self.run("file.markSaved", json!({})); + dialogs::file_saved(self, &r); + } + } + pub fn save(&mut self) { // A branch tab saves its main file. let tab = figcraft_engine::branches::file_tab(&self.session); @@ -57,6 +63,9 @@ impl FigApp { if self.run("file.save", json!({})).is_some() { self.toast(i18n::tr("Saved")); } + } else if let Some(name) = self.services.host_file().filter(|n| is_document_file(n)) { + // A host-opened document is saved as the file the host gave: the host updates that file. + self.save_download(Some(&name)); } else { self.save_as_dialog(); } @@ -299,3 +308,8 @@ impl FigApp { failed } } + +/// A `.figcraft` file name: the document format a Save writes (a `.fig` is an export copy). +fn is_document_file(name: &str) -> bool { + Path::new(name).extension().and_then(OsStr::to_str).is_some_and(|e| e.eq_ignore_ascii_case("figcraft")) +} diff --git a/crates/ui-egui/src/host_save_tests.rs b/crates/ui-egui/src/host_save_tests.rs new file mode 100644 index 000000000..0e84d8ab5 --- /dev/null +++ b/crates/ui-egui/src/host_save_tests.rs @@ -0,0 +1,60 @@ +//! In a page host the browser hands files to the host: a Save of the document the host opened goes +//! out under the host's file name, a Save As under the document's own name. + +use std::sync::{Arc, Mutex}; + +use figcraft_engine::Session; +use serde_json::json; + +use crate::{FigApp, Services}; + +type Saved = Arc>>; + +/// Services that save by downloads, with a document file the host opened. +struct Host { + got: Saved, + file: Option<&'static str>, +} + +impl Services for Host { + fn downloads(&self) -> bool { + true + } + fn download(&mut self, name: &str, _bytes: &[u8]) -> bool { + self.got.lock().unwrap().push(name.to_string()); + true + } + fn host_file(&self) -> Option { + self.file.map(str::to_string) + } +} + +fn app(file: Option<&'static str>) -> (FigApp, Saved) { + let got = Saved::default(); + let mut app = FigApp::new(Session::empty(), Box::new(Host { got: got.clone(), file })); + app.session.execute("file.new", &json!({"name": "Original"})).unwrap(); + (app, got) +} + +#[test] +fn save_keeps_the_name_the_host_opened() { + let (mut app, got) = app(Some("client.figcraft")); + app.save(); + assert_eq!(got.lock().unwrap().as_slice(), ["client.figcraft"]); +} + +#[test] +fn save_as_uses_the_document_s_own_name() { + let (mut app, got) = app(Some("client.figcraft")); + app.save_as_dialog(); + assert_eq!(got.lock().unwrap().as_slice(), ["Original.figcraft"]); +} + +#[test] +fn without_a_host_file_or_for_a_copy_format_save_is_a_save_as() { + for file in [None, Some("design.json"), Some("export.fig")] { + let (mut app, got) = app(file); + app.save(); + assert_eq!(got.lock().unwrap().as_slice(), ["Original.figcraft"], "{file:?}"); + } +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e9969c106..a7397cee4 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -124,6 +124,8 @@ mod home_recents_tests; #[cfg(test)] mod home_tests; #[cfg(test)] +mod host_save_tests; +#[cfg(test)] mod idle_tests; #[cfg(test)] mod ime_tests; diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index c9f8e22a2..b5a997a85 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -24,6 +24,11 @@ pub trait Services { fn download(&mut self, _name: &str, _bytes: &[u8]) -> bool { false } + /// The document file a host page opened, where the app runs inside one (the browser's host + /// mode): a Save of that document keeps this name instead of the document's own. + fn host_file(&self) -> Option { + None + } /// A folder to write several files into. fn pick_folder(&mut self, _title: &str) -> DialogFuture { Box::pin(std::future::ready(None))