Skip to main content

brep_app/
desktop_loop.rs

1//! The desktop event loop.
2//!
3//! `eframe::run_native` runs the app only under winit's `RedrawRequested`.
4//! On Wayland winit withholds that event while the surface's frame callback
5//! is outstanding (winit 0.30 `wayland/event_loop/mod.rs`, which skips a window
6//! whose callback is `Requested`, with no timeout), and a compositor sends no
7//! callbacks to a window it is not showing (another workspace, a hidden
8//! scratchpad, a covered window under some compositors). An automation command
9//! queued then waited until the user focused the window: the 2026-10-31
10//! desktop report's 29 s initial read. eframe's fallback for invisible windows
11//! does not apply, because Wayland reports neither visibility nor minimisation
12//! (`is_visible`/`is_minimized` return `None`). And once a repaint is
13//! outstanding with no pass run, egui stops calling its repaint callback, so a
14//! later submit did not even wake the loop.
15//!
16//! `run` owns the winit loop instead (`eframe::create_native`) and wraps
17//! eframe's handler in `DesktopLoop`. The automation queue's waker sends the
18//! loop its own event (never forwarded to eframe), and after every batch of
19//! events the loop runs `BrepApp::service_automation` on this, the app's
20//! thread, whenever the queue's head can be dispatched without a frame. Frames
21//! still drain the queue exactly as before; this path draws nothing.
22//!
23//! The loop also keeps an undrawn window idle. eframe arms
24//! `ControlFlow::Poll` whenever it requests a redraw (`check_redraw_requests`)
25//! and disarms it only in the pass that redraw runs, so a redraw the
26//! compositor never delivers leaves the loop polling, a core spinning while
27//! idle. winit wakes the loop itself when a requested redraw is delivered, so
28//! after one polled iteration without a `RedrawRequested`, the loop waits.
29use crate::app::BrepApp;
30use eframe::UserEvent;
31use std::cell::RefCell;
32use std::rc::Rc;
33use std::sync::{Arc, Mutex};
34use std::time::{Duration, Instant};
35use winit::application::ApplicationHandler;
36use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop, EventLoopProxy};
37
38/// Wakes the loop thread (the automation queue's waker).
39pub type Waker = Arc<dyn Fn() + Send + Sync>;
40
41/// The pass number the wake event carries. It is never a real pass, and the
42/// event is never forwarded to eframe, so egui's repaint bookkeeping cannot
43/// see or swallow it.
44const WAKE: u64 = u64::MAX;
45
46/// While work a frame-free dispatch started is still in flight (a background
47/// history run, a store write) and no frame comes, it is serviced at the frame
48/// cadence: the rate a drawn window pumps it. Background runs report
49/// completion only by being polled (`Documents::pump_all`). This paces
50/// completion of accepted work only; dispatch itself is event-driven.
51const SERVICE_PACE: Duration = Duration::from_millis(16);
52
53/// The app, shared on the loop thread between eframe (through [`Shell`]) and
54/// the loop's service step. The two never run at once: winit delivers events
55/// to one handler, sequentially, on this thread.
56#[derive(Clone, Default)]
57pub struct Slot(Rc<RefCell<Option<(Rc<RefCell<BrepApp>>, egui::Context)>>>);
58
59impl Slot {
60    /// Install the app from the eframe creation closure; returns eframe's
61    /// handle to it.
62    pub fn install(&self, app: BrepApp, ctx: &egui::Context) -> Shell {
63        let app = Rc::new(RefCell::new(app));
64        *self.0.borrow_mut() = Some((app.clone(), ctx.clone()));
65        Shell(app)
66    }
67
68    fn get(&self) -> Option<(Rc<RefCell<BrepApp>>, egui::Context)> {
69        self.0.borrow().clone()
70    }
71}
72
73/// eframe's view of the shared app: every [`eframe::App`] method forwards.
74pub struct Shell(Rc<RefCell<BrepApp>>);
75
76impl eframe::App for Shell {
77    fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
78        eframe::App::logic(&mut *self.0.borrow_mut(), ctx, frame)
79    }
80    fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
81        eframe::App::ui(&mut *self.0.borrow_mut(), ui, frame)
82    }
83    fn save(&mut self, storage: &mut dyn eframe::Storage) {
84        eframe::App::save(&mut *self.0.borrow_mut(), storage)
85    }
86    fn on_exit(&mut self) {
87        eframe::App::on_exit(&mut *self.0.borrow_mut())
88    }
89    fn auto_save_interval(&self) -> Duration {
90        eframe::App::auto_save_interval(&*self.0.borrow())
91    }
92    fn clear_color(&self, visuals: &egui::Visuals) -> [f32; 4] {
93        eframe::App::clear_color(&*self.0.borrow(), visuals)
94    }
95    fn persist_egui_memory(&self) -> bool {
96        eframe::App::persist_egui_memory(&*self.0.borrow())
97    }
98    fn raw_input_hook(&mut self, ctx: &egui::Context, raw_input: &mut egui::RawInput) {
99        eframe::App::raw_input_hook(&mut *self.0.borrow_mut(), ctx, raw_input)
100    }
101}
102
103/// A waker that sends the loop its wake event. Safe from any thread; a closed
104/// loop ignores it.
105pub fn waker(proxy: EventLoopProxy<UserEvent>) -> Waker {
106    let proxy = Mutex::new(proxy);
107    Arc::new(move || {
108        let event = UserEvent::RequestRepaint { viewport_id: egui::ViewportId::ROOT, when: Instant::now(), cumulative_pass_nr: WAKE };
109        let _ = proxy.lock().unwrap_or_else(|p| p.into_inner()).send_event(event);
110    })
111}
112
113fn is_wake(event: &UserEvent) -> bool {
114    matches!(event, UserEvent::RequestRepaint { cumulative_pass_nr: WAKE, .. })
115}
116
117/// eframe's winit handler plus the frame-free service step.
118pub struct DesktopLoop<H> {
119    inner: H,
120    slot: Slot,
121    /// Work the last service step started is still in flight.
122    busy: bool,
123    /// The `WaitUntil` deadline the service step armed to pace that work.
124    paced: Option<Instant>,
125    /// The last iteration ended polling (eframe requested a redraw).
126    polled: bool,
127    /// A `RedrawRequested` arrived since the last `about_to_wait`.
128    redrawn: bool,
129}
130
131impl<H: ApplicationHandler<UserEvent>> DesktopLoop<H> {
132    pub fn new(inner: H, slot: Slot) -> Self {
133        DesktopLoop { inner, slot, busy: false, paced: None, polled: false, redrawn: false }
134    }
135
136    /// End eframe's poll for a redraw that did not come within one polled
137    /// iteration (a hidden Wayland window gets none). Stock behaviour while
138    /// redraws are delivered: each one disarms the poll in its own pass.
139    fn unstick_poll(&mut self, event_loop: &ActiveEventLoop) {
140        if event_loop.control_flow() == ControlFlow::Poll {
141            if self.polled && !self.redrawn {
142                event_loop.set_control_flow(ControlFlow::Wait);
143                self.polled = false;
144            } else {
145                self.polled = true;
146            }
147        } else {
148            self.polled = false;
149        }
150        self.redrawn = false;
151    }
152
153    /// One frame-free service step, when the queue's head needs no frame or
154    /// work it started is still in flight.
155    fn service(&mut self, event_loop: &ActiveEventLoop) {
156        #[cfg(feature = "automation")]
157        self.step();
158        #[cfg(not(feature = "automation"))]
159        let _ = &self.slot;
160        // The deadline this step armed last time, if the loop still holds it
161        // (eframe replaces it with its own when it schedules a repaint).
162        let ours = self.paced.take().filter(|at| event_loop.control_flow() == ControlFlow::WaitUntil(*at));
163        if self.busy {
164            let next = Instant::now() + SERVICE_PACE;
165            match event_loop.control_flow() {
166                ControlFlow::Poll => {}
167                // eframe's own earlier wake stands; ours is renewed.
168                ControlFlow::WaitUntil(at) if at <= next && ours.is_none() => {}
169                _ => {
170                    event_loop.set_control_flow(ControlFlow::WaitUntil(next));
171                    self.paced = Some(next);
172                }
173            }
174        } else if ours.is_some() {
175            // Work done: a deadline left in the past would wake the loop at
176            // once, every iteration.
177            event_loop.set_control_flow(ControlFlow::Wait);
178        }
179    }
180
181    #[cfg(feature = "automation")]
182    fn step(&mut self) {
183        let Some((app, ctx)) = self.slot.get() else { return };
184        // Only eframe holds this borrow, and never across a loop callback.
185        let Ok(mut app) = app.try_borrow_mut() else { return };
186        if !self.busy && !app.automation().has_frameless_work() {
187            return;
188        }
189        self.busy = app.service_automation(&ctx);
190    }
191}
192
193impl<H: ApplicationHandler<UserEvent>> ApplicationHandler<UserEvent> for DesktopLoop<H> {
194    fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: winit::event::StartCause) {
195        self.inner.new_events(event_loop, cause);
196    }
197
198    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
199        self.inner.resumed(event_loop);
200    }
201
202    fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
203        // The wake is ours alone; `about_to_wait`, which always follows the
204        // batch it arrived in, services the queue.
205        if !is_wake(&event) {
206            self.inner.user_event(event_loop, event);
207        }
208    }
209
210    fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: winit::window::WindowId, event: winit::event::WindowEvent) {
211        self.redrawn |= matches!(event, winit::event::WindowEvent::RedrawRequested);
212        self.inner.window_event(event_loop, window_id, event);
213    }
214
215    fn device_event(&mut self, event_loop: &ActiveEventLoop, device_id: winit::event::DeviceId, event: winit::event::DeviceEvent) {
216        self.inner.device_event(event_loop, device_id, event);
217    }
218
219    /// After every batch of events, including the batch a wake arrived in
220    /// and the one in which eframe created the app: a command queued before
221    /// the app existed, or before the waker was installed, is found here.
222    fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
223        self.inner.about_to_wait(event_loop);
224        self.unstick_poll(event_loop);
225        if !event_loop.exiting() {
226            self.service(event_loop);
227        }
228    }
229
230    fn suspended(&mut self, event_loop: &ActiveEventLoop) {
231        self.inner.suspended(event_loop);
232    }
233
234    fn exiting(&mut self, event_loop: &ActiveEventLoop) {
235        self.inner.exiting(event_loop);
236    }
237
238    fn memory_warning(&mut self, event_loop: &ActiveEventLoop) {
239        self.inner.memory_warning(event_loop);
240    }
241}
242
243/// What the creation closure is given: eframe's context and the waker to
244/// install on the app's automation queue before anything can submit to it.
245pub type Create<'a> = Box<dyn 'a + FnOnce(&eframe::CreationContext<'_>, Waker) -> Result<BrepApp, String>>;
246
247/// Run the app on a new event loop.
248pub fn run(app_name: &str, options: eframe::NativeOptions, create: Create<'_>) -> Result<(), String> {
249    let event_loop = EventLoop::<UserEvent>::with_user_event().build().map_err(|e| format!("event loop: {e}"))?;
250    run_on(event_loop, app_name, options, create, |eframe| eframe)
251}
252
253/// Run the app on `event_loop`. `wrap` sits between [`DesktopLoop`] and
254/// eframe's handler: the identity in production; a test interposes there to
255/// play a compositor that withholds frames.
256pub fn run_on<'a, H: ApplicationHandler<UserEvent>>(
257    event_loop: EventLoop<UserEvent>,
258    app_name: &str,
259    options: eframe::NativeOptions,
260    create: Create<'a>,
261    wrap: impl FnOnce(eframe::EframeWinitApplication<'a>) -> H,
262) -> Result<(), String> {
263    let slot = Slot::default();
264    let failure: Rc<RefCell<Option<String>>> = Rc::default();
265    let waker = waker(event_loop.create_proxy());
266    let (installer, failed) = (slot.clone(), failure.clone());
267    let creator: eframe::AppCreator<'a> = Box::new(move |cc| match create(cc, waker) {
268        Ok(app) => Ok(Box::new(installer.install(app, &cc.egui_ctx)) as Box<dyn eframe::App>),
269        Err(error) => {
270            *failed.borrow_mut() = Some(error.clone());
271            Err(error.into())
272        }
273    });
274    let eframe = eframe::create_native(app_name, options, creator, &event_loop);
275    let mut handler = DesktopLoop::new(wrap(eframe), slot);
276    event_loop.run_app(&mut handler).map_err(|e| format!("event loop: {e}"))?;
277    let failed = failure.borrow_mut().take();
278    match failed {
279        Some(error) => Err(error),
280        None => Ok(()),
281    }
282}