From 5011ae5896ebba2972a5954addbbedec3ecc6e7f Mon Sep 17 00:00:00 2001 From: jyviko <2501648+jyviko@users.noreply.github.com> Date: Wed, 14 Oct 2026 10:15:41 -0400 Subject: [PATCH] ui: on the web, run a background job once its progress is shown The browser build has no threads, so a background job (opening a file, meshing from a form, solving) ran inline in the click and the page froze before it said what it was doing. Now the work waits until the window has drawn the job's progress, then runs on the window thread in the next frame. Native jobs still start on a worker thread at once. Part of #119 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --- crates/ui-egui/src/files.rs | 70 +++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/crates/ui-egui/src/files.rs b/crates/ui-egui/src/files.rs index af2faaf..b394a34 100644 --- a/crates/ui-egui/src/files.rs +++ b/crates/ui-egui/src/files.rs @@ -179,6 +179,11 @@ pub struct Loading { form: Option<&'static str>, /// The finished session copy and the command's reply. rx: Receiver<(Box, Result)>, + /// The work, while it waits to run on the window thread (the web, which has no threads). + pending: Option, + /// Whether the window has drawn the job's progress: on the web the work runs (and the page + /// stops drawing) only once the user has seen that it runs. + shown: bool, } /// What waits for an answer about unsaved changes. @@ -231,27 +236,45 @@ fn with_extension(path: String, ext: &str) -> String { /// Run `work` off the window thread (inline on the web, which has no threads). fn off_thread(name: String, work: impl FnOnce() + Send + 'static) { - #[cfg(not(target_arch = "wasm32"))] - if let Err(e) = std::thread::Builder::new().name(name.clone()).spawn(work) { - // The work went with the closure: a job then reports that it could not start. - log::warn!("{name}: no worker thread: {e}"); - } - #[cfg(target_arch = "wasm32")] - { - let _ = name; + if let Some(work) = start(name, Box::new(work)) { work(); } } -/// Run `command` on `session` off the window thread. -fn spawn(session: Session, command: &'static str, params: Value) -> Receiver<(Box, Result)> { +/// A background job's work. +type Work = Box; + +/// Start `work` on a worker thread. The web has no threads: the work comes back, to run on the +/// window thread once the window has shown that it runs ([`Loading::shown`]). +fn start(name: String, work: Work) -> Option { + #[cfg(not(target_arch = "wasm32"))] + { + if let Err(e) = std::thread::Builder::new().name(name.clone()).spawn(work) { + // The work went with the closure: a job then reports that it could not start. + log::warn!("{name}: no worker thread: {e}"); + } + None + } + #[cfg(target_arch = "wasm32")] + { + let _ = name; + Some(work) + } +} + +/// Run `command` on `session` off the window thread: the reply's receiver, and the work when it +/// waits to run on the window thread (the web). +fn spawn(session: Session, command: &'static str, params: Value) -> (Receiver<(Box, Result)>, Option) { let (tx, rx) = std::sync::mpsc::channel(); - off_thread(format!("fieldcraft {command}"), move || { - let mut s = session; - let r = s.execute(command, ¶ms).map_err(|e| e.to_string()); - let _ = tx.send((Box::new(s), r)); - }); - rx + let work = start( + format!("fieldcraft {command}"), + Box::new(move || { + let mut s = session; + let r = s.execute(command, ¶ms).map_err(|e| e.to_string()); + let _ = tx.send((Box::new(s), r)); + }), + ); + (rx, work) } impl ModelApp { @@ -281,10 +304,10 @@ impl ModelApp { return false; } let base = if fresh { Session::default() } else { self.session.clone() }; - let rx = spawn(base, command, params); + let (rx, pending) = spawn(base, command, params); self.set_status(format!("{title}…"), false); let fit = fresh || recent.is_some(); - self.loading = Some(Loading { title, done, recent, fit, form: None, rx }); + self.loading = Some(Loading { title, done, recent, fit, form: None, rx, pending, shown: false }); true } @@ -406,7 +429,13 @@ impl ModelApp { /// Take in a finished background job (called every frame); `true` while one is running. pub fn poll_loading(&mut self) -> bool { - let Some(l) = &self.loading else { return false }; + let Some(l) = &mut self.loading else { return false }; + if l.pending.is_some() && !l.shown { + return true; + } + if let Some(work) = l.pending.take() { + work(); + } let outcome = match l.rx.try_recv() { Ok(done) => Some(done), Err(TryRecvError::Empty) => return true, @@ -616,6 +645,9 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) { }); }, ); + if let Some(l) = app.loading.as_mut() { + l.shown = true; + } if cancel { app.cancel_loading(); }