egui_winit/
window_settings.rs

1use egui::ViewportBuilder;
2
3/// Can be used to store native window settings (position and size).
4#[derive(Clone, Copy, Debug, Default)]
5#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
6#[cfg_attr(feature = "serde", serde(default))]
7pub struct WindowSettings {
8    /// Position of window content in physical pixels.
9    inner_position_pixels: Option<egui::Pos2>,
10
11    /// Position of window frame/titlebar in physical pixels.
12    outer_position_pixels: Option<egui::Pos2>,
13
14    fullscreen: bool,
15
16    /// Inner size of window in logical pixels
17    inner_size_points: Option<egui::Vec2>,
18}
19
20impl WindowSettings {
21    pub fn from_window(egui_zoom_factor: f32, window: &winit::window::Window) -> Self {
22        let inner_size_points = window
23            .inner_size()
24            .to_logical::<f32>(egui_zoom_factor as f64 * window.scale_factor());
25
26        let inner_position_pixels = window
27            .inner_position()
28            .ok()
29            .map(|p| egui::pos2(p.x as f32, p.y as f32));
30
31        let outer_position_pixels = window
32            .outer_position()
33            .ok()
34            .map(|p| egui::pos2(p.x as f32, p.y as f32));
35
36        Self {
37            inner_position_pixels,
38            outer_position_pixels,
39
40            fullscreen: window.fullscreen().is_some(),
41
42            inner_size_points: Some(egui::vec2(
43                inner_size_points.width,
44                inner_size_points.height,
45            )),
46        }
47    }
48
49    pub fn inner_size_points(&self) -> Option<egui::Vec2> {
50        self.inner_size_points
51    }
52
53    pub fn initialize_viewport_builder(
54        &self,
55        egui_zoom_factor: f32,
56        event_loop: &winit::event_loop::ActiveEventLoop,
57        mut viewport_builder: ViewportBuilder,
58    ) -> ViewportBuilder {
59        profiling::function_scope!();
60
61        // `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
62        // See [`winit::window::WindowBuilder::with_position`] for details.
63        let pos_px = if cfg!(target_os = "macos") {
64            self.inner_position_pixels
65        } else {
66            self.outer_position_pixels
67        };
68        if let Some(pos) = pos_px {
69            let monitor_scale_factor = if let Some(inner_size_points) = self.inner_size_points {
70                find_active_monitor(egui_zoom_factor, event_loop, inner_size_points, &pos)
71                    .map_or(1.0, |monitor| monitor.scale_factor() as f32)
72            } else {
73                1.0
74            };
75
76            let scaled_pos = pos / (egui_zoom_factor * monitor_scale_factor);
77            viewport_builder = viewport_builder.with_position(scaled_pos);
78        }
79
80        if let Some(inner_size_points) = self.inner_size_points {
81            viewport_builder = viewport_builder
82                .with_inner_size(inner_size_points)
83                .with_fullscreen(self.fullscreen);
84        }
85
86        viewport_builder
87    }
88
89    pub fn initialize_window(&self, window: &winit::window::Window) {
90        if cfg!(target_os = "macos") {
91            // Mac sometimes has problems restoring the window to secondary monitors
92            // using only `WindowBuilder::with_position`, so we need this extra step:
93            if let Some(pos) = self.outer_position_pixels {
94                window.set_outer_position(winit::dpi::PhysicalPosition { x: pos.x, y: pos.y });
95            }
96        }
97    }
98
99    pub fn clamp_size_to_sane_values(&mut self, largest_monitor_size_points: egui::Vec2) {
100        use egui::NumExt as _;
101
102        if let Some(size) = &mut self.inner_size_points {
103            // Prevent ridiculously small windows:
104            let min_size = egui::Vec2::splat(64.0);
105            *size = size.at_least(min_size);
106
107            // Make sure we don't try to create a window larger than the largest monitor
108            // because on Linux that can lead to a crash.
109            *size = size.at_most(largest_monitor_size_points);
110        }
111    }
112
113    pub fn clamp_position_to_monitors(
114        &mut self,
115        egui_zoom_factor: f32,
116        event_loop: &winit::event_loop::ActiveEventLoop,
117    ) {
118        // If the app last ran on two monitors and only one is now connected, then
119        // the given position is invalid.
120        // If this happens on Mac, the window is clamped into valid area.
121        // If this happens on Windows, the window becomes invisible to the user 🤦‍♂️
122        // So on Windows we clamp the position to the monitor it is on.
123        if !cfg!(target_os = "windows") {
124            return;
125        }
126
127        let Some(inner_size_points) = self.inner_size_points else {
128            return;
129        };
130
131        if let Some(pos_px) = &mut self.inner_position_pixels {
132            clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px);
133        }
134        if let Some(pos_px) = &mut self.outer_position_pixels {
135            clamp_pos_to_monitors(egui_zoom_factor, event_loop, inner_size_points, pos_px);
136        }
137    }
138}
139
140fn find_active_monitor(
141    egui_zoom_factor: f32,
142    event_loop: &winit::event_loop::ActiveEventLoop,
143    window_size_pts: egui::Vec2,
144    position_px: &egui::Pos2,
145) -> Option<winit::monitor::MonitorHandle> {
146    profiling::function_scope!();
147    let monitors = event_loop.available_monitors();
148
149    // default to primary monitor, in case the correct monitor was disconnected.
150    let Some(mut active_monitor) = event_loop
151        .primary_monitor()
152        .or_else(|| event_loop.available_monitors().next())
153    else {
154        return None; // no monitors 🤷
155    };
156
157    for monitor in monitors {
158        let window_size_px = window_size_pts * (egui_zoom_factor * monitor.scale_factor() as f32);
159        let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
160            ..(monitor.position().x + monitor.size().width as i32);
161        let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
162            ..(monitor.position().y + monitor.size().height as i32);
163
164        if monitor_x_range.contains(&(position_px.x as i32))
165            && monitor_y_range.contains(&(position_px.y as i32))
166        {
167            active_monitor = monitor;
168        }
169    }
170
171    Some(active_monitor)
172}
173
174fn clamp_pos_to_monitors(
175    egui_zoom_factor: f32,
176    event_loop: &winit::event_loop::ActiveEventLoop,
177    window_size_pts: egui::Vec2,
178    position_px: &mut egui::Pos2,
179) {
180    profiling::function_scope!();
181
182    let Some(active_monitor) =
183        find_active_monitor(egui_zoom_factor, event_loop, window_size_pts, position_px)
184    else {
185        return; // no monitors 🤷
186    };
187
188    let mut window_size_px =
189        window_size_pts * (egui_zoom_factor * active_monitor.scale_factor() as f32);
190    // Add size of title bar. This is 32 px by default in Win 10/11.
191    if cfg!(target_os = "windows") {
192        window_size_px += egui::Vec2::new(
193            0.0,
194            32.0 * egui_zoom_factor * active_monitor.scale_factor() as f32,
195        );
196    }
197    let monitor_position = egui::Pos2::new(
198        active_monitor.position().x as f32,
199        active_monitor.position().y as f32,
200    );
201    let monitor_size_px = egui::Vec2::new(
202        active_monitor.size().width as f32,
203        active_monitor.size().height as f32,
204    );
205
206    // Window size cannot be negative or the subsequent `clamp` will panic.
207    let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
208    // To get the maximum position, we get the rightmost corner of the display, then
209    // subtract the size of the window to get the bottom right most value window.position
210    // can have.
211    *position_px = position_px.clamp(monitor_position, monitor_position + window_size);
212}