Live preview: the tool alone, the boolean only on OK, computed off the window thread (#1201)
The preview builds only the tool; the boolean runs once on OK, off the window thread, with a Computing label while it takes a while. Follow-up to #1020.
This commit is contained in:
1 parent
a261aaa841
commit
9935133b56
12 files changed
+343
-136
No files matched your search
+103
-10
@@ -316,6 +316,61 @@ pub struct Dialog {
|
||||
pub then_start: Option<&'static str>,
|
||||
}
|
||||
|
||||
/// OK computing off the window thread: the dialog's commands evaluated on a scratch copy of the
|
||||
/// session, which leaves the feature in the evaluation cache the session shares. Once that is
|
||||
/// in, OK runs them for real and finds the feature there; meanwhile the window stays live and
|
||||
/// the dialog says it is computing.
|
||||
pub struct Applying {
|
||||
pub(crate) done: std::sync::mpsc::Receiver<()>,
|
||||
/// When OK was pressed (ms).
|
||||
pub(crate) started: f64,
|
||||
/// The commands being computed.
|
||||
pub(crate) cmds: Vec<(String, Value)>,
|
||||
}
|
||||
|
||||
impl Applying {
|
||||
/// The evaluation is in (or its worker ended).
|
||||
fn finished(&self) -> bool {
|
||||
!matches!(self.done.try_recv(), Err(std::sync::mpsc::TryRecvError::Empty))
|
||||
}
|
||||
}
|
||||
|
||||
/// How long the desktop app's OK waits for its evaluation before it lets the window go on and
|
||||
/// shows that it is computing ([`SolveApp::apply_off_thread`]): a feature quicker than this
|
||||
/// applies at once, as it always did.
|
||||
pub const APPLY_AT_ONCE: std::time::Duration = std::time::Duration::from_millis(150);
|
||||
|
||||
/// Start OK's evaluation off the window thread, for a feature dialog. `None` when OK should
|
||||
/// just run: not a feature, not asked for ([`SolveApp::apply_off_thread`]), no threads (the
|
||||
/// web), or done within the wait.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn begin_applying(app: &SolveApp, d: &Dialog) -> Option<Applying> {
|
||||
let wait = app.apply_off_thread?;
|
||||
if !d.previews() {
|
||||
return None;
|
||||
}
|
||||
let cmds = apply_commands(app, d).ok()?;
|
||||
let scratch = app.session.scratch();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
let computed = cmds.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("apply".into())
|
||||
.spawn(move || {
|
||||
let _ = scratch.preview(&computed);
|
||||
let _ = tx.send(());
|
||||
})
|
||||
.ok()?;
|
||||
match rx.recv_timeout(wait) {
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Some(Applying { done: rx, started: crate::now_ms(), cmds }),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn begin_applying(_app: &SolveApp, _d: &Dialog) -> Option<Applying> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The commands a dialog's inputs make (or why they make none), its picks, and the design's
|
||||
/// revision (a parameter the commands name can change without changing them).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -1881,6 +1936,10 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let footer_height = ui.spacing().interact_size.y.max(24.0) + 6.0 + ui.spacing().item_spacing.y;
|
||||
let fields_height = (vp.bottom() - anchor.y - 8.0 - vertical_frame - ui.min_rect().height() - footer_height).max(1.0);
|
||||
egui::ScrollArea::vertical().id_salt("feature_fields").min_scrolled_height(fields_height).max_height(fields_height).show(ui, |ui| {
|
||||
// While OK computes, its inputs hold: what is applied is what was asked.
|
||||
if app.applying.is_some() {
|
||||
ui.disable();
|
||||
}
|
||||
// Configuration tables own their column widths and scrolling; the two-column form
|
||||
// grid would squeeze their row names and action buttons into a label-sized cell.
|
||||
if let Kind::Motion(k @ crate::dialogs_motion::Mo::Configurations { .. }) = &mut d.kind {
|
||||
@@ -2616,7 +2675,12 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// the automatic ids of OK and Cancel, and a click pressed in one frame and
|
||||
// released in the next would be lost (an animating preview toggles it often).
|
||||
let (sr, sresp) = ui.allocate_exact_size(vec2(14.0, 14.0), egui::Sense::hover());
|
||||
if app.preview.busy {
|
||||
if let Some(job) = &app.applying {
|
||||
egui::Spinner::new().size(14.0).paint_at(ui, sr);
|
||||
let seconds = ((crate::now_ms() - job.started) / 1000.0).floor().max(0.0);
|
||||
ui.label(RichText::new(crate::i18n::format("dialog.computing", &[("seconds", &format!("{seconds:.0}"))])).color(t.text_dim));
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(100));
|
||||
} else if app.preview.busy {
|
||||
egui::Spinner::new().size(14.0).paint_at(ui, sr);
|
||||
sresp.on_hover_text(tr("Updating the preview"));
|
||||
}
|
||||
@@ -2636,7 +2700,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
.fill(if valid { t.accent } else { Color32::TRANSPARENT })
|
||||
.wrap_mode(egui::TextWrapMode::Extend)
|
||||
.min_size(vec2(ok_width, 24.0));
|
||||
let r = ui.add_enabled(valid, b);
|
||||
let r = ui.add_enabled(valid && app.applying.is_none(), b);
|
||||
crate::scenario::publish_handle(&format!("dialog:{raw_label}"), r.rect.center());
|
||||
if r.clicked() {
|
||||
ok = true;
|
||||
@@ -2661,7 +2725,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let enter_applies = !matches!(d.kind, Kind::Sketch | Kind::Params { .. } | Kind::Measure { .. } | Kind::ConfirmDelete { .. })
|
||||
&& !matches!(&d.kind, Kind::Motion(k) if k.wide());
|
||||
let report_active = app.issue_report.draft.is_some();
|
||||
if !report_active && enter_applies && (enter || canvas_enter || (enter_free && nothing_focused)) {
|
||||
if !report_active && enter_applies && app.applying.is_none() && (enter || canvas_enter || (enter_free && nothing_focused)) {
|
||||
ok = true;
|
||||
}
|
||||
if !report_active && esc && !app.esc_handled {
|
||||
@@ -2682,20 +2746,50 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Err(error) => d.refuse(app, error),
|
||||
}
|
||||
}
|
||||
if ok {
|
||||
if ok && app.applying.is_none() {
|
||||
// Edit Text applies its own way: its live changes and OK are one undo step.
|
||||
let run = match &mut d.kind {
|
||||
Kind::Text(k) => crate::sketch_text::apply(app, k),
|
||||
_ => run_dialog(app, &d),
|
||||
Kind::Text(k) => Some(crate::sketch_text::apply(app, k)),
|
||||
_ => match begin_applying(app, &d) {
|
||||
// Computing: the dialog stays, saying so, until it is in.
|
||||
Some(job) => {
|
||||
app.applying = Some(job);
|
||||
None
|
||||
}
|
||||
None => Some(run_dialog(app, &d)),
|
||||
},
|
||||
};
|
||||
match run {
|
||||
Ok(()) => {
|
||||
Some(Ok(())) => {
|
||||
cancel = true;
|
||||
applied = true;
|
||||
}
|
||||
Err(e) => d.refuse(app, e),
|
||||
Some(Err(e)) => d.refuse(app, e),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
// OK's evaluation is in: run it, the feature taken from the cache. Inputs changed meanwhile
|
||||
// (nothing should change them) are computed in turn, off the window thread too: run here,
|
||||
// they would hold the window for the whole boolean.
|
||||
if app.applying.as_ref().is_some_and(Applying::finished) && !cancel {
|
||||
let computed = app.applying.take().map(|job| job.cmds);
|
||||
let current = apply_commands(app, &d).ok();
|
||||
let again = if current.is_some() && current != computed { begin_applying(app, &d) } else { None };
|
||||
match again {
|
||||
Some(job) => app.applying = Some(job),
|
||||
None => match run_dialog(app, &d) {
|
||||
Ok(()) => {
|
||||
cancel = true;
|
||||
applied = true;
|
||||
}
|
||||
Err(e) => d.refuse(app, e),
|
||||
},
|
||||
}
|
||||
}
|
||||
// Cancel while computing: the result goes (its worker runs on to its end).
|
||||
if cancel && !applied {
|
||||
app.applying = None;
|
||||
}
|
||||
if !keep || cancel {
|
||||
crate::dialogs_motion::closed(app, &mut d, applied);
|
||||
if let Kind::Text(k) = &mut d.kind {
|
||||
@@ -2724,6 +2818,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
|
||||
/// Close the dialog without applying it (Esc, Cancel). An edit puts the timeline marker back.
|
||||
pub fn cancel(app: &mut SolveApp) {
|
||||
app.applying = None;
|
||||
if let Some(mut d) = app.dialog.take() {
|
||||
crate::dialogs_motion::closed(app, &mut d, false);
|
||||
if let Kind::Text(k) = &mut d.kind {
|
||||
@@ -3032,8 +3127,6 @@ fn sels(d: &Dialog, i: usize) -> &[Sel] {
|
||||
|
||||
pub(crate) fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
|
||||
let commands = apply_commands(app, d)?;
|
||||
// The live preview may be computing these very commands: OK takes its result.
|
||||
crate::preview::settle(app, &commands);
|
||||
if let Some((_, marker)) = d.editing {
|
||||
// Record the actual pre-edit timeline state in the command's undo snapshot.
|
||||
roll_marker(app, marker)?;
|
||||
|
||||
Reference in new issue
Block a user