feat: draw a notice for one frame before rebuilding a layout

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 <[email protected]>
This commit is contained in:
PanosandClaude Opus 5 committed 2026-09-05 18:53:02 +03:00
1 parent d9a1d55f2e
commit 09838be9ec
6 files changed
+62 -1

No files matched your search

+37 -1
View File
@@ -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,