diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index c97e91a90..6e57b1115 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -167,6 +167,7 @@ clipboard = .paste-to-original-coordinates = Επικόλληση στις αρχικές συντεταγμένες viewport = + .preparing-layout = Προετοιμασία διάταξης… .split-vertical = Κατακόρυφος διαχωρισμός θύρας προβολής .split-horizontal = Οριζόντιος διαχωρισμός θύρας προβολής .move = Μετακίνηση θύρας προβολής diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index d812808aa..f18f782ad 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -167,6 +167,7 @@ clipboard = .paste-to-original-coordinates = Paste to Original Coordinates viewport = + .preparing-layout = Preparing layout… .split-vertical = Split Viewport Vertically .split-horizontal = Split Viewport Horizontally .move = Move Viewport diff --git a/src/app/mod.rs b/src/app/mod.rs index 563b7e0e9..261a32d27 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1055,6 +1055,15 @@ pub(super) struct OpenCADStudio { /// `Some` while a CAD file is loading — drives the modal overlay. /// Cleared when the load finishes, errors, or the user cancels. pub(super) opening: Option, + /// Set the moment a layout switch changes `current_layout`, cleared on the + /// next presented frame. + /// + /// The switch itself costs about 10 ms; the frame after it rebuilds the + /// whole wire set and can block the main thread for seconds. While this is + /// set the viewport is replaced by a notice, so that frame is cheap — which + /// gives the compositor an answer, and the user something other than a + /// stale window, immediately before the expensive one. + pub(super) layout_settling: bool, open_job_serial: u64, /// Last repair or failed-open report shown in the recovery modal. recovery_report: Option, @@ -2283,6 +2292,9 @@ pub enum Message { /// Timer pulse while the cursor is dwelling on a grip; drives the /// dwell-to-popup transition without requiring further mouse motion. GripDwellTick, + /// One frame has been presented since a layout switch; the next may do the + /// expensive rebuild. + LayoutSettled, /// Timer pulse while a rollover hit-test is queued; fires when the /// cursor has been still long enough to safely run the pick. HoverDwellTick, @@ -3484,6 +3496,7 @@ impl OpenCADStudio { print_all_plot_window_prev: None, print_all_plot_setup_prev: None, opening: None, + layout_settling: false, open_job_serial: 0, recovery_report: None, pending_opens: std::collections::VecDeque::new(), diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index bd903e6d5..47ae2acb7 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3045,6 +3045,12 @@ impl OpenCADStudio { Task::none() } + Message::LayoutSettled => { + // The notice frame has been presented, so the compositor has + // had its answer. Let the next frame build the real thing. + self.layout_settling = false; + Task::none() + } Message::GripDwellTick => { let i = self.active_tab; // Reuse the move-time logic — `p` is the last cursor diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 3474330de..5917e7f0f 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -5534,6 +5534,10 @@ impl OpenCADStudio { .layers .sync_with_viewports(&doc_layers, vp_info); let layers_ms = perf_phase.elapsed().as_secs_f64() * 1000.0; + // The switch is cheap; the frame that follows it is not. Draw a notice + // for one frame first, so the compositor gets an answer before the main + // thread disappears into rebuilding the wire set. + self.layout_settling = true; if perf { crate::perf_record!( "[perf] layout-switch from={} to={} total={:.2}ms sync={:.2}ms switch={:.2}ms restore={:.2}ms layers={:.2}ms epoch={}", diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 864496a60..334de7834 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -272,7 +272,33 @@ impl OpenCADStudio { // viewport draws the layout's own geometry (white sheet + entities + // borders) and the floating content viewports blit on top. mark("viewport_3d"); - let viewport_3d: Element<'_, Message> = if tab.is_start { + let viewport_3d: Element<'_, Message> = if self.layout_settling && !tab.is_start { + // One cheap frame between the switch and the rebuild. No shader + // widget means no scene primitive, so `prepare` has nothing to do + // and the event loop turns — which is what stops the compositor + // deciding the application has stopped responding. + let bg = crosshair_background(tab, is_paper); + let lum = 0.299 * bg[0] + 0.587 * bg[1] + 0.114 * bg[2]; + let ink = if lum > 0.5 { + Color::from_rgba(0.0, 0.0, 0.0, 0.55) + } else { + Color::from_rgba(1.0, 1.0, 1.0, 0.55) + }; + container( + text(crate::tr!("viewport", "preparing-layout")) + .size(15) + .color(ink), + ) + .width(Fill) + .height(Fill) + .center_x(Fill) + .center_y(Fill) + .style(move |_| container::Style { + background: Some(Color::from_rgba(bg[0], bg[1], bg[2], bg[3]).into()), + ..container::Style::default() + }) + .into() + } else if tab.is_start { start_page_view( &self.patrons, &self.videos, @@ -2253,6 +2279,15 @@ impl OpenCADStudio { // the mouse perfectly still — `ViewportMove` alone would never // fire again. Auto-stops once the hover clears or the popup is // already open. + // One frame after a layout switch, then stop. `window::frames` fires + // once a frame has actually been presented, which is the guarantee this + // needs: the notice is on screen and the compositor has been answered + // before the expensive frame starts. + let layout_settle = if self.layout_settling { + window::frames().map(|_| Message::LayoutSettled) + } else { + Subscription::none() + }; let grip_dwell = if self.grip_hover.is_some() && self.grip_popup.is_none() { window::frames().map(|_| Message::GripDwellTick) } else { @@ -2478,6 +2513,7 @@ impl OpenCADStudio { control, frames, history_tick, + layout_settle, grip_dwell, hover_dwell, nav_settle,