fix: Keep the window draggable while a design opens (#643)

The loading box was a full-window modal whose backdrop covered the custom title bar. It is now a light overlay under the title bar, so dragging, snapping, resizing and the window buttons keep working during open, import and recovery. The timeline rebuild shows feature N of M, and Cancel or Esc stops it at the next feature (solidcraft_doc::progress). Recovery restore now runs off the window thread too. A control or MCP doc.open or import is answered once the file is in and doesn't block frames; edits sent meanwhile wait their turn. The web build still opens in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 06:59:20 -04:00
1 parent 8be0f64f5a
commit 392fae1af4
11 files changed
+734 -125

No files matched your search

+133
View File
@@ -0,0 +1,133 @@
//! Progress and cooperative cancellation for a long evaluation (opening a large design) run
//! off the window thread. The caller wraps the work in [`watch`]; the timeline's evaluation
//! loop reports each feature it reaches and, once [`Progress::cancel`] is called, skips the
//! rest. Nothing changes for evaluations that are not watched.
use std::cell::RefCell;
use std::sync::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
};
/// Shared between the worker evaluating a design and the window showing its progress.
#[derive(Debug, Default)]
pub struct Progress {
cancelled: AtomicBool,
done: AtomicUsize,
total: AtomicUsize,
}
impl Progress {
/// Ask the watched evaluation to stop at its next feature.
pub fn cancel(&self) {
self.cancelled.store(true, Ordering::Relaxed);
}
pub fn is_cancelled(&self) -> bool {
self.cancelled.load(Ordering::Relaxed)
}
/// Features evaluated so far and the timeline's length (0 before evaluation starts).
pub fn features(&self) -> (usize, usize) {
(self.done.load(Ordering::Relaxed), self.total.load(Ordering::Relaxed))
}
}
struct Current {
progress: Arc<Progress>,
/// Nested evaluations (derived designs) don't overwrite the outer timeline's count.
depth: usize,
}
thread_local! {
static CURRENT: RefCell<Option<Current>> = const { RefCell::new(None) };
}
/// Run `f` with `progress` watching every evaluation it makes on this thread.
pub fn watch<R>(progress: Arc<Progress>, f: impl FnOnce() -> R) -> R {
let previous = CURRENT.with(|c| c.replace(Some(Current { progress, depth: 0 })));
struct Restore(Option<Option<Current>>);
impl Drop for Restore {
fn drop(&mut self) {
if let Some(previous) = self.0.take() {
CURRENT.with(|c| *c.borrow_mut() = previous);
}
}
}
let _restore = Restore(Some(previous));
f()
}
/// Entered at the start of an evaluation; the guard leaves it.
pub(crate) struct Evaluation {
outer: bool,
}
pub(crate) fn begin(total: usize) -> Evaluation {
let outer = CURRENT.with(|c| {
let mut c = c.borrow_mut();
let Some(cur) = c.as_mut() else { return false };
cur.depth += 1;
if cur.depth == 1 {
cur.progress.total.store(total, Ordering::Relaxed);
cur.progress.done.store(0, Ordering::Relaxed);
true
} else {
false
}
});
Evaluation { outer }
}
impl Evaluation {
/// Feature `index` is next. True when the watcher asked to stop: skip it.
pub(crate) fn step(&self, index: usize) -> bool {
CURRENT.with(|c| {
let c = c.borrow();
let Some(cur) = c.as_ref() else { return false };
if self.outer {
cur.progress.done.store(index, Ordering::Relaxed);
}
cur.progress.is_cancelled()
})
}
}
impl Drop for Evaluation {
fn drop(&mut self) {
CURRENT.with(|c| {
if let Some(cur) = c.borrow_mut().as_mut() {
cur.depth = cur.depth.saturating_sub(1);
if self.outer {
let total = cur.progress.total.load(Ordering::Relaxed);
cur.progress.done.store(total, Ordering::Relaxed);
}
}
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unwatched_evaluations_never_stop_and_watched_ones_report_and_stop() {
let e = begin(3);
assert!(!e.step(0));
drop(e);
let p = Arc::new(Progress::default());
watch(p.clone(), || {
let e = begin(5);
assert!(!e.step(2));
assert_eq!(p.features(), (2, 5));
{
let inner = begin(100);
assert!(!inner.step(50));
}
assert_eq!(p.features(), (2, 5), "a nested evaluation keeps the outer count");
p.cancel();
assert!(e.step(3));
});
assert_eq!(p.features(), (5, 5));
assert!(!begin(1).step(0), "the watch ends with its scope");
}
}