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
This commit is contained in:
张小白 2024-05-15 01:54:18 +08:00 committed by GitHub
parent 5154910c64
commit 491c04e176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 90 additions and 15 deletions

View file

@ -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<DevicePixels>;
/// Get the default bounds for this display to place a window
fn default_bounds(&self) -> Bounds<DevicePixels> {
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

View file

@ -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<DevicePixels>) -> 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<Rc<dyn PlatformDisplay>> {
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 {

View file

@ -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<WindowsWindowStatePtr>) -> Option<isize> {
// 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,

View file

@ -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() };

View file

@ -51,6 +51,9 @@ mod prompts;
pub use prompts::*;
pub(crate) const DEFAULT_WINDOW_SIZE: Size<DevicePixels> =
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<DisplayId>, cx: &mut AppContext) -> Bounds<DevicePixels> {
const DEFAULT_WINDOW_SIZE: Size<DevicePixels> = size(DevicePixels(1024), DevicePixels(700));
const DEFAULT_WINDOW_OFFSET: Point<DevicePixels> = point(DevicePixels(0), DevicePixels(35));
cx.active_window()
@ -585,12 +587,7 @@ fn default_bounds(display_id: Option<DisplayId>, 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)
})