windows: Properly handle `DPI` (#9456)

张小白 created

As I mentioned before, there are the following issues with how GPUI
handles scale factors greater than 1.0:
1. The title bar buttons do not function correctly, with minimizing
button performing maximization and maximizing button performing closure.
2. As discussed in #8809, setting a scale factor greater than 1.0 causes
GPUI's drawing content to be pushed off the screen.

This PR introduces `LogicalSize` and `PhysicalSize` to differentiate
between coordinate systems for proper GPUI rendering, and now scale
factors above 1.5 are working correctly.

`Zed` with a scale factor equals 1.5, and change between different scale
factors:



https://github.com/zed-industries/zed/assets/14981363/3348536d-8bd3-41dd-82f6-052723312a5b



Release Notes:

- N/A

Change summary

crates/gpui/src/platform/windows/window.rs | 219 +++++++++++++++--------
1 file changed, 139 insertions(+), 80 deletions(-)

Detailed changes

crates/gpui/src/platform/windows/window.rs 🔗

@@ -13,6 +13,7 @@ use std::{
 };
 
 use ::util::ResultExt;
+use anyhow::Context;
 use blade_graphics as gpu;
 use futures::channel::oneshot::{self, Receiver};
 use itertools::Itertools;
@@ -41,14 +42,13 @@ use crate::*;
 pub(crate) struct WindowsWindowInner {
     hwnd: HWND,
     origin: Cell<Point<GlobalPixels>>,
-    size: Cell<Size<GlobalPixels>>,
-    mouse_position: Cell<Point<Pixels>>,
+    physical_size: Cell<Size<GlobalPixels>>,
+    scale_factor: Cell<f32>,
     input_handler: Cell<Option<PlatformInputHandler>>,
     renderer: RefCell<BladeRenderer>,
     callbacks: RefCell<Callbacks>,
     platform_inner: Rc<WindowsPlatformInner>,
     pub(crate) handle: AnyWindowHandle,
-    scale_factor: f32,
     hide_title_bar: bool,
     display: RefCell<Rc<WindowsDisplay>>,
 }
@@ -62,12 +62,16 @@ impl WindowsWindowInner {
         hide_title_bar: bool,
         display: Rc<WindowsDisplay>,
     ) -> Self {
-        let origin = Cell::new(Point::new((cs.x as f64).into(), (cs.y as f64).into()));
-        let size = Cell::new(Size {
-            width: (cs.cx as f64).into(),
-            height: (cs.cy as f64).into(),
+        let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
+        let origin = Cell::new(Point {
+            x: GlobalPixels(cs.x as f32),
+            y: GlobalPixels(cs.y as f32),
         });
-        let mouse_position = Cell::new(Point::default());
+        let physical_size = Cell::new(Size {
+            width: GlobalPixels(cs.cx as f32),
+            height: GlobalPixels(cs.cy as f32),
+        });
+        let scale_factor = Cell::new(monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32);
         let input_handler = Cell::new(None);
         struct RawWindow {
             hwnd: *mut c_void,
@@ -109,14 +113,13 @@ impl WindowsWindowInner {
         Self {
             hwnd,
             origin,
-            size,
-            mouse_position,
+            physical_size,
+            scale_factor,
             input_handler,
             renderer,
             callbacks,
             platform_inner,
             handle,
-            scale_factor: 1.0,
             hide_title_bar,
             display,
         }
@@ -133,6 +136,7 @@ impl WindowsWindowInner {
 
     fn get_titlebar_rect(&self) -> anyhow::Result<RECT> {
         let top_and_bottom_borders = 2;
+        let scale_factor = self.scale_factor.get();
         let theme = unsafe { OpenThemeData(self.hwnd, w!("WINDOW")) };
         let title_bar_size = unsafe {
             GetThemePartSize(
@@ -147,7 +151,7 @@ impl WindowsWindowInner {
         unsafe { CloseThemeData(theme) }?;
 
         let mut height =
-            (title_bar_size.cy as f32 * self.scale_factor).round() as i32 + top_and_bottom_borders;
+            (title_bar_size.cy as f32 * scale_factor).round() as i32 + top_and_bottom_borders;
 
         if self.is_maximized() {
             let dpi = unsafe { GetDpiForWindow(self.hwnd) };
@@ -189,7 +193,7 @@ impl WindowsWindowInner {
             WM_MOVE => self.handle_move_msg(lparam),
             WM_SIZE => self.handle_size_msg(lparam),
             WM_NCCALCSIZE => self.handle_calc_client_size(msg, wparam, lparam),
-            WM_DPICHANGED => self.handle_dpi_changed_msg(msg, wparam, lparam),
+            WM_DPICHANGED => self.handle_dpi_changed_msg(wparam, lparam),
             WM_NCHITTEST => self.handle_hit_test_msg(msg, wparam, lparam),
             WM_PAINT => self.handle_paint_msg(),
             WM_CLOSE => self.handle_close_msg(msg, wparam, lparam),
@@ -255,12 +259,15 @@ impl WindowsWindowInner {
     }
 
     fn handle_move_msg(&self, lparam: LPARAM) -> LRESULT {
-        let x = lparam.signed_loword() as f64;
-        let y = lparam.signed_hiword() as f64;
-        self.origin.set(Point::new(x.into(), y.into()));
-        let size = self.size.get();
-        let center_x = x as f32 + size.width.0 / 2.0;
-        let center_y = y as f32 + size.height.0 / 2.0;
+        let x = lparam.signed_loword() as f32;
+        let y = lparam.signed_hiword() as f32;
+        self.origin.set(Point {
+            x: GlobalPixels(x),
+            y: GlobalPixels(y),
+        });
+        let size = self.physical_size.get();
+        let center_x = x + size.width.0 / 2.0;
+        let center_y = y + size.height.0 / 2.0;
         let monitor_bounds = self.display.borrow().bounds();
         if center_x < monitor_bounds.left().0
             || center_x > monitor_bounds.right().0
@@ -282,23 +289,22 @@ impl WindowsWindowInner {
     }
 
     fn handle_size_msg(&self, lparam: LPARAM) -> LRESULT {
-        let width = lparam.loword().max(1) as f64;
-        let height = lparam.hiword().max(1) as f64;
-        self.renderer
-            .borrow_mut()
-            .update_drawable_size(Size { width, height });
-        let width = width.into();
-        let height = height.into();
-        self.size.set(Size { width, height });
+        let width = lparam.loword().max(1) as f32;
+        let height = lparam.hiword().max(1) as f32;
+        let scale_factor = self.scale_factor.get();
+        let new_physical_size = Size {
+            width: GlobalPixels(width),
+            height: GlobalPixels(height),
+        };
+        self.physical_size.set(new_physical_size);
+        self.renderer.borrow_mut().update_drawable_size(Size {
+            width: width as f64,
+            height: height as f64,
+        });
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.resize.as_mut() {
-            callback(
-                Size {
-                    width: Pixels(width.0),
-                    height: Pixels(height.0),
-                },
-                1.0,
-            );
+            let logical_size = logical_size(new_physical_size, scale_factor);
+            callback(logical_size, scale_factor);
         }
         self.invalidate_client_area();
         LRESULT(0)
@@ -351,9 +357,6 @@ impl WindowsWindowInner {
     }
 
     fn handle_mouse_move_msg(&self, lparam: LPARAM, wparam: WPARAM) -> LRESULT {
-        let x = Pixels::from(lparam.signed_loword() as f32);
-        let y = Pixels::from(lparam.signed_hiword() as f32);
-        self.mouse_position.set(Point { x, y });
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
             let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
@@ -368,8 +371,11 @@ impl WindowsWindowInner {
                 }
                 _ => None,
             };
+            let x = lparam.signed_loword() as f32;
+            let y = lparam.signed_hiword() as f32;
+            let scale_factor = self.scale_factor.get();
             let event = MouseMoveEvent {
-                position: Point { x, y },
+                position: logical_point(x, y, scale_factor),
                 pressed_button,
                 modifiers: self.current_modifiers(),
             };
@@ -601,11 +607,12 @@ impl WindowsWindowInner {
     fn handle_mouse_down_msg(&self, button: MouseButton, lparam: LPARAM) -> LRESULT {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
-            let x = Pixels::from(lparam.signed_loword() as f32);
-            let y = Pixels::from(lparam.signed_hiword() as f32);
+            let x = lparam.signed_loword() as f32;
+            let y = lparam.signed_hiword() as f32;
+            let scale_factor = self.scale_factor.get();
             let event = MouseDownEvent {
                 button,
-                position: Point { x, y },
+                position: logical_point(x, y, scale_factor),
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
@@ -619,11 +626,12 @@ impl WindowsWindowInner {
     fn handle_mouse_up_msg(&self, button: MouseButton, lparam: LPARAM) -> LRESULT {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
-            let x = Pixels::from(lparam.signed_loword() as f32);
-            let y = Pixels::from(lparam.signed_hiword() as f32);
+            let x = lparam.signed_loword() as f32;
+            let y = lparam.signed_hiword() as f32;
+            let scale_factor = self.scale_factor.get();
             let event = MouseUpEvent {
                 button,
-                position: Point { x, y },
+                position: logical_point(x, y, scale_factor),
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
@@ -637,12 +645,13 @@ impl WindowsWindowInner {
     fn handle_mouse_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
-            let x = Pixels::from(lparam.signed_loword() as f32);
-            let y = Pixels::from(lparam.signed_hiword() as f32);
             let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
                 * self.platform_inner.settings.borrow().wheel_scroll_lines as f32;
+            let x = lparam.signed_loword() as f32;
+            let y = lparam.signed_hiword() as f32;
+            let scale_factor = self.scale_factor.get();
             let event = crate::ScrollWheelEvent {
-                position: Point { x, y },
+                position: logical_point(x, y, scale_factor),
                 delta: ScrollDelta::Lines(Point {
                     x: 0.0,
                     y: wheel_distance,
@@ -659,12 +668,13 @@ impl WindowsWindowInner {
     fn handle_mouse_horizontal_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
-            let x = Pixels::from(lparam.signed_loword() as f32);
-            let y = Pixels::from(lparam.signed_hiword() as f32);
             let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
                 * self.platform_inner.settings.borrow().wheel_scroll_chars as f32;
+            let x = lparam.signed_loword() as f32;
+            let y = lparam.signed_hiword() as f32;
+            let scale_factor = self.scale_factor.get();
             let event = crate::ScrollWheelEvent {
-                position: Point { x, y },
+                position: logical_point(x, y, scale_factor),
                 delta: ScrollDelta::Lines(Point {
                     x: wheel_distance,
                     y: 0.0,
@@ -819,12 +829,13 @@ impl WindowsWindowInner {
     fn handle_create_msg(&self, _lparam: LPARAM) -> LRESULT {
         let mut size_rect = RECT::default();
         unsafe { GetWindowRect(self.hwnd, &mut size_rect).log_err() };
+
         let width = size_rect.right - size_rect.left;
         let height = size_rect.bottom - size_rect.top;
 
-        self.size.set(Size {
-            width: GlobalPixels::from(width as f64),
-            height: GlobalPixels::from(height as f64),
+        self.physical_size.set(Size {
+            width: GlobalPixels(width as f32),
+            height: GlobalPixels(height as f32),
         });
 
         if self.hide_title_bar {
@@ -847,8 +858,31 @@ impl WindowsWindowInner {
         LRESULT(0)
     }
 
-    fn handle_dpi_changed_msg(&self, _msg: u32, _wparam: WPARAM, _lparam: LPARAM) -> LRESULT {
-        LRESULT(1)
+    fn handle_dpi_changed_msg(&self, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+        let new_dpi = wparam.loword() as f32;
+        let scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
+        self.scale_factor.set(scale_factor);
+        let rect = unsafe { &*(lparam.0 as *const RECT) };
+        let width = rect.right - rect.left;
+        let height = rect.bottom - rect.top;
+        // this will emit `WM_SIZE` and `WM_MOVE` right here
+        // even before this funtion returns
+        // the new size is handled in `WM_SIZE`
+        unsafe {
+            SetWindowPos(
+                self.hwnd,
+                None,
+                rect.left,
+                rect.top,
+                width,
+                height,
+                SWP_NOZORDER | SWP_NOACTIVATE,
+            )
+            .context("unable to set window position after dpi has changed")
+            .log_err();
+        }
+        self.invalidate_client_area();
+        LRESULT(0)
     }
 
     fn handle_hit_test_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
@@ -910,18 +944,16 @@ impl WindowsWindowInner {
             return unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
         }
 
-        let mut cursor_point = POINT {
-            x: lparam.signed_loword().into(),
-            y: lparam.signed_hiword().into(),
-        };
-        unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
-        let x = Pixels::from(cursor_point.x as f32);
-        let y = Pixels::from(cursor_point.y as f32);
-        self.mouse_position.set(Point { x, y });
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(callback) = callbacks.input.as_mut() {
+            let mut cursor_point = POINT {
+                x: lparam.signed_loword().into(),
+                y: lparam.signed_hiword().into(),
+            };
+            unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
+            let scale_factor = self.scale_factor.get();
             let event = MouseMoveEvent {
-                position: Point { x, y },
+                position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
                 pressed_button: None,
                 modifiers: self.current_modifiers(),
             };
@@ -951,11 +983,10 @@ impl WindowsWindowInner {
                 y: lparam.signed_hiword().into(),
             };
             unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
-            let x = Pixels::from(cursor_point.x as f32);
-            let y = Pixels::from(cursor_point.y as f32);
+            let scale_factor = self.scale_factor.get();
             let event = MouseDownEvent {
                 button,
-                position: Point { x, y },
+                position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
@@ -990,11 +1021,10 @@ impl WindowsWindowInner {
                 y: lparam.signed_hiword().into(),
             };
             unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
-            let x = Pixels::from(cursor_point.x as f32);
-            let y = Pixels::from(cursor_point.y as f32);
+            let scale_factor = self.scale_factor.get();
             let event = MouseUpEvent {
                 button,
-                position: Point { x, y },
+                position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
@@ -1194,7 +1224,7 @@ impl PlatformWindow for WindowsWindow {
     fn bounds(&self) -> Bounds<GlobalPixels> {
         Bounds {
             origin: self.inner.origin.get(),
-            size: self.inner.size.get(),
+            size: self.inner.physical_size.get(),
         }
     }
 
@@ -1202,18 +1232,19 @@ impl PlatformWindow for WindowsWindow {
         self.inner.is_maximized()
     }
 
-    // todo(windows)
+    /// get the logical size of the app's drawable area.
+    ///
+    /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as
+    /// whether the mouse collides with other elements of GPUI).
     fn content_size(&self) -> Size<Pixels> {
-        let size = self.inner.size.get();
-        Size {
-            width: size.width.0.into(),
-            height: size.height.0.into(),
-        }
+        logical_size(
+            self.inner.physical_size.get(),
+            self.inner.scale_factor.get(),
+        )
     }
 
-    // todo(windows)
     fn scale_factor(&self) -> f32 {
-        self.inner.scale_factor
+        self.inner.scale_factor.get()
     }
 
     // todo(windows)
@@ -1226,7 +1257,19 @@ impl PlatformWindow for WindowsWindow {
     }
 
     fn mouse_position(&self) -> Point<Pixels> {
-        self.inner.mouse_position.get()
+        let point = unsafe {
+            let mut point: POINT = std::mem::zeroed();
+            GetCursorPos(&mut point)
+                .context("unable to get cursor position")
+                .log_err();
+            ScreenToClient(self.inner.hwnd, &mut point);
+            point
+        };
+        logical_point(
+            point.x as f32,
+            point.y as f32,
+            self.inner.scale_factor.get(),
+        )
     }
 
     // todo(windows)
@@ -1657,5 +1700,21 @@ fn oemkey_vkcode_to_string(code: u16) -> Option<String> {
     }
 }
 
+#[inline]
+fn logical_size(physical_size: Size<GlobalPixels>, scale_factor: f32) -> Size<Pixels> {
+    Size {
+        width: px(physical_size.width.0 / scale_factor),
+        height: px(physical_size.height.0 / scale_factor),
+    }
+}
+
+#[inline]
+fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels> {
+    Point {
+        x: px(x / scale_factor),
+        y: px(y / scale_factor),
+    }
+}
+
 // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
 const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;