Skip to content

Commit

Permalink
eframe: Better restore Window position on Mac when on secondary monit…
Browse files Browse the repository at this point in the history
…or (#3239)
  • Loading branch information
emilk authored Aug 11, 2023
1 parent f78db80 commit dd417cf
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 84 deletions.
15 changes: 11 additions & 4 deletions crates/eframe/src/native/epi_integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,11 @@ pub fn window_builder<E>(

let inner_size_points = if let Some(mut window_settings) = window_settings {
// Restore pos/size from previous session
window_settings.clamp_to_sane_values(largest_monitor_point_size(event_loop));
#[cfg(windows)]
window_settings.clamp_window_to_sane_position(event_loop);
window_builder = window_settings.initialize_window(window_builder);

window_settings.clamp_size_to_sane_values(largest_monitor_point_size(event_loop));
window_settings.clamp_position_to_monitors(event_loop);

window_builder = window_settings.initialize_window_builder(window_builder);
window_settings.inner_size_points()
} else {
if let Some(pos) = *initial_window_pos {
Expand Down Expand Up @@ -173,19 +174,25 @@ pub fn window_builder<E>(
}
}
}

window_builder
}

pub fn apply_native_options_to_window(
window: &winit::window::Window,
native_options: &crate::NativeOptions,
window_settings: Option<WindowSettings>,
) {
use winit::window::WindowLevel;
window.set_window_level(if native_options.always_on_top {
WindowLevel::AlwaysOnTop
} else {
WindowLevel::Normal
});

if let Some(window_settings) = window_settings {
window_settings.initialize_window(window);
}
}

fn largest_monitor_point_size<E>(event_loop: &EventLoopWindowTarget<E>) -> egui::Vec2 {
Expand Down
12 changes: 10 additions & 2 deletions crates/eframe/src/native/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,11 @@ mod glow_integration {
glutin_window_context.on_resume(event_loop)?;

if let Some(window) = &glutin_window_context.window {
epi_integration::apply_native_options_to_window(window, native_options);
epi_integration::apply_native_options_to_window(
window,
native_options,
window_settings,
);
}

let gl = unsafe {
Expand Down Expand Up @@ -1128,7 +1132,11 @@ mod wgpu_integration {
let window_builder =
epi_integration::window_builder(event_loop, title, native_options, window_settings);
let window = window_builder.build(event_loop)?;
epi_integration::apply_native_options_to_window(&window, native_options);
epi_integration::apply_native_options_to_window(
&window,
native_options,
window_settings,
);
Ok(window)
}

Expand Down
193 changes: 115 additions & 78 deletions crates/egui-winit/src/window_settings.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
/// Can be used to store native window settings (position and size).
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "serde", serde(default))]
pub struct WindowSettings {
/// Position of window in physical pixels. This is either
/// the inner or outer position depending on the platform.
/// See [`winit::window::WindowBuilder::with_position`] for details.
position: Option<egui::Pos2>,
/// Position of window content in physical pixels.
inner_position_pixels: Option<egui::Pos2>,

/// Position of window frame/titlebar in physical pixels.
outer_position_pixels: Option<egui::Pos2>,

fullscreen: bool,

Expand All @@ -16,22 +18,20 @@ pub struct WindowSettings {
impl WindowSettings {
pub fn from_display(window: &winit::window::Window) -> Self {
let inner_size_points = window.inner_size().to_logical::<f32>(window.scale_factor());
let position = if cfg!(macos) {
// MacOS uses inner position when positioning windows.
window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
} else {
// Other platforms use the outer position.
window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32))
};

let inner_position_pixels = window
.inner_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32));

let outer_position_pixels = window
.outer_position()
.ok()
.map(|p| egui::pos2(p.x as f32, p.y as f32));

Self {
position,
inner_position_pixels,
outer_position_pixels,

fullscreen: window.fullscreen().is_some(),

Expand All @@ -46,19 +46,21 @@ impl WindowSettings {
self.inner_size_points
}

pub fn initialize_window(
pub fn initialize_window_builder(
&self,
mut window: winit::window::WindowBuilder,
) -> winit::window::WindowBuilder {
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the clamping behavior is managed by the function
// clamp_window_to_sane_position.
if let Some(pos) = self.position {
// `WindowBuilder::with_position` expects inner position in Macos, and outer position elsewhere
// See [`winit::window::WindowBuilder::with_position`] for details.
let pos_px = if cfg!(target_os = "macos") {
self.inner_position_pixels
} else {
self.outer_position_pixels
};
if let Some(pos_px) = pos_px {
window = window.with_position(winit::dpi::PhysicalPosition {
x: pos.x as f64,
y: pos.y as f64,
x: pos_px.x as f64,
y: pos_px.y as f64,
});
}

Expand All @@ -77,68 +79,103 @@ impl WindowSettings {
}
}

pub fn clamp_to_sane_values(&mut self, max_size: egui::Vec2) {
pub fn initialize_window(&self, window: &winit::window::Window) {
if cfg!(target_os = "macos") {
// Mac sometimes has problems restoring the window to secondary monitors
// using only `WindowBuilder::with_position`, so we need this extra step:
if let Some(pos) = self.outer_position_pixels {
window.set_outer_position(winit::dpi::PhysicalPosition { x: pos.x, y: pos.y });
}
}
}

pub fn clamp_size_to_sane_values(&mut self, largest_monitor_size_points: egui::Vec2) {
use egui::NumExt as _;

if let Some(size) = &mut self.inner_size_points {
// Prevent ridiculously small windows
// Prevent ridiculously small windows:
let min_size = egui::Vec2::splat(64.0);
*size = size.at_least(min_size);
*size = size.at_most(max_size);

// Make sure we don't try to create a window larger than the largest monitor
// because on Linux that can lead to a crash.
*size = size.at_most(largest_monitor_size_points);
}
}

pub fn clamp_window_to_sane_position<E>(
pub fn clamp_position_to_monitors<E>(
&mut self,
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
) {
if let (Some(position), Some(inner_size_points)) =
(&mut self.position, &self.inner_size_points)
{
let monitors = event_loop.available_monitors();
// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};
for monitor in monitors {
let monitor_x_range = (monitor.position().x - inner_size_points.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - inner_size_points.y as i32)
..(monitor.position().y + monitor.size().height as i32);

if monitor_x_range.contains(&(position.x as i32))
&& monitor_y_range.contains(&(position.y as i32))
{
active_monitor = monitor;
}
}
// If the app last ran on two monitors and only one is now connected, then
// the given position is invalid.
// If this happens on Mac, the window is clamped into valid area.
// If this happens on Windows, the window becomes invisible to the user 🤦‍♂️
// So on Windows we clamp the position to the monitor it is on.
if !cfg!(target_os = "windows") {
return;
}

let mut inner_size_pixels = *inner_size_points * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
inner_size_pixels +=
egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);

// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size - inner_size_pixels).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position = position.clamp(monitor_position, monitor_position + window_size);
let Some(inner_size_points) = self.inner_size_points else { return; };

if let Some(pos_px) = &mut self.inner_position_pixels {
clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
}
if let Some(pos_px) = &mut self.outer_position_pixels {
clamp_pos_to_monitors(event_loop, inner_size_points, pos_px);
}
}
}

fn clamp_pos_to_monitors<E>(
event_loop: &winit::event_loop::EventLoopWindowTarget<E>,
window_size_pts: egui::Vec2,
position_px: &mut egui::Pos2,
) {
let monitors = event_loop.available_monitors();

// default to primary monitor, in case the correct monitor was disconnected.
let mut active_monitor = if let Some(active_monitor) = event_loop
.primary_monitor()
.or_else(|| event_loop.available_monitors().next())
{
active_monitor
} else {
return; // no monitors 🤷
};

for monitor in monitors {
let window_size_px = window_size_pts * (monitor.scale_factor() as f32);
let monitor_x_range = (monitor.position().x - window_size_px.x as i32)
..(monitor.position().x + monitor.size().width as i32);
let monitor_y_range = (monitor.position().y - window_size_px.y as i32)
..(monitor.position().y + monitor.size().height as i32);

if monitor_x_range.contains(&(position_px.x as i32))
&& monitor_y_range.contains(&(position_px.y as i32))
{
active_monitor = monitor;
}
}

let mut window_size_px = window_size_pts * (active_monitor.scale_factor() as f32);
// Add size of title bar. This is 32 px by default in Win 10/11.
if cfg!(target_os = "windows") {
window_size_px += egui::Vec2::new(0.0, 32.0 * active_monitor.scale_factor() as f32);
}
let monitor_position = egui::Pos2::new(
active_monitor.position().x as f32,
active_monitor.position().y as f32,
);
let monitor_size_px = egui::Vec2::new(
active_monitor.size().width as f32,
active_monitor.size().height as f32,
);

// Window size cannot be negative or the subsequent `clamp` will panic.
let window_size = (monitor_size_px - window_size_px).max(egui::Vec2::ZERO);
// To get the maximum position, we get the rightmost corner of the display, then
// subtract the size of the window to get the bottom right most value window.position
// can have.
*position_px = position_px.clamp(monitor_position, monitor_position + window_size);
}

0 comments on commit dd417cf

Please sign in to comment.