Web: the page title names the open photo and unsaved changes (#2397)

eframe's web runner doesn't handle ViewportCommand::Title, so the web
build never named what was open, and a host embedding the page could
only guess whether a dropped file had opened. The page title is now
"<file name> — LightCraft" for the active photo, with " •" after the
name while changes are not yet written to browser storage, and the
window title when no photo is active. It is set only when it changes.
This commit is contained in:
Bruno-Alumn
2026-10-21 00:02:23 +00:00
parent 5fe74557ec
commit 8ef46aa088
4 changed files with 46 additions and 0 deletions
+11
View File
@@ -383,6 +383,8 @@ struct WebApp {
lib_dir: String,
bench: Option<Bench>,
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<u8>,
}
@@ -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());
+12
View File
@@ -1418,6 +1418,18 @@ impl LightcraftApp {
}
}
/// The browser page's title: "<file name>[ •] — 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);
@@ -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 •");
}
+4
View File
@@ -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**: `<file name> — 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