Skip to main content

egui_winit/
window_settings.rs

1use egui::ViewportBuilder;
2
3/// On Mac and Windows, [`WindowSettings::initialize_window`] moves the window to its saved position
4/// after it is created, and only then maximizes it or makes it fullscreen.
5const PLACE_AFTER_CREATION: bool = cfg!(any(target_os = "macos", target_os = "windows"));
6
7/// Position and size of a native window.
8///
9/// Windows and Linux position windows in physical pixels, which are unambiguous
10/// across monitors with different scale factors. Mac positions them in logical points.
11/// We store the position in pixels together with the scale factor it was saved with,
12/// so it can be converted to the logical units winit and Mac expect at restore time.
13#[derive(Clone, Copy, Debug)]
14#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
15pub struct WindowGeometry {
16    /// Position of window frame/titlebar in physical pixels.
17    outer_position_pixels: Option<egui::Pos2>,
18
19    /// Scale factor of the window when it was saved, without egui zoom.
20    ///
21    /// Needed to convert `outer_position_pixels` to logical units, which
22    /// `ViewportBuilder::with_position` and Mac's `set_outer_position` take.
23    /// Stored because at restore time there is no window to ask yet, and the
24    /// monitor winit creates it on may not be the one it was saved on.
25    scale_factor: f32,
26
27    /// Inner size of window in points.
28    inner_size_points: egui::Vec2,
29}
30
31impl WindowGeometry {
32    pub fn from_window(egui_zoom_factor: f32, window: &winit::window::Window) -> Self {
33        let scale_factor = window.scale_factor() as f32;
34        let inner_size = window.inner_size();
35        Self {
36            outer_position_pixels: window
37                .outer_position()
38                .ok()
39                .map(|p| egui::pos2(p.x as f32, p.y as f32)),
40            scale_factor,
41            inner_size_points: egui::vec2(inner_size.width as f32, inner_size.height as f32)
42                / (egui_zoom_factor * scale_factor),
43        }
44    }
45}
46
47/// Can be used to store native window settings (position and size).
48#[derive(Clone, Copy, Debug)]
49#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
50pub struct WindowSettings {
51    /// Position and size when neither maximized nor fullscreen.
52    normal_geometry: WindowGeometry,
53
54    fullscreen: bool,
55
56    maximized: bool,
57}
58
59impl WindowSettings {
60    /// While maximized or fullscreen, keeps `last_normal_geometry`, so the window restores to it.
61    pub fn from_window(
62        egui_zoom_factor: f32,
63        window: &winit::window::Window,
64        last_normal_geometry: Option<WindowGeometry>,
65    ) -> Self {
66        let fullscreen = window.fullscreen().is_some();
67        let maximized = window.is_maximized();
68        let normal_geometry = last_normal_geometry
69            .filter(|_| fullscreen || maximized)
70            .unwrap_or_else(|| WindowGeometry::from_window(egui_zoom_factor, window));
71        Self {
72            normal_geometry,
73            fullscreen,
74            maximized,
75        }
76    }
77
78    pub fn inner_size_points(&self) -> egui::Vec2 {
79        self.normal_geometry.inner_size_points
80    }
81
82    /// Position and size when neither maximized nor fullscreen.
83    pub fn normal_geometry(&self) -> WindowGeometry {
84        self.normal_geometry
85    }
86
87    pub fn initialize_viewport_builder(
88        &self,
89        egui_zoom_factor: f32,
90        mut viewport_builder: ViewportBuilder,
91    ) -> ViewportBuilder {
92        profiling::function_scope!();
93
94        let geometry = &self.normal_geometry;
95
96        if let Some(pos) = geometry.outer_position_pixels {
97            // On Windows, winit converts this back to pixels with the scale factor of the monitor
98            // the window is created on, which can differ from `geometry.scale_factor`.
99            // So on Mac and Windows, `initialize_window` sets the saved position again.
100            viewport_builder =
101                viewport_builder.with_position(pos / (egui_zoom_factor * geometry.scale_factor));
102        }
103
104        viewport_builder = viewport_builder.with_inner_size(geometry.inner_size_points);
105        if PLACE_AFTER_CREATION {
106            // Moving a maximized or fullscreen window resets it,
107            // so `initialize_window` maximizes or enters fullscreen after moving.
108            viewport_builder
109                .with_fullscreen(false)
110                .with_maximized(false)
111        } else {
112            viewport_builder
113                .with_fullscreen(self.fullscreen)
114                .with_maximized(self.maximized)
115        }
116    }
117
118    pub fn initialize_window(&self, window: &winit::window::Window) {
119        if !PLACE_AFTER_CREATION {
120            return;
121        }
122
123        // The position from `initialize_viewport_builder` can be off:
124        // on Windows, winit converts it to pixels with the scale factor of the monitor the window
125        // was created on, not the one we saved it on.
126        // On Mac, windows were sometimes not restored onto secondary monitors from it alone.
127        // So set the saved position directly, in the unit each platform positions windows in:
128        let geometry = &self.normal_geometry;
129        if let Some(pos) = geometry.outer_position_pixels {
130            if cfg!(target_os = "macos") {
131                // Mac places windows in points. Pixels are ambiguous across monitors with different scale factors.
132                let pos = pos / geometry.scale_factor;
133                #[expect(
134                    clippy::disallowed_types,
135                    reason = "these are OS points, unaffected by egui zoom"
136                )]
137                window.set_outer_position(winit::dpi::LogicalPosition::new(pos.x, pos.y));
138            } else {
139                window.set_outer_position(winit::dpi::PhysicalPosition::new(pos.x, pos.y));
140            }
141        }
142
143        if self.fullscreen && cfg!(target_os = "macos") {
144            // Mac only enters fullscreen once the window is visible, animating from its current frame.
145            // Maximize so the first frame fills the screen,
146            // and enter fullscreen in `before_first_show`.
147            window.set_maximized(true);
148        } else if self.fullscreen {
149            window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
150        } else if self.maximized {
151            crate::set_maximized(window, true);
152        }
153    }
154
155    /// Call once the first frame is painted, right before showing the window.
156    pub fn before_first_show(&self, window: &winit::window::Window) {
157        if cfg!(target_os = "macos") && self.fullscreen {
158            // Enter the fullscreen deferred by `initialize_window`.
159            // Un-maximize first, so the window leaves fullscreen to its normal frame:
160            window.set_maximized(false);
161            window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
162        }
163    }
164
165    /// Call right after showing the window for the first time.
166    pub fn after_first_show(&self, window: &winit::window::Window) {
167        // X11 window managers ignore maximize and fullscreen requests for hidden windows,
168        // so the ones from `initialize_viewport_builder` are lost:
169        if is_x11(window) {
170            if self.fullscreen {
171                window.set_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
172            } else if self.maximized {
173                window.set_maximized(true);
174            }
175        }
176    }
177
178    pub fn clamp_size_to_sane_values(&mut self, largest_monitor_size_points: egui::Vec2) {
179        use egui::NumExt as _;
180
181        let size = &mut self.normal_geometry.inner_size_points;
182
183        // Prevent ridiculously small windows:
184        let min_size = egui::Vec2::splat(64.0);
185        *size = size.at_least(min_size);
186
187        // Make sure we don't try to create a window larger than the largest monitor
188        // because on Linux that can lead to a crash.
189        *size = size.at_most(largest_monitor_size_points);
190    }
191
192    pub fn clamp_position_to_monitors(
193        &mut self,
194        egui_zoom_factor: f32,
195        event_loop: &winit::event_loop::ActiveEventLoop,
196    ) {
197        // If the app last ran on two monitors and only one is now connected, then
198        // the given position is invalid.
199        // If this happens on Mac, a maximized window gets zero size, so we drop the position.
200        // If this happens on Windows, the window becomes invisible to the user 🤦‍♂️
201        // So on Windows we clamp the position to the monitor it is on.
202        let geometry = &mut self.normal_geometry;
203        let Some(pos_px) = geometry.outer_position_pixels else {
204            return;
205        };
206
207        if cfg!(target_os = "macos") {
208            // Pixels mean different points on each Mac monitor, so compare in points:
209            let pos = pos_px / geometry.scale_factor;
210            let is_on_a_monitor = event_loop.available_monitors().any(|monitor| {
211                (monitor_rect_px(&monitor) / monitor.scale_factor() as f32).contains(pos)
212            });
213            if !is_on_a_monitor {
214                geometry.outer_position_pixels = None;
215            }
216        } else if cfg!(target_os = "windows") {
217            let mut pos_px = pos_px;
218            clamp_pos_to_monitors(
219                egui_zoom_factor,
220                event_loop,
221                geometry.inner_size_points,
222                &mut pos_px,
223            );
224            geometry.outer_position_pixels = Some(pos_px);
225        }
226    }
227}
228
229#[cfg(feature = "x11")]
230fn is_x11(window: &winit::window::Window) -> bool {
231    use raw_window_handle::{HasWindowHandle as _, RawWindowHandle};
232    matches!(
233        window.window_handle().map(|h| h.as_raw()),
234        Ok(RawWindowHandle::Xlib(_) | RawWindowHandle::Xcb(_))
235    )
236}
237
238#[cfg(not(feature = "x11"))]
239fn is_x11(_window: &winit::window::Window) -> bool {
240    false
241}
242
243fn find_active_monitor(
244    egui_zoom_factor: f32,
245    event_loop: &winit::event_loop::ActiveEventLoop,
246    window_size_pts: egui::Vec2,
247    position_px: &egui::Pos2,
248) -> Option<winit::monitor::MonitorHandle> {
249    profiling::function_scope!();
250    let monitors = event_loop.available_monitors();
251
252    // default to primary monitor, in case the correct monitor was disconnected.
253    let Some(mut active_monitor) = event_loop
254        .primary_monitor()
255        .or_else(|| event_loop.available_monitors().next())
256    else {
257        return None; // no monitors 🤷
258    };
259
260    let mut active_monitor_overlap = 0.0;
261    for monitor in monitors {
262        let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32);
263        let window_rect = egui::Rect::from_min_size(*position_px, window_size_px);
264        let overlap = window_rect.intersect(monitor_rect_px(&monitor)).area();
265
266        if active_monitor_overlap < overlap {
267            active_monitor = monitor;
268            active_monitor_overlap = overlap;
269        }
270    }
271
272    Some(active_monitor)
273}
274
275fn monitor_rect_px(monitor: &winit::monitor::MonitorHandle) -> egui::Rect {
276    let pos = monitor.position();
277    let size = monitor.size();
278    egui::Rect::from_min_size(
279        egui::pos2(pos.x as f32, pos.y as f32),
280        egui::vec2(size.width as f32, size.height as f32),
281    )
282}
283
284fn clamp_pos_to_monitors(
285    egui_zoom_factor: f32,
286    event_loop: &winit::event_loop::ActiveEventLoop,
287    window_size_pts: egui::Vec2,
288    position_px: &mut egui::Pos2,
289) {
290    profiling::function_scope!();
291
292    let Some(active_monitor) =
293        find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px)
294    else {
295        return; // no monitors 🤷
296    };
297
298    let mut window_size_px =
299        window_size_pts * (egui_zoom_factor * active_monitor.scale_factor() as f32);
300    // Add size of title bar. This is 32 px by default in Win 10/11.
301    if cfg!(target_os = "windows") {
302        window_size_px += egui::Vec2::new(
303            0.0,
304            32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32,
305        );
306    }
307    let monitor_rect = monitor_rect_px(&active_monitor);
308
309    // Window size cannot be negative or the subsequent `clamp` will panic.
310    let window_size = (monitor_rect.size() - window_size_px).max(egui::Vec2::ZERO);
311    // To get the maximum position, we get the rightmost corner of the display, then
312    // subtract the size of the window to get the bottom right most value window.position
313    // can have.
314    *position_px = position_px.clamp(monitor_rect.min, monitor_rect.min + window_size);
315}