diff --git a/crates/ui-egui/src/documents.rs b/crates/ui-egui/src/documents.rs index 7ab22a72..be579a94 100644 --- a/crates/ui-egui/src/documents.rs +++ b/crates/ui-egui/src/documents.rs @@ -99,7 +99,7 @@ fn swap_in(app: &mut SolveApp, slot: DocSlot) -> DocSlot { app.cancel_view_animation(); app.viewport.orbit_snap = None; app.viewport.orbit_free = None; - app.preview = crate::preview::PreviewState::default(); + app.preview.reset(); app.viewport.hover = None; app.menu.props = None; crate::sketch_attachment_ui::clear(app); @@ -199,6 +199,24 @@ pub fn new_design(app: &mut SolveApp) { } } +/// The tab strip's +: always a new tab, except over the blank design the start page covers +/// (a launch), which New takes as [`new_design`] does. +pub fn new_tab(app: &mut SolveApp) { + if app.file_picker_busy() { + return; + } + if app.home.open && active_is_blank(app) { + new_design(app); + return; + } + app.home.open = false; + if let Err(e) = open_tab(app, Session::default()) { + app.set_status(e, true); + } else { + crate::prefs::apply_new_design(app); + } +} + /// Before opening a file: a fresh tab unless the active design is blank. The empty design, /// new or already showing, takes Units of new designs so a cancelled open is not millimetres /// when the preference says otherwise. @@ -455,7 +473,7 @@ pub fn tabs(app: &mut SolveApp, ui: &mut egui::Ui, r: Rect, x0: f32, x1: f32) { // A file loading into the window keeps the tabs as they are (dragging still moves it). let idle = !app.file_load.busy(); if presp.on_hover_text("New Design (Ctrl+N)").clicked() && idle { - new_design(app); + new_tab(app); } match (shut, pick) { _ if !idle => {} @@ -703,6 +721,21 @@ mod tests { assert!(active_is_blank(&a) && a.home.open); } + /// The tab strip's + opens a tab every time, an empty design's included; over the start + /// page's blank design (a launch) it uses that design. + #[test] + fn plus_opens_a_tab_every_time() { + let mut a = app(); + a.home.open = true; + new_tab(&mut a); + assert_eq!(a.docs.count(), 1, "the launch's blank design is used"); + assert!(!a.home.open); + new_tab(&mut a); + new_tab(&mut a); + assert_eq!(a.docs.count(), 3); + assert_eq!(a.docs.active, 2); + } + /// Quitting asks once per design with unsaved changes: Don't Save moves on (and keeps no /// recovery copy), Save saves and moves on, and the app quits after the last answer. #[test] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ede551ac..8311be00 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -642,7 +642,7 @@ impl SolveApp { // applying cancellation hooks (such as restoring its timeline marker). self.dialog = None; self.tool = None; - self.preview = preview::PreviewState::default(); + self.preview.reset(); self.cam = Camera::default(); self.fit_view(); } diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs index 25af7b69..f234ab8a 100644 --- a/crates/ui-egui/src/preview.rs +++ b/crates/ui-egui/src/preview.rs @@ -118,6 +118,15 @@ impl PreviewState { } } + /// Drop the preview of a design that leaves the window. Its scene goes too: the viewport + /// keeps drawing the last scene it took until the slot it reads hands it another, so a fresh + /// state (another slot, empty) would leave the old preview drawn over the next design. + pub fn reset(&mut self) { + let (slot, key) = (std::mem::take(&mut self.slot), self.key); + *self = PreviewState { slot, key, ..PreviewState::default() }; + self.set_scene(GpuScene::default()); + } + fn set_scene(&mut self, sc: GpuScene) { if let Ok(mut slot) = self.slot.lock() { *slot = Some(sc); @@ -1052,6 +1061,28 @@ mod tests { pv.poll(4, None); assert!(pv.error.is_none() && pv.shown_key == Some(1), "an old error is dropped"); } + + /// A design leaving the window (another tab) takes its preview off the viewport: an empty + /// scene in the slot the viewport reads, under a new key. A fresh slot left the fillet + /// preview of the old design drawn over the new tab's empty design. + #[test] + fn reset_hands_the_viewport_an_empty_scene() { + let mut pv = PreviewState::default(); + let mut sc = GpuScene::default(); + sc.xray_tri([0.0; 3], [0.0, 0.0, 1.0], [255; 4]); + pv.set_scene(sc); + pv.active = true; + pv.replaced = vec!["Body1".into()]; + let slot = pv.slot.clone(); + let key = pv.key; + // The viewport took the scene. + assert!(slot.lock().unwrap().take().is_some_and(|s| !s.xray.is_empty())); + pv.reset(); + assert!(std::sync::Arc::ptr_eq(&slot, &pv.slot), "the slot the viewport reads"); + assert_ne!(pv.key, key, "a new key: the viewport takes the slot again"); + assert!(slot.lock().unwrap().as_ref().is_some_and(|s| s.xray.is_empty() && s.blocks.is_empty())); + assert!(!pv.active && pv.replaced.is_empty()); + } } #[cfg(test)]