From 491c04e176018d70c082ccde1bb207fff2b4422a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=B0=8F=E7=99=BD?= <364772080@qq.com> Date: Wed, 15 May 2024 01:54:18 +0800 Subject: [PATCH] windows: Support multi-monitor (#11699) Zed can detect changes in monitor connections and disconnections and provide corresponding feedback. For example, if the current window's display monitor is disconnected, the window will be moved to the primary monitor. And now Zed always opens on the monitor specified in `WindowParams`. Release Notes: - N/A --- crates/gpui/src/platform.rs | 11 +++++- crates/gpui/src/platform/windows/display.rs | 21 ++++++++++++ crates/gpui/src/platform/windows/events.rs | 38 +++++++++++++++++++++ crates/gpui/src/platform/windows/window.rs | 24 +++++++++---- crates/gpui/src/window.rs | 11 +++--- 5 files changed, 90 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 06e388f88c42427fbc21fc64697c2c9d8ec2519b..0d311f1e32e1ddb03c34eb9c6f24525399b541a8 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -25,10 +25,11 @@ mod test; mod windows; use crate::{ - Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, + point, Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlyphId, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, SharedString, Size, Task, TaskLabel, WindowContext, + DEFAULT_WINDOW_SIZE, }; use anyhow::Result; use async_task::Runnable; @@ -167,6 +168,14 @@ pub trait PlatformDisplay: Send + Sync + Debug { /// Get the bounds for this display fn bounds(&self) -> Bounds; + + /// Get the default bounds for this display to place a window + fn default_bounds(&self) -> Bounds { + let center = self.bounds().center(); + let offset = DEFAULT_WINDOW_SIZE / 2; + let origin = point(center.x - offset.width, center.y - offset.height); + Bounds::new(origin, DEFAULT_WINDOW_SIZE) + } } /// An opaque identifier for a hardware display diff --git a/crates/gpui/src/platform/windows/display.rs b/crates/gpui/src/platform/windows/display.rs index eaba1370cc4d8e4b4f7219da04b602e9b4f63f8f..e1e7b1e2251f89dafac17a50357720ac16fb2507 100644 --- a/crates/gpui/src/platform/windows/display.rs +++ b/crates/gpui/src/platform/windows/display.rs @@ -108,6 +108,22 @@ impl WindowsDisplay { Some(WindowsDisplay::new_with_handle(monitor)) } + /// Check if the center point of given bounds is inside this monitor + pub fn check_given_bounds(&self, bounds: Bounds) -> bool { + let center = bounds.center(); + let center = POINT { + x: center.x.0, + y: center.y.0, + }; + let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) }; + if monitor.is_invalid() { + false + } else { + let display = WindowsDisplay::new_with_handle(monitor); + display.uuid == self.uuid + } + } + pub fn displays() -> Vec> { available_monitors() .into_iter() @@ -135,6 +151,11 @@ impl WindowsDisplay { .then(|| devmode.dmDisplayFrequency) }) } + + /// Check if this monitor is still online + pub fn is_connected(hmonitor: HMONITOR) -> bool { + available_monitors().iter().contains(&hmonitor) + } } impl PlatformDisplay for WindowsDisplay { diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 9182adf8bec332e1c91730ef28a990352bfdf7f7..8cc5c65a912afef7cef61a710271b7ff4f8e12b8 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -39,6 +39,7 @@ pub(crate) fn handle_msg( WM_TIMER => handle_timer_msg(handle, wparam, state_ptr), WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr), WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr), + WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr), WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr), WM_PAINT => handle_paint_msg(handle, state_ptr), WM_CLOSE => handle_close_msg(state_ptr), @@ -112,6 +113,8 @@ fn handle_move_msg( { // center of the window may have moved to another monitor let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // minimize the window can trigger this event too, in this case, + // monitor is invalid, we do nothing. if !monitor.is_invalid() && lock.display.handle != monitor { // we will get the same monitor if we only have one lock.display = WindowsDisplay::new_with_handle(monitor); @@ -775,6 +778,41 @@ fn handle_dpi_changed_msg( Some(0) } +/// The following conditions will trigger this event: +/// 1. The monitor on which the window is located goes offline or changes resolution. +/// 2. Another monitor goes offline, is plugged in, or changes resolution. +/// +/// In either case, the window will only receive information from the monitor on which +/// it is located. +/// +/// For example, in the case of condition 2, where the monitor on which the window is +/// located has actually changed nothing, it will still receive this event. +fn handle_display_change_msg(handle: HWND, state_ptr: Rc) -> Option { + // NOTE: + // Even the `lParam` holds the resolution of the screen, we just ignore it. + // Because WM_DPICHANGED, WM_MOVE, WM_SIEZ will come first, window reposition and resize + // are handled there. + // So we only care about if monitor is disconnected. + let previous_monitor = state_ptr.as_ref().state.borrow().display; + if WindowsDisplay::is_connected(previous_monitor.handle) { + // we are fine, other display changed + return None; + } + // display disconnected + // in this case, the OS will move our window to another monitor, and minimize it. + // we deminimize the window and query the monitor after moving + unsafe { ShowWindow(handle, SW_SHOWNORMAL) }; + let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) }; + // all monitors disconnected + if new_monitor.is_invalid() { + log::error!("No monitor detected!"); + return None; + } + let new_display = WindowsDisplay::new_with_handle(new_monitor); + state_ptr.as_ref().state.borrow_mut().display = new_display; + Some(0) +} + fn handle_hit_test_msg( handle: HWND, msg: u32, diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index cfa720a4b157211b46f8b5ddac7b65a38ddd6c88..ee714c9c2944351b0328ebfa276b38a557202e61 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -258,13 +258,17 @@ impl WindowsWindow { ); let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX; let hinstance = get_module_handle(); + let display = if let Some(display_id) = params.display_id { + // if we obtain a display_id, then this ID must be valid. + WindowsDisplay::new(display_id).unwrap() + } else { + WindowsDisplay::primary_monitor().unwrap() + }; let mut context = WindowCreateContext { inner: None, handle, hide_title_bar, - // todo(windows) move window to target monitor - // options.display_id - display: WindowsDisplay::primary_monitor().unwrap(), + display, transparent: params.window_background != WindowBackgroundAppearance::Opaque, executor, mouse_wheel_settings, @@ -297,10 +301,16 @@ impl WindowsWindow { ..Default::default() }; GetWindowPlacement(raw_hwnd, &mut placement).log_err(); - placement.rcNormalPosition.left = params.bounds.left().0; - placement.rcNormalPosition.right = params.bounds.right().0; - placement.rcNormalPosition.top = params.bounds.top().0; - placement.rcNormalPosition.bottom = params.bounds.bottom().0; + // the bounds may be not inside the display + let bounds = if display.check_given_bounds(params.bounds) { + params.bounds + } else { + display.default_bounds() + }; + placement.rcNormalPosition.left = bounds.left().0; + placement.rcNormalPosition.right = bounds.right().0; + placement.rcNormalPosition.top = bounds.top().0; + placement.rcNormalPosition.bottom = bounds.bottom().0; SetWindowPlacement(raw_hwnd, &placement).log_err(); } unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok().log_err() }; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ef9c0a610fe2e02d81a654a1f08075f367173680..f0fbe28e4978a97a8146d76fbd2dc0d6d759865b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -51,6 +51,9 @@ mod prompts; pub use prompts::*; +pub(crate) const DEFAULT_WINDOW_SIZE: Size = + size(DevicePixels(1024), DevicePixels(700)); + /// Represents the two different phases when dispatching events. #[derive(Default, Copy, Clone, Debug, Eq, PartialEq)] pub enum DispatchPhase { @@ -573,7 +576,6 @@ pub(crate) struct ElementStateBox { } fn default_bounds(display_id: Option, cx: &mut AppContext) -> Bounds { - const DEFAULT_WINDOW_SIZE: Size = size(DevicePixels(1024), DevicePixels(700)); const DEFAULT_WINDOW_OFFSET: Point = point(DevicePixels(0), DevicePixels(35)); cx.active_window() @@ -585,12 +587,7 @@ fn default_bounds(display_id: Option, cx: &mut AppContext) -> Bounds< .unwrap_or_else(|| cx.primary_display()); display - .map(|display| { - let center = display.bounds().center(); - let offset = DEFAULT_WINDOW_SIZE / 2; - let origin = point(center.x - offset.width, center.y - offset.height); - Bounds::new(origin, DEFAULT_WINDOW_SIZE) - }) + .map(|display| display.default_bounds()) .unwrap_or_else(|| { Bounds::new(point(DevicePixels(0), DevicePixels(0)), DEFAULT_WINDOW_SIZE) })