Merge pull request #59 from storytold/pr/never-crash-ui
Never crash: ui-egui and the web app
This commit is contained in:
commit
ba5fff320b
|
|
@ -14,6 +14,7 @@
|
|||
//! console (see [`bench`]); `?store=idb` forces the IndexedDB backend, `?store=memory` disables
|
||||
//! persistence and `?workers=0` renders on the main thread.
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod bench;
|
||||
pub mod files;
|
||||
|
|
|
|||
|
|
@ -29,17 +29,18 @@ const LIBRARY_DIR: &str = "library";
|
|||
/// How often view state, UI prefs and the thumbnail index are saved (ms).
|
||||
const SAVE_EVERY_MS: f64 = 1000.0;
|
||||
|
||||
fn window() -> web_sys::Window {
|
||||
web_sys::window().expect("no window")
|
||||
/// The page's window (`None` only outside a browser page, e.g. in a worker).
|
||||
fn window() -> Option<web_sys::Window> {
|
||||
web_sys::window()
|
||||
}
|
||||
|
||||
/// Milliseconds since navigation start.
|
||||
fn perf_now() -> f64 {
|
||||
window().performance().map(|p| p.now()).unwrap_or(0.0)
|
||||
window().and_then(|w| w.performance()).map(|p| p.now()).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
fn set_status(text: &str) {
|
||||
if let Some(el) = window().document().and_then(|d| d.get_element_by_id("lightcraft_status")) {
|
||||
if let Some(el) = window().and_then(|w| w.document()).and_then(|d| d.get_element_by_id("lightcraft_status")) {
|
||||
el.set_text_content(Some(text));
|
||||
}
|
||||
}
|
||||
|
|
@ -59,7 +60,7 @@ struct Options {
|
|||
|
||||
impl Options {
|
||||
fn from_url() -> Options {
|
||||
let q = window().location().search().unwrap_or_default();
|
||||
let q = window().and_then(|w| w.location().search().ok()).unwrap_or_default();
|
||||
let params: Vec<(String, String)> = q
|
||||
.trim_start_matches('?')
|
||||
.split('&')
|
||||
|
|
@ -102,7 +103,7 @@ async fn read_file(originals: Originals, backend: Option<Backend>, file: web_sys
|
|||
|
||||
/// Show the browser's open dialog; picked files are imported asynchronously.
|
||||
fn open_picker(originals: Originals, backend: Option<Backend>, ctx: egui::Context) {
|
||||
let Some(doc) = window().document() else { return };
|
||||
let Some(doc) = window().and_then(|w| w.document()) else { return };
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
input.set_multiple(true);
|
||||
|
|
@ -131,7 +132,7 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
|||
opts.set_type(crate::store::mime_for(name));
|
||||
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&arr, &opts).map_err(e)?;
|
||||
let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(e)?;
|
||||
let doc = window().document().ok_or("no document")?;
|
||||
let doc = window().and_then(|w| w.document()).ok_or("no document")?;
|
||||
let a: web_sys::HtmlAnchorElement = doc.create_element("a").map_err(e)?.unchecked_into();
|
||||
a.set_href(&url);
|
||||
a.set_download(name);
|
||||
|
|
@ -140,7 +141,9 @@ fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
|||
let revoke = Closure::once_into_js(move || {
|
||||
let _ = web_sys::Url::revoke_object_url(&url);
|
||||
});
|
||||
let _ = window().set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 5000);
|
||||
if let Some(w) = window() {
|
||||
let _ = w.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 5000);
|
||||
}
|
||||
log::info!("exported {name} ({} bytes)", bytes.len());
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -315,7 +318,7 @@ impl WebApp {
|
|||
}
|
||||
app.ui = app.ui.sanitized();
|
||||
let n = opts.workers.unwrap_or_else(|| {
|
||||
let cores = window().navigator().hardware_concurrency() as usize;
|
||||
let cores = window().map_or(1, |w| w.navigator().hardware_concurrency() as usize);
|
||||
cores.saturating_sub(1).clamp(1, 4)
|
||||
});
|
||||
let workers =
|
||||
|
|
@ -516,8 +519,10 @@ pub fn start() {
|
|||
log::info!("lightcraft: wasm instantiated at {:.0} ms", perf_now());
|
||||
let opts = Options::from_url();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let Some(canvas) =
|
||||
window().document().and_then(|d| d.get_element_by_id("lightcraft_canvas")).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok())
|
||||
let Some(canvas) = window()
|
||||
.and_then(|w| w.document())
|
||||
.and_then(|d| d.get_element_by_id("lightcraft_canvas"))
|
||||
.and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok())
|
||||
else {
|
||||
log::error!("missing <canvas id=\"lightcraft_canvas\">");
|
||||
return;
|
||||
|
|
@ -527,7 +532,7 @@ pub fn start() {
|
|||
let r = runner.start(canvas, eframe::WebOptions::default(), Box::new(move |cc| Ok(Box::new(WebApp::new(cc, boot))))).await;
|
||||
match r {
|
||||
Ok(()) => {
|
||||
if let Some(el) = window().document().and_then(|d| d.get_element_by_id("lightcraft_loading")) {
|
||||
if let Some(el) = window().and_then(|w| w.document()).and_then(|d| d.get_element_by_id("lightcraft_loading")) {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,7 +214,7 @@ pub fn poll_scan(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
return;
|
||||
}
|
||||
};
|
||||
let task = app.scan.take().expect("scan task");
|
||||
let Some(task) = app.scan.take() else { return };
|
||||
if task.progress.cancel.load(Ordering::Relaxed) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -333,7 +333,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
p["renameStart"] = json!(1 + task.imported);
|
||||
}
|
||||
let r = app.session.execute("library.import", &p);
|
||||
let task = app.import.as_mut().expect("import task");
|
||||
let Some(task) = app.import.as_mut() else { return };
|
||||
task.done += n;
|
||||
match r {
|
||||
Ok(v) => {
|
||||
|
|
@ -354,7 +354,7 @@ pub fn tick(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if !task.queue.is_empty() {
|
||||
return;
|
||||
}
|
||||
let task = app.import.take().expect("import task");
|
||||
let Some(task) = app.import.take() else { return };
|
||||
let steps = app.session.undo.len().saturating_sub(task.undo0);
|
||||
let label = format!("Add {} Photo{}", task.imported, if task.imported == 1 { "" } else { "s" });
|
||||
app.session.merge_undo(steps, &label);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
//! (views, panels, zoom — see [`menus::UI_COMMANDS`]) and forwards everything else to the engine.
|
||||
//! The same entry point serves menus, shortcuts, buttons and the control channel ([`control`]).
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||
|
||||
pub mod control;
|
||||
pub mod export_task;
|
||||
|
|
|
|||
|
|
@ -124,12 +124,16 @@ fn spawn(app: &LightcraftApp, options: &MergeDialog, ids: &[PhotoId], preview: b
|
|||
let (tx, rx) = channel();
|
||||
let (j, p, c) = (job.clone(), progress.clone(), cancel.clone());
|
||||
let work = move || {
|
||||
let r = j.run(&|f, stage| {
|
||||
if let Ok(mut g) = p.lock() {
|
||||
*g = (f, stage.to_string());
|
||||
}
|
||||
!c.load(Ordering::Relaxed)
|
||||
});
|
||||
let run = || {
|
||||
j.run(&|f, stage| {
|
||||
if let Ok(mut g) = p.lock() {
|
||||
*g = (f, stage.to_string());
|
||||
}
|
||||
!c.load(Ordering::Relaxed)
|
||||
})
|
||||
};
|
||||
// a panicking merge reports an error instead of leaving "Merging…" up forever
|
||||
let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)).unwrap_or_else(|_| Err("the merge failed unexpectedly".into()));
|
||||
let _ = tx.send(r);
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
|
@ -220,7 +224,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if let Some(t) = &app.merge.preview_task {
|
||||
match t.rx.try_recv() {
|
||||
Ok(Ok(out)) => {
|
||||
let t = app.merge.preview_task.take().expect("task");
|
||||
let Some(t) = app.merge.preview_task.take() else { return };
|
||||
if let Some(img) = out.preview {
|
||||
let color = Arc::new(egui::ColorImage::from_rgba_unmultiplied([img.width, img.height], &img.as_bytes()));
|
||||
let tex = ctx.load_texture("merge-preview", color.clone(), egui::TextureOptions::LINEAR);
|
||||
|
|
@ -229,7 +233,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
}
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
let t = app.merge.preview_task.take().expect("task");
|
||||
let Some(t) = app.merge.preview_task.take() else { return };
|
||||
if e != "cancelled" {
|
||||
app.merge.error = Some(e);
|
||||
app.merge.failed_options = Some(t.options);
|
||||
|
|
@ -242,7 +246,7 @@ pub fn poll(app: &mut LightcraftApp, ctx: &egui::Context) {
|
|||
if let Some(t) = &app.merge.final_task {
|
||||
match t.rx.try_recv() {
|
||||
Ok(r) => {
|
||||
let t = app.merge.final_task.take().expect("task");
|
||||
let Some(t) = app.merge.final_task.take() else { return };
|
||||
let what = if t.options.command == "merge.panorama" {
|
||||
"Panorama"
|
||||
} else if t.options.command == "merge.hdrPanorama" {
|
||||
|
|
|
|||
Loading…
Reference in New Issue