diff --git a/apps/lightcraft-web/src/web.rs b/apps/lightcraft-web/src/web.rs index 6a4487d09..93c8b434c 100644 --- a/apps/lightcraft-web/src/web.rs +++ b/apps/lightcraft-web/src/web.rs @@ -383,6 +383,8 @@ struct WebApp { lib_dir: String, bench: Option, first_frame_logged: bool, + /// The page title last set (`document.title`), so it is written only when it changes. + title: String, last_save: f64, ui_written: Vec, } @@ -475,6 +477,7 @@ impl WebApp { lib_dir, bench: opts.bench.then(|| Bench::new(origin)), first_frame_logged: false, + title: String::new(), last_save: 0.0, ui_written, } @@ -660,6 +663,14 @@ impl eframe::App for WebApp { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { self.app.ui(ui); + // eframe's web runner ignores `ViewportCommand::Title`: name the open photo in the page title + let title = self.app.page_title(self.files.is_dirty() || self.flushing.get()); + if title != self.title + && let Some(doc) = window().and_then(|w| w.document()) + { + doc.set_title(&title); + self.title = title; + } if !self.first_frame_logged && !self.app.widgets.is_empty() { self.first_frame_logged = true; log::info!("lightcraft first UI frame at {:.0} ms after navigation start", perf_now()); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bb290bd86..a8db64d7d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1418,6 +1418,18 @@ impl LightcraftApp { } } + /// The browser page's title: "[ •] — LightCraft" for the active photo (• while + /// `unsaved`: changes not yet written to storage), else the window title. The web build has no + /// window for `ViewportCommand::Title`; a host embedding it reads this title to tell which + /// document a drop opened and whether it is saved (issue #2397). + pub fn page_title(&self, unsaved: bool) -> String { + let dot = if unsaved { " •" } else { "" }; + match self.session.active().and_then(|id| self.session.catalog.photo(id)) { + Some(p) => format!("{}{dot} — LightCraft", p.file_name), + None => format!("{}{dot}", self.window_title()), + } + } + /// Lay out the whole window. pub fn ui(&mut self, ui: &mut egui::Ui) { i18n::set_language(self.ui.language); diff --git a/crates/ui-egui/src/tests_switch_library.rs b/crates/ui-egui/src/tests_switch_library.rs index d1ad85c96..892744edc 100644 --- a/crates/ui-egui/src/tests_switch_library.rs +++ b/crates/ui-egui/src/tests_switch_library.rs @@ -220,3 +220,22 @@ fn library_names_must_work_as_folder_names_on_every_system() { assert_eq!(app.session.library.as_ref().unwrap().dir, dir.join("Trip 2026")); let _ = std::fs::remove_dir_all(&dir); } + +/// The web build's page title names the active photo (a host embedding the page reads it to know +/// which document a drop opened), with • while changes are not yet in storage (issue #2397). +#[test] +fn page_title_names_the_active_photo_and_unsaved_changes() { + let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), Services::default()); + let ids = app.session.visible_cloned(); + let name = |app: &LightcraftApp, id: lightcraft_catalog::PhotoId| app.session.catalog.photo(id).unwrap().file_name.clone(); + app.run("library.select", json!({"ids": [ids[0].0]})).unwrap(); + assert_eq!(app.page_title(false), format!("{} — LightCraft", name(&app, ids[0]))); + assert_eq!(app.page_title(true), format!("{} • — LightCraft", name(&app, ids[0]))); + app.run("library.select", json!({"ids": [ids[1].0]})).unwrap(); + assert_eq!(app.page_title(false), format!("{} — LightCraft", name(&app, ids[1]))); + // no photo: the window title + let empty = LightcraftApp::new(lightcraft_engine::Session::new(), Services::default()); + assert!(empty.session.active().is_none()); + assert_eq!(empty.page_title(false), "LightCraft"); + assert_eq!(empty.page_title(true), "LightCraft •"); +} diff --git a/docs/web.md b/docs/web.md index b275e837d..10cb1ebdc 100644 --- a/docs/web.md +++ b/docs/web.md @@ -152,6 +152,10 @@ are the same in every version, so serve them with `Cache-Control: no-cache` (not - **Automation.** `await lightcraft.command("library.info", "{}")` runs any engine or UI command by id on the next frame and resolves to the JSON result (`web.stats` reports storage, workers and the render queue). This is how the headless-Chrome checks drive the page. +- **The page title names the open photo**: ` — LightCraft`, with ` •` after the name while + changes are not yet written to browser storage (`LightcraftApp::page_title`). eframe's web runner + ignores the window title the desktop app sets, and a host that embeds the page reads this title to + tell which photo a drop opened and whether it is saved (issue #2397). ## Not yet