eframe/native/
epi_integration.rs

1//! Common tools used by [`super::glow_integration`] and [`super::wgpu_integration`].
2
3use web_time::Instant;
4
5use std::path::PathBuf;
6use winit::event_loop::ActiveEventLoop;
7
8use raw_window_handle::{HasDisplayHandle as _, HasWindowHandle as _};
9
10use egui::{DeferredViewportUiCallback, ViewportBuilder, ViewportId};
11use egui_winit::{EventResponse, WindowSettings};
12
13use crate::epi;
14
15#[cfg_attr(target_os = "ios", allow(dead_code, unused_variables, unused_mut))]
16pub fn viewport_builder(
17    egui_zoom_factor: f32,
18    event_loop: &ActiveEventLoop,
19    native_options: &mut epi::NativeOptions,
20    window_settings: Option<WindowSettings>,
21) -> ViewportBuilder {
22    profiling::function_scope!();
23
24    let mut viewport_builder = native_options.viewport.clone();
25
26    // On some Linux systems, a window size larger than the monitor causes crashes,
27    // and on Windows the window does not appear at all.
28    let clamp_size_to_monitor_size = viewport_builder.clamp_size_to_monitor_size.unwrap_or(true);
29
30    // Always use the default window size / position on iOS. Trying to restore the previous position
31    // causes the window to be shown too small.
32    #[cfg(not(target_os = "ios"))]
33    let inner_size_points = if let Some(mut window_settings) = window_settings {
34        // Restore pos/size from previous session
35
36        if clamp_size_to_monitor_size {
37            window_settings.clamp_size_to_sane_values(largest_monitor_point_size(
38                egui_zoom_factor,
39                event_loop,
40            ));
41        }
42        window_settings.clamp_position_to_monitors(egui_zoom_factor, event_loop);
43
44        viewport_builder = window_settings.initialize_viewport_builder(
45            egui_zoom_factor,
46            event_loop,
47            viewport_builder,
48        );
49        window_settings.inner_size_points()
50    } else {
51        if let Some(pos) = viewport_builder.position {
52            viewport_builder = viewport_builder.with_position(pos);
53        }
54
55        if clamp_size_to_monitor_size {
56            if let Some(initial_window_size) = viewport_builder.inner_size {
57                let initial_window_size = egui::NumExt::at_most(
58                    initial_window_size,
59                    largest_monitor_point_size(egui_zoom_factor, event_loop),
60                );
61                viewport_builder = viewport_builder.with_inner_size(initial_window_size);
62            }
63        }
64
65        viewport_builder.inner_size
66    };
67
68    #[cfg(not(target_os = "ios"))]
69    if native_options.centered {
70        profiling::scope!("center");
71        if let Some(monitor) = event_loop
72            .primary_monitor()
73            .or_else(|| event_loop.available_monitors().next())
74        {
75            let monitor_size = monitor
76                .size()
77                .to_logical::<f32>(egui_zoom_factor as f64 * monitor.scale_factor());
78            let inner_size = inner_size_points.unwrap_or(egui::Vec2 { x: 800.0, y: 600.0 });
79            if 0.0 < monitor_size.width && 0.0 < monitor_size.height {
80                let x = (monitor_size.width - inner_size.x) / 2.0;
81                let y = (monitor_size.height - inner_size.y) / 2.0;
82                viewport_builder = viewport_builder.with_position([x, y]);
83            }
84        }
85    }
86
87    match std::mem::take(&mut native_options.window_builder) {
88        Some(hook) => hook(viewport_builder),
89        None => viewport_builder,
90    }
91}
92
93pub fn apply_window_settings(
94    window: &winit::window::Window,
95    window_settings: Option<WindowSettings>,
96) {
97    profiling::function_scope!();
98    if let Some(window_settings) = window_settings {
99        window_settings.initialize_window(window);
100    }
101}
102
103#[cfg(not(target_os = "ios"))]
104fn largest_monitor_point_size(egui_zoom_factor: f32, event_loop: &ActiveEventLoop) -> egui::Vec2 {
105    profiling::function_scope!();
106    let mut max_size = egui::Vec2::ZERO;
107
108    let available_monitors = {
109        profiling::scope!("available_monitors");
110        event_loop.available_monitors()
111    };
112
113    for monitor in available_monitors {
114        let size = monitor
115            .size()
116            .to_logical::<f32>(egui_zoom_factor as f64 * monitor.scale_factor());
117        let size = egui::vec2(size.width, size.height);
118        max_size = max_size.max(size);
119    }
120
121    if max_size == egui::Vec2::ZERO {
122        egui::Vec2::splat(16000.0)
123    } else {
124        max_size
125    }
126}
127
128// ----------------------------------------------------------------------------
129
130/// For loading/saving app state and/or egui memory to disk.
131pub fn create_storage(_app_name: &str) -> Option<Box<dyn epi::Storage>> {
132    #[cfg(feature = "persistence")]
133    if let Some(storage) = super::file_storage::FileStorage::from_app_id(_app_name) {
134        return Some(Box::new(storage));
135    }
136    None
137}
138
139#[allow(clippy::unnecessary_wraps)]
140pub fn create_storage_with_file(_file: impl Into<PathBuf>) -> Option<Box<dyn epi::Storage>> {
141    #[cfg(feature = "persistence")]
142    return Some(Box::new(
143        super::file_storage::FileStorage::from_ron_filepath(_file),
144    ));
145    #[cfg(not(feature = "persistence"))]
146    None
147}
148
149// ----------------------------------------------------------------------------
150
151/// Everything needed to make a winit-based integration for [`epi`].
152///
153/// Only one instance per app (not one per viewport).
154pub struct EpiIntegration {
155    pub frame: epi::Frame,
156    last_auto_save: Instant,
157    pub beginning: Instant,
158    is_first_frame: bool,
159    pub egui_ctx: egui::Context,
160    pending_full_output: egui::FullOutput,
161
162    /// When set, it is time to close the native window.
163    close: bool,
164
165    can_drag_window: bool,
166    #[cfg(feature = "persistence")]
167    persist_window: bool,
168    app_icon_setter: super::app_icon::AppTitleIconSetter,
169}
170
171impl EpiIntegration {
172    #[allow(clippy::too_many_arguments)]
173    pub fn new(
174        egui_ctx: egui::Context,
175        window: &winit::window::Window,
176        app_name: &str,
177        native_options: &crate::NativeOptions,
178        storage: Option<Box<dyn epi::Storage>>,
179        #[cfg(feature = "glow")] gl: Option<std::sync::Arc<glow::Context>>,
180        #[cfg(feature = "glow")] glow_register_native_texture: Option<
181            Box<dyn FnMut(glow::Texture) -> egui::TextureId>,
182        >,
183        #[cfg(feature = "wgpu")] wgpu_render_state: Option<egui_wgpu::RenderState>,
184    ) -> Self {
185        let frame = epi::Frame {
186            info: epi::IntegrationInfo { cpu_usage: None },
187            storage,
188            #[cfg(feature = "glow")]
189            gl,
190            #[cfg(feature = "glow")]
191            glow_register_native_texture,
192            #[cfg(feature = "wgpu")]
193            wgpu_render_state,
194            raw_display_handle: window.display_handle().map(|h| h.as_raw()),
195            raw_window_handle: window.window_handle().map(|h| h.as_raw()),
196        };
197
198        let icon = native_options
199            .viewport
200            .icon
201            .clone()
202            .unwrap_or_else(|| std::sync::Arc::new(load_default_egui_icon()));
203
204        let app_icon_setter = super::app_icon::AppTitleIconSetter::new(
205            native_options
206                .viewport
207                .title
208                .clone()
209                .unwrap_or_else(|| app_name.to_owned()),
210            Some(icon),
211        );
212
213        Self {
214            frame,
215            last_auto_save: Instant::now(),
216            egui_ctx,
217            pending_full_output: Default::default(),
218            close: false,
219            can_drag_window: false,
220            #[cfg(feature = "persistence")]
221            persist_window: native_options.persist_window,
222            app_icon_setter,
223            beginning: Instant::now(),
224            is_first_frame: true,
225        }
226    }
227
228    /// If `true`, it is time to close the native window.
229    pub fn should_close(&self) -> bool {
230        self.close
231    }
232
233    pub fn on_window_event(
234        &mut self,
235        window: &winit::window::Window,
236        egui_winit: &mut egui_winit::State,
237        event: &winit::event::WindowEvent,
238    ) -> EventResponse {
239        profiling::function_scope!(egui_winit::short_window_event_description(event));
240
241        use winit::event::{ElementState, MouseButton, WindowEvent};
242
243        if let WindowEvent::MouseInput {
244            button: MouseButton::Left,
245            state: ElementState::Pressed,
246            ..
247        } = event
248        {
249            self.can_drag_window = true;
250        }
251
252        egui_winit.on_window_event(window, event)
253    }
254
255    pub fn pre_update(&mut self) {
256        self.app_icon_setter.update();
257    }
258
259    /// Run user code - this can create immediate viewports, so hold no locks over this!
260    ///
261    /// If `viewport_ui_cb` is None, we are in the root viewport and will call [`crate::App::update`].
262    pub fn update(
263        &mut self,
264        app: &mut dyn epi::App,
265        viewport_ui_cb: Option<&DeferredViewportUiCallback>,
266        mut raw_input: egui::RawInput,
267    ) -> egui::FullOutput {
268        raw_input.time = Some(self.beginning.elapsed().as_secs_f64());
269
270        let close_requested = raw_input.viewport().close_requested();
271
272        app.raw_input_hook(&self.egui_ctx, &mut raw_input);
273
274        let full_output = self.egui_ctx.run(raw_input, |egui_ctx| {
275            if let Some(viewport_ui_cb) = viewport_ui_cb {
276                // Child viewport
277                profiling::scope!("viewport_callback");
278                viewport_ui_cb(egui_ctx);
279            } else {
280                profiling::scope!("App::update");
281                app.update(egui_ctx, &mut self.frame);
282            }
283        });
284
285        let is_root_viewport = viewport_ui_cb.is_none();
286        if is_root_viewport && close_requested {
287            let canceled = full_output.viewport_output[&ViewportId::ROOT]
288                .commands
289                .contains(&egui::ViewportCommand::CancelClose);
290            if canceled {
291                log::debug!("Closing of root viewport canceled with ViewportCommand::CancelClose");
292            } else {
293                log::debug!("Closing root viewport (ViewportCommand::CancelClose was not sent)");
294                self.close = true;
295            }
296        }
297
298        self.pending_full_output.append(full_output);
299        std::mem::take(&mut self.pending_full_output)
300    }
301
302    pub fn report_frame_time(&mut self, seconds: f32) {
303        self.frame.info.cpu_usage = Some(seconds);
304    }
305
306    pub fn post_rendering(&mut self, window: &winit::window::Window) {
307        profiling::function_scope!();
308        if std::mem::take(&mut self.is_first_frame) {
309            // We keep hidden until we've painted something. See https://github.com/emilk/egui/pull/2279
310            window.set_visible(true);
311        }
312    }
313
314    // ------------------------------------------------------------------------
315    // Persistence stuff:
316
317    pub fn maybe_autosave(
318        &mut self,
319        app: &mut dyn epi::App,
320        window: Option<&winit::window::Window>,
321    ) {
322        let now = Instant::now();
323        if now - self.last_auto_save > app.auto_save_interval() {
324            self.save(app, window);
325            self.last_auto_save = now;
326        }
327    }
328
329    #[allow(clippy::unused_self)]
330    pub fn save(&mut self, _app: &mut dyn epi::App, _window: Option<&winit::window::Window>) {
331        #[cfg(feature = "persistence")]
332        if let Some(storage) = self.frame.storage_mut() {
333            profiling::function_scope!();
334
335            if let Some(window) = _window {
336                if self.persist_window {
337                    profiling::scope!("native_window");
338                    epi::set_value(
339                        storage,
340                        STORAGE_WINDOW_KEY,
341                        &WindowSettings::from_window(self.egui_ctx.zoom_factor(), window),
342                    );
343                }
344            }
345            if _app.persist_egui_memory() {
346                profiling::scope!("egui_memory");
347                self.egui_ctx
348                    .memory(|mem| epi::set_value(storage, STORAGE_EGUI_MEMORY_KEY, mem));
349            }
350            {
351                profiling::scope!("App::save");
352                _app.save(storage);
353            }
354
355            profiling::scope!("Storage::flush");
356            storage.flush();
357        }
358    }
359}
360
361fn load_default_egui_icon() -> egui::IconData {
362    profiling::function_scope!();
363    crate::icon_data::from_png_bytes(&include_bytes!("../../data/icon.png")[..]).unwrap()
364}
365
366#[cfg(feature = "persistence")]
367const STORAGE_EGUI_MEMORY_KEY: &str = "egui";
368
369#[cfg(feature = "persistence")]
370const STORAGE_WINDOW_KEY: &str = "window";
371
372pub fn load_window_settings(_storage: Option<&dyn epi::Storage>) -> Option<WindowSettings> {
373    profiling::function_scope!();
374    #[cfg(feature = "persistence")]
375    {
376        epi::get_value(_storage?, STORAGE_WINDOW_KEY)
377    }
378    #[cfg(not(feature = "persistence"))]
379    None
380}
381
382pub fn load_egui_memory(_storage: Option<&dyn epi::Storage>) -> Option<egui::Memory> {
383    profiling::function_scope!();
384    #[cfg(feature = "persistence")]
385    {
386        epi::get_value(_storage?, STORAGE_EGUI_MEMORY_KEY)
387    }
388    #[cfg(not(feature = "persistence"))]
389    None
390}