From 09838be9eca467679e8ae0132cc1daeb20004ad3 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:33:43 +0300 Subject: [PATCH] feat: draw a notice for one frame before rebuilding a layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching to a Layout stops the main thread long enough that the compositor decides the application has stopped responding and offers to kill it. The switch itself is not the problem — `layout-switch` reports about 10 ms — it is the frame after it, which rebuilds the whole wire set. That frame now has a cheap one in front of it. `layout_settling` is set when the switch takes, and while it holds, the viewport is replaced by a centered "Preparing layout…" on the canvas background. No shader widget means no scene primitive, so `prepare` has nothing to do and the frame costs nothing. A `window::frames` subscription clears the flag, which is the guarantee this needs: that callback runs once a frame has actually been presented, so the notice is on screen and the compositor has been answered before the expensive frame starts. The deferral is of the *frame*, not the switch. Two callers depend on the switch having completed when it returns — one calls `fit_all` immediately after, another compares `current_layout` — so deferring the switch itself would have broken both. This restarts the compositor's timer immediately before the slow frame and replaces a stale window with something that says what is happening. It does not make the slow frame fast: if the rebuild still exceeds the compositor's timeout the dialog can return, and the durable fix is the 2 s `memo_miss` behind it, which is a separate change. Only `en-US` is validated against at build time; Greek is filled in too since it is in use here, and the rest fall back until translated. Co-Authored-By: Claude Opus 5 --- locales/el-GR/opencadstudio.ftl | 1 + locales/en-US/opencadstudio.ftl | 1 + src/app/mod.rs | 13 +++++++++++ src/app/update/mod.rs | 6 ++++++ src/app/update/viewport.rs | 4 ++++ src/app/view/mod.rs | 38 ++++++++++++++++++++++++++++++++- 6 files changed, 62 insertions(+), 1 deletion(-) 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,