windows: Fix autohide taskbar dosen't automatically appear when Zed is maximized (#16806)

张小白 and Mikayla Maki created

Closes #12313

This PR introduces the following improvements:
1. Fixed the issue where the auto-hide taskbar wouldn't automatically
appear when Zed is maximized.
2. Refactored the `WM_NCCALCSIZE` code, making it more human-readable.

Release Notes:

- Fixed auto-hide taskbar would refuse to show itself when `Zed` is
maximized on
Winodws([#12313](https://github.com/zed-industries/zed/issues/12313)).

---------

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/gpui/src/platform/windows/events.rs          | 157 +++++++++++---
crates/gpui/src/platform/windows/system_settings.rs | 122 ++++++++++
crates/gpui/src/platform/windows/window.rs          |   2 
3 files changed, 231 insertions(+), 50 deletions(-)

Detailed changes

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

@@ -19,6 +19,7 @@ pub(crate) const CURSOR_STYLE_CHANGED: u32 = WM_USER + 1;
 pub(crate) const CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
 
 const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
+const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
 
 pub(crate) fn handle_msg(
     handle: HWND,
@@ -680,29 +681,43 @@ fn handle_calc_client_size(
         return None;
     }
 
-    let dpi = unsafe { GetDpiForWindow(handle) };
-
-    let frame_x = unsafe { GetSystemMetricsForDpi(SM_CXFRAME, dpi) };
-    let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
-    let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
-
+    let is_maximized = state_ptr.state.borrow().is_maximized();
+    let insets = get_client_area_insets(handle, is_maximized, state_ptr.windows_version);
     // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure
     let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS;
     let mut requested_client_rect = unsafe { &mut ((*params).rgrc) };
 
-    requested_client_rect[0].right -= frame_x + padding;
-    requested_client_rect[0].left += frame_x + padding;
-    requested_client_rect[0].bottom -= frame_y + padding;
-
-    if state_ptr.state.borrow().is_maximized() {
-        requested_client_rect[0].top += frame_y + padding;
-    } else {
-        match state_ptr.windows_version {
-            WindowsVersion::Win10 => {}
-            WindowsVersion::Win11 => {
-                // Magic number that calculates the width of the border
-                let border = (dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32).round() as i32;
-                requested_client_rect[0].top += border;
+    requested_client_rect[0].left += insets.left;
+    requested_client_rect[0].top += insets.top;
+    requested_client_rect[0].right -= insets.right;
+    requested_client_rect[0].bottom -= insets.bottom;
+
+    // Fix auto hide taskbar not showing. This solution is based on the approach
+    // used by Chrome. However, it may result in one row of pixels being obscured
+    // in our client area. But as Chrome says, "there seems to be no better solution."
+    if is_maximized {
+        if let Some(ref taskbar_position) = state_ptr
+            .state
+            .borrow()
+            .system_settings
+            .auto_hide_taskbar_position
+        {
+            // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
+            // so the window isn't treated as a "fullscreen app", which would cause
+            // the taskbar to disappear.
+            match taskbar_position {
+                AutoHideTaskbarPosition::Left => {
+                    requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Top => {
+                    requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Right => {
+                    requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
+                AutoHideTaskbarPosition::Bottom => {
+                    requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
+                }
             }
         }
     }
@@ -742,28 +757,12 @@ fn handle_activate_msg(
 }
 
 fn handle_create_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
-    let mut size_rect = RECT::default();
-    unsafe { GetWindowRect(handle, &mut size_rect).log_err() };
-
-    let width = size_rect.right - size_rect.left;
-    let height = size_rect.bottom - size_rect.top;
-
     if state_ptr.hide_title_bar {
-        unsafe {
-            SetWindowPos(
-                handle,
-                None,
-                size_rect.left,
-                size_rect.top,
-                width,
-                height,
-                SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE,
-            )
-            .log_err()
-        };
+        notify_frame_changed(handle);
+        Some(0)
+    } else {
+        None
     }
-
-    Some(0)
 }
 
 fn handle_dpi_changed_msg(
@@ -1099,12 +1098,17 @@ fn handle_system_settings_changed(
     state_ptr: Rc<WindowsWindowStatePtr>,
 ) -> Option<isize> {
     let mut lock = state_ptr.state.borrow_mut();
-    // mouse wheel
-    lock.system_settings.mouse_wheel_settings.update();
+    let display = lock.display;
+    // system settings
+    lock.system_settings.update(display);
     // mouse double click
     lock.click_state.system_update();
     // window border offset
     lock.border_offset.update(handle).log_err();
+    drop(lock);
+    // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide
+    // taskbar correctly.
+    notify_frame_changed(handle);
     Some(0)
 }
 
@@ -1343,6 +1347,77 @@ pub(crate) fn current_modifiers() -> Modifiers {
     }
 }
 
+fn get_client_area_insets(
+    handle: HWND,
+    is_maximized: bool,
+    windows_version: WindowsVersion,
+) -> RECT {
+    // For maximized windows, Windows outdents the window rect from the screen's client rect
+    // by `frame_thickness` on each edge, meaning `insets` must contain `frame_thickness`
+    // on all sides (including the top) to avoid the client area extending onto adjacent
+    // monitors.
+    //
+    // For non-maximized windows, things become complicated:
+    //
+    // - On Windows 10
+    // The top inset must be zero, since if there is any nonclient area, Windows will draw
+    // a full native titlebar outside the client area. (This doesn't occur in the maximized
+    // case.)
+    //
+    // - On Windows 11
+    // The top inset is calculated using an empirical formula that I derived through various
+    // tests. Without this, the top 1-2 rows of pixels in our window would be obscured.
+    let dpi = unsafe { GetDpiForWindow(handle) };
+    let frame_thickness = get_frame_thickness(dpi);
+    let top_insets = if is_maximized {
+        frame_thickness
+    } else {
+        match windows_version {
+            WindowsVersion::Win10 => 0,
+            WindowsVersion::Win11 => (dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32).round() as i32,
+        }
+    };
+    RECT {
+        left: frame_thickness,
+        top: top_insets,
+        right: frame_thickness,
+        bottom: frame_thickness,
+    }
+}
+
+// there is some additional non-visible space when talking about window
+// borders on Windows:
+// - SM_CXSIZEFRAME: The resize handle.
+// - SM_CXPADDEDBORDER: Additional border space that isn't part of the resize handle.
+fn get_frame_thickness(dpi: u32) -> i32 {
+    let resize_frame_thickness = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
+    let padding_thickness = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
+    resize_frame_thickness + padding_thickness
+}
+
+fn notify_frame_changed(handle: HWND) {
+    unsafe {
+        SetWindowPos(
+            handle,
+            None,
+            0,
+            0,
+            0,
+            0,
+            SWP_FRAMECHANGED
+                | SWP_NOACTIVATE
+                | SWP_NOCOPYBITS
+                | SWP_NOMOVE
+                | SWP_NOOWNERZORDER
+                | SWP_NOREPOSITION
+                | SWP_NOSENDCHANGING
+                | SWP_NOSIZE
+                | SWP_NOZORDER,
+        )
+        .log_err();
+    }
+}
+
 fn with_input_handler<F, R>(state_ptr: &Rc<WindowsWindowStatePtr>, f: F) -> Option<R>
 where
     F: FnOnce(&mut PlatformInputHandler) -> R,

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

@@ -1,16 +1,24 @@
 use std::ffi::{c_uint, c_void};
 
-use util::ResultExt;
-use windows::Win32::UI::WindowsAndMessaging::{
-    SystemParametersInfoW, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
-    SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
+use ::util::ResultExt;
+use windows::Win32::UI::{
+    Shell::{SHAppBarMessage, ABM_GETSTATE, ABM_GETTASKBARPOS, ABS_AUTOHIDE, APPBARDATA},
+    WindowsAndMessaging::{
+        SystemParametersInfoW, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES,
+        SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS,
+    },
 };
 
+use crate::*;
+
+use super::WindowsDisplay;
+
 /// Windows settings pulled from SystemParametersInfo
 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-systemparametersinfow
 #[derive(Default, Debug, Clone, Copy)]
 pub(crate) struct WindowsSystemSettings {
     pub(crate) mouse_wheel_settings: MouseWheelSettings,
+    pub(crate) auto_hide_taskbar_position: Option<AutoHideTaskbarPosition>,
 }
 
 #[derive(Default, Debug, Clone, Copy)]
@@ -22,19 +30,20 @@ pub(crate) struct MouseWheelSettings {
 }
 
 impl WindowsSystemSettings {
-    pub(crate) fn new() -> Self {
+    pub(crate) fn new(display: WindowsDisplay) -> Self {
         let mut settings = Self::default();
-        settings.init();
+        settings.update(display);
         settings
     }
 
-    fn init(&mut self) {
+    pub(crate) fn update(&mut self, display: WindowsDisplay) {
         self.mouse_wheel_settings.update();
+        self.auto_hide_taskbar_position = AutoHideTaskbarPosition::new(display).log_err().flatten();
     }
 }
 
 impl MouseWheelSettings {
-    pub(crate) fn update(&mut self) {
+    fn update(&mut self) {
         self.update_wheel_scroll_chars();
         self.update_wheel_scroll_lines();
     }
@@ -71,3 +80,100 @@ impl MouseWheelSettings {
         }
     }
 }
+
+#[derive(Debug, Clone, Copy, Default)]
+pub(crate) enum AutoHideTaskbarPosition {
+    Left,
+    Right,
+    Top,
+    #[default]
+    Bottom,
+}
+
+impl AutoHideTaskbarPosition {
+    fn new(display: WindowsDisplay) -> anyhow::Result<Option<Self>> {
+        if !check_auto_hide_taskbar_enable() {
+            // If auto hide taskbar is not enable, we do nothing in this case.
+            return Ok(None);
+        }
+        let mut info = APPBARDATA {
+            cbSize: std::mem::size_of::<APPBARDATA>() as u32,
+            ..Default::default()
+        };
+        let ret = unsafe { SHAppBarMessage(ABM_GETTASKBARPOS, &mut info) };
+        if ret == 0 {
+            anyhow::bail!(
+                "Unable to retrieve taskbar position: {}",
+                std::io::Error::last_os_error()
+            );
+        }
+        let taskbar_bounds: Bounds<DevicePixels> = Bounds::new(
+            point(info.rc.left.into(), info.rc.top.into()),
+            size(
+                (info.rc.right - info.rc.left).into(),
+                (info.rc.bottom - info.rc.top).into(),
+            ),
+        );
+        let display_bounds = display.physical_bounds();
+        if display_bounds.intersect(&taskbar_bounds) != taskbar_bounds {
+            // This case indicates that taskbar is not on the current monitor.
+            return Ok(None);
+        }
+        if taskbar_bounds.bottom() == display_bounds.bottom()
+            && taskbar_bounds.right() == display_bounds.right()
+        {
+            if taskbar_bounds.size.height < display_bounds.size.height
+                && taskbar_bounds.size.width == display_bounds.size.width
+            {
+                return Ok(Some(Self::Bottom));
+            }
+            if taskbar_bounds.size.width < display_bounds.size.width
+                && taskbar_bounds.size.height == display_bounds.size.height
+            {
+                return Ok(Some(Self::Right));
+            }
+            log::error!(
+                "Unrecognized taskbar bounds {:?} give display bounds {:?}",
+                taskbar_bounds,
+                display_bounds
+            );
+            return Ok(None);
+        }
+        if taskbar_bounds.top() == display_bounds.top()
+            && taskbar_bounds.left() == display_bounds.left()
+        {
+            if taskbar_bounds.size.height < display_bounds.size.height
+                && taskbar_bounds.size.width == display_bounds.size.width
+            {
+                return Ok(Some(Self::Top));
+            }
+            if taskbar_bounds.size.width < display_bounds.size.width
+                && taskbar_bounds.size.height == display_bounds.size.height
+            {
+                return Ok(Some(Self::Left));
+            }
+            log::error!(
+                "Unrecognized taskbar bounds {:?} give display bounds {:?}",
+                taskbar_bounds,
+                display_bounds
+            );
+            return Ok(None);
+        }
+        log::error!(
+            "Unrecognized taskbar bounds {:?} give display bounds {:?}",
+            taskbar_bounds,
+            display_bounds
+        );
+        Ok(None)
+    }
+}
+
+/// Check if auto hide taskbar is enable or not.
+fn check_auto_hide_taskbar_enable() -> bool {
+    let mut info = APPBARDATA {
+        cbSize: std::mem::size_of::<APPBARDATA>() as u32,
+        ..Default::default()
+    };
+    let ret = unsafe { SHAppBarMessage(ABM_GETSTATE, &mut info) } as u32;
+    ret == ABS_AUTOHIDE
+}

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

@@ -92,7 +92,7 @@ impl WindowsWindowState {
         let input_handler = None;
         let system_key_handled = false;
         let click_state = ClickState::new();
-        let system_settings = WindowsSystemSettings::new();
+        let system_settings = WindowsSystemSettings::new(display);
         let nc_button_pressed = None;
         let fullscreen = None;