windows: Fix title bar height when maximized (#9449)

Ezekiel Warren created

screenshots and description incoming

## title bar when window is maximized
| before | after |
| ---    | ---   |
|
![image](https://github.com/zed-industries/zed/assets/1284289/075a943d-54db-4b71-9fa0-15f823255182)
|
![image](https://github.com/zed-industries/zed/assets/1284289/39a1d381-fcfd-4651-aab4-231a8ec3bd99)
|

## ~~caption buttons at 200%~~
~~buttons are now properly responsive at different scales~~
~~closes #9438~~
~~proper scale factor handling in follow up PR (possibly #9440)~~

<details>
  <summary>out of date image</summary>


![scale-factor](https://github.com/zed-industries/zed/assets/1284289/299d37b8-0d2e-4f2e-81db-2fff6fc59a62)
</details>

should be fixed by https://github.com/zed-industries/zed/pull/9456


Release Notes:

- N/A

Change summary

Cargo.lock                                                    |  1 
crates/gpui/src/platform/windows/window.rs                    | 49 ++--
crates/ui/Cargo.toml                                          |  3 
crates/ui/src/components/title_bar/title_bar.rs               | 48 +++-
crates/ui/src/components/title_bar/windows_window_controls.rs |  8 
5 files changed, 67 insertions(+), 42 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10626,6 +10626,7 @@ dependencies = [
  "story",
  "strum",
  "theme",
+ "windows 0.53.0",
 ]
 
 [[package]]

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

@@ -134,33 +134,37 @@ impl WindowsWindowInner {
         return false;
     }
 
-    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(
-                theme,
-                HDC::default(),
-                WP_CAPTION.0,
-                CS_ACTIVE.0,
-                None,
-                TS_TRUE,
-            )
-        }?;
-        unsafe { CloseThemeData(theme) }?;
-
-        let mut height =
-            (title_bar_size.cy as f32 * scale_factor).round() as i32 + top_and_bottom_borders;
+    pub(crate) fn title_bar_padding(&self) -> Pixels {
+        // using USER_DEFAULT_SCREEN_DPI because GPUI handles the scale with the scale factor
+        let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
+        px(padding as f32)
+    }
 
+    pub(crate) fn title_bar_top_offset(&self) -> Pixels {
         if self.is_maximized() {
-            let dpi = unsafe { GetDpiForWindow(self.hwnd) };
-            height += unsafe { (GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2) as i32 };
+            self.title_bar_padding() * 2
+        } else {
+            px(0.)
         }
+    }
+
+    pub(crate) fn title_bar_height(&self) -> Pixels {
+        // todo(windows) this is hard set to match the ui title bar
+        //               in the future the ui title bar component will report the size
+        px(32.) + self.title_bar_top_offset()
+    }
 
+    pub(crate) fn caption_button_width(&self) -> Pixels {
+        // todo(windows) this is hard set to match the ui title bar
+        //               in the future the ui title bar component will report the size
+        px(36.)
+    }
+
+    fn get_titlebar_rect(&self) -> anyhow::Result<RECT> {
+        let height = self.title_bar_height();
         let mut rect = RECT::default();
         unsafe { GetClientRect(self.hwnd, &mut rect) }?;
-        rect.bottom = rect.top + height;
+        rect.bottom = rect.top + ((height.0 as f32 * self.scale_factor.get()).round() as i32);
         Ok(rect)
     }
 
@@ -923,7 +927,8 @@ impl WindowsWindowInner {
         let titlebar_rect = self.get_titlebar_rect();
         if let Ok(titlebar_rect) = titlebar_rect {
             if cursor_point.y < titlebar_rect.bottom {
-                let caption_btn_width = unsafe { GetSystemMetricsForDpi(SM_CXSIZE, dpi) };
+                let caption_btn_width =
+                    (self.caption_button_width().0 * self.scale_factor.get()) as i32;
                 if cursor_point.x >= titlebar_rect.right - caption_btn_width {
                     return LRESULT(HTCLOSE as _);
                 } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 2 {

crates/ui/Cargo.toml 🔗

@@ -23,6 +23,9 @@ story = { workspace = true, optional = true }
 strum = { version = "0.25.0", features = ["derive"] }
 theme.workspace = true
 
+[target.'cfg(windows)'.dependencies]
+windows.workspace = true
+
 [features]
 default = []
 stories = ["dep:itertools", "dep:story"]

crates/ui/src/components/title_bar/title_bar.rs 🔗

@@ -10,12 +10,40 @@ pub struct TitleBar {
     content: Stateful<Div>,
     children: SmallVec<[AnyElement; 2]>,
 }
+#[cfg(not(target_os = "windows"))]
+fn title_bar_top_padding(_cx: &WindowContext) -> Pixels {
+    px(0.)
+}
+
+#[cfg(target_os = "windows")]
+fn title_bar_top_padding(cx: &WindowContext) -> Pixels {
+    use windows::Win32::UI::{
+        HiDpi::GetSystemMetricsForDpi,
+        WindowsAndMessaging::{SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI},
+    };
+
+    // this top padding is not dependent on the title bar style and is instead a quirk of maximized windows on Windows
+    // https://devblogs.microsoft.com/oldnewthing/20150304-00/?p=44543
+    let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
+    if cx.is_maximized() {
+        px((padding * 2) as f32)
+    } else {
+        px(0.)
+    }
+}
 
 impl TitleBar {
+    #[cfg(not(target_os = "windows"))]
     pub fn height(cx: &mut WindowContext) -> Pixels {
         (1.75 * cx.rem_size()).max(px(32.))
     }
 
+    #[cfg(target_os = "windows")]
+    pub fn height(_cx: &mut WindowContext) -> Pixels {
+        // todo(windows) instead of hard coded size report the actual size to the Windows platform API
+        px(32.)
+    }
+
     pub fn new(id: impl Into<ElementId>) -> Self {
         Self {
             platform_style: PlatformStyle::platform(),
@@ -29,16 +57,6 @@ impl TitleBar {
         self.platform_style = style;
         self
     }
-
-    fn top_padding(&self, cx: &WindowContext) -> Pixels {
-        if self.platform_style == PlatformStyle::Windows && cx.is_maximized() {
-            // todo(windows): get padding from win32 api, need HWND from window context somehow
-            // should be GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2
-            px(8.)
-        } else {
-            px(0.)
-        }
-    }
 }
 
 impl InteractiveElement for TitleBar {
@@ -58,13 +76,11 @@ impl ParentElement for TitleBar {
 impl RenderOnce for TitleBar {
     fn render(self, cx: &mut WindowContext) -> impl IntoElement {
         let height = Self::height(cx);
-        let top_padding = self.top_padding(cx);
-
         h_flex()
             .id("titlebar")
             .w_full()
-            .pt(top_padding)
-            .h(height)
+            .pt(title_bar_top_padding(cx))
+            .h(height + title_bar_top_padding(cx))
             .map(|this| {
                 if cx.is_fullscreen() {
                     this.pl_2()
@@ -88,9 +104,7 @@ impl RenderOnce for TitleBar {
                     .children(self.children),
             )
             .when(self.platform_style == PlatformStyle::Windows, |title_bar| {
-                let button_height = Self::height(cx) - top_padding;
-
-                title_bar.child(WindowsWindowControls::new(button_height))
+                title_bar.child(WindowsWindowControls::new(height))
             })
     }
 }

crates/ui/src/components/title_bar/windows_window_controls.rs 🔗

@@ -98,9 +98,11 @@ impl WindowsCaptionButton {
 
 impl RenderOnce for WindowsCaptionButton {
     fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        // todo(windows): get padding from win32 api, need HWND from window context somehow
-        // should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
-        let width = px(36.0);
+        // todo(windows) report this width to the Windows platform API
+        // NOTE: this is intentionally hard coded. An option to use the 'native' size
+        //       could be added when the width is reported to the Windows platform API
+        //       as this could change between future Windows versions.
+        let width = px(36.);
 
         h_flex()
             .id(self.id)