windows: better looking titlebar (#9053)

Ezekiel Warren and Mikayla created

~~work in progress. not ready for review. made for visibility only, but
feel free to comment :)~~

TODO:
- [x] add close/min/max buttons (to be rendered with gpui)
- [x] snap layout support
- [x] fix issues with clicking items in titlebar
- [x] cleanup/document

Release Notes:

- Added custom windows titlebar

![](https://media.discordapp.net/attachments/1208481909676576818/1216985375969378324/caption-buttons-working.gif?ex=660260f4&is=65efebf4&hm=53a17af6e2f233eba54302a5adb9efe23900f4d6f6d1d854bec887120789130c&=)

---------

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

Change summary

Cargo.toml                                       |  11 
crates/collab_ui/src/collab_titlebar_item.rs     |  36 -
crates/gpui/src/platform.rs                      |  11 
crates/gpui/src/platform/linux/wayland/window.rs |  11 
crates/gpui/src/platform/linux/x11/window.rs     |  11 
crates/gpui/src/platform/mac/window.rs           |  21 
crates/gpui/src/platform/test/window.rs          |  18 
crates/gpui/src/platform/windows/display.rs      |   3 
crates/gpui/src/platform/windows/text_system.rs  |  17 
crates/gpui/src/platform/windows/util.rs         |  33 +
crates/gpui/src/platform/windows/window.rs       | 377 +++++++++++++++--
crates/gpui/src/window.rs                        |  31 +
crates/ui/src/components.rs                      |   2 
crates/ui/src/components/platform_titlebar.rs    | 226 ++++++++++
crates/workspace/src/workspace.rs                |   8 
15 files changed, 694 insertions(+), 122 deletions(-)

Detailed changes

Cargo.toml 🔗

@@ -343,6 +343,17 @@ features = [
     "Win32_Globalization",
     "Win32_Graphics_DirectComposition",
     "Win32_Graphics_Gdi",
+    "Win32_UI_Controls",
+    "Win32_Graphics_DirectWrite",
+    "Win32_UI_WindowsAndMessaging",
+    "Win32_UI_Input_KeyboardAndMouse",
+    "Win32_UI_Shell",
+    "Win32_System_Com",
+    "Win32_UI_HiDpi",
+    "Win32_UI_Controls",
+    "Win32_System_SystemInformation",
+    "Win32_System_SystemServices",
+    "Win32_System_Time",
     "Win32_Security",
     "Win32_Storage_FileSystem",
     "Win32_System_Com",

crates/collab_ui/src/collab_titlebar_item.rs 🔗

@@ -13,12 +13,12 @@ use rpc::proto;
 use std::sync::Arc;
 use theme::ActiveTheme;
 use ui::{
-    h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike,
-    ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
+    h_flex, platform_titlebar, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator,
+    Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
 };
 use util::ResultExt;
 use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
-use workspace::{notifications::NotifyResultExt, titlebar_height, Workspace};
+use workspace::{notifications::NotifyResultExt, Workspace};
 
 const MAX_PROJECT_NAME_LENGTH: usize = 40;
 const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -58,26 +58,18 @@ impl Render for CollabTitlebarItem {
         let project_id = self.project.read(cx).remote_id();
         let workspace = self.workspace.upgrade();
 
-        h_flex()
-            .id("titlebar")
-            .justify_between()
-            .w_full()
-            .h(titlebar_height(cx))
-            .map(|this| {
-                if cx.is_fullscreen() {
-                    this.pl_2()
-                } else {
-                    // Use pixels here instead of a rem-based size because the macOS traffic
-                    // lights are a static size, and don't scale with the rest of the UI.
-                    this.pl(px(80.))
-                }
-            })
-            .bg(cx.theme().colors().title_bar_background)
-            .on_click(|event, cx| {
-                if event.up.click_count == 2 {
-                    cx.zoom_window();
-                }
+        platform_titlebar("collab-titlebar")
+            .titlebar_bg(cx.theme().colors().title_bar_background)
+            // note: on windows titlebar behaviour is handled by the platform implementation
+            .when(cfg!(not(windows)), |this| {
+                this.on_click(|event, cx| {
+                    if event.up.click_count == 2 {
+                        cx.zoom_window();
+                    }
+                })
             })
+            .px_2()
+            .justify_between()
             // left side
             .child(
                 h_flex()

crates/gpui/src/platform.rs 🔗

@@ -22,10 +22,10 @@ mod test;
 mod windows;
 
 use crate::{
-    Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels, Font,
-    FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, Keymap, LineLayout,
-    Pixels, PlatformInput, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene,
-    SharedString, Size, Task, TaskLabel, WindowContext,
+    Action, AnyWindowHandle, AsyncWindowContext, BackgroundExecutor, Bounds, DevicePixels,
+    DispatchEventResult, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels,
+    GlyphId, Keymap, LineLayout, Pixels, PlatformInput, Point, RenderGlyphParams,
+    RenderImageParams, RenderSvgParams, Scene, SharedString, Size, Task, TaskLabel, WindowContext,
 };
 use anyhow::Result;
 use async_task::Runnable;
@@ -168,6 +168,7 @@ unsafe impl Send for DisplayId {}
 
 pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn bounds(&self) -> Bounds<GlobalPixels>;
+    fn is_maximized(&self) -> bool;
     fn content_size(&self) -> Size<Pixels>;
     fn scale_factor(&self) -> f32;
     fn titlebar_height(&self) -> Pixels;
@@ -194,7 +195,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
     fn toggle_fullscreen(&self);
     fn is_fullscreen(&self) -> bool;
     fn on_request_frame(&self, callback: Box<dyn FnMut()>);
-    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>);
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>);
     fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>);
     fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>);
     fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>);

crates/gpui/src/platform/linux/wayland/window.rs 🔗

@@ -29,7 +29,7 @@ use crate::{
 #[derive(Default)]
 pub(crate) struct Callbacks {
     request_frame: Option<Box<dyn FnMut()>>,
-    input: Option<Box<dyn FnMut(crate::PlatformInput) -> bool>>,
+    input: Option<Box<dyn FnMut(crate::PlatformInput) -> crate::DispatchEventResult>>,
     active_status_change: Option<Box<dyn FnMut(bool)>>,
     resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     fullscreen: Option<Box<dyn FnMut(bool)>>,
@@ -237,7 +237,7 @@ impl WaylandWindowState {
 
     pub fn handle_input(&self, input: PlatformInput) {
         if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
-            if fun(input.clone()) {
+            if !fun(input.clone()).propagate {
                 return;
             }
         }
@@ -279,6 +279,11 @@ impl PlatformWindow for WaylandWindow {
         unimplemented!()
     }
 
+    // todo(linux)
+    fn is_maximized(&self) -> bool {
+        unimplemented!()
+    }
+
     fn content_size(&self) -> Size<Pixels> {
         let inner = self.0.inner.borrow_mut();
         Size {
@@ -378,7 +383,7 @@ impl PlatformWindow for WaylandWindow {
         self.0.callbacks.borrow_mut().request_frame = Some(callback);
     }
 
-    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
         self.0.callbacks.borrow_mut().input = Some(callback);
     }
 

crates/gpui/src/platform/linux/x11/window.rs 🔗

@@ -31,7 +31,7 @@ use super::X11Display;
 #[derive(Default)]
 struct Callbacks {
     request_frame: Option<Box<dyn FnMut()>>,
-    input: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
+    input: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
     active_status_change: Option<Box<dyn FnMut(bool)>>,
     resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     fullscreen: Option<Box<dyn FnMut(bool)>>,
@@ -303,7 +303,7 @@ impl X11WindowState {
 
     pub fn handle_input(&self, input: PlatformInput) {
         if let Some(ref mut fun) = self.callbacks.borrow_mut().input {
-            if fun(input.clone()) {
+            if !fun(input.clone()).propagate {
                 return;
             }
         }
@@ -333,6 +333,11 @@ impl PlatformWindow for X11Window {
             .map(|v| GlobalPixels(v as f32))
     }
 
+    // todo(linux)
+    fn is_maximized(&self) -> bool {
+        unimplemented!()
+    }
+
     fn content_size(&self) -> Size<Pixels> {
         self.0.inner.borrow_mut().content_size()
     }
@@ -451,7 +456,7 @@ impl PlatformWindow for X11Window {
         self.0.callbacks.borrow_mut().request_frame = Some(callback);
     }
 
-    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
         self.0.callbacks.borrow_mut().input = Some(callback);
     }
 

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

@@ -324,7 +324,7 @@ struct MacWindowState {
     renderer: renderer::Renderer,
     kind: WindowKind,
     request_frame_callback: Option<Box<dyn FnMut()>>,
-    event_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
+    event_callback: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
@@ -411,6 +411,14 @@ impl MacWindowState {
         self.display_link = None;
     }
 
+    fn is_maximized(&self) -> bool {
+        unsafe {
+            let bounds = self.bounds();
+            let screen_size = self.native_window.screen().visibleFrame().into();
+            bounds.size == screen_size
+        }
+    }
+
     fn is_fullscreen(&self) -> bool {
         unsafe {
             let style_mask = self.native_window.styleMask();
@@ -716,6 +724,11 @@ impl PlatformWindow for MacWindow {
         self.0.as_ref().lock().bounds()
     }
 
+    // todo(mac)
+    fn is_maximized(&self) -> bool {
+        self.0.as_ref().lock().is_maximized()
+    }
+
     fn content_size(&self) -> Size<Pixels> {
         self.0.as_ref().lock().content_size()
     }
@@ -968,7 +981,7 @@ impl PlatformWindow for MacWindow {
         self.0.as_ref().lock().request_frame_callback = Some(callback);
     }
 
-    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>) {
         self.0.as_ref().lock().event_callback = Some(callback);
     }
 
@@ -1191,7 +1204,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
                     window_state.lock().previous_keydown_inserted_text = Some(text.clone());
                     if let Some(callback) = callback.as_mut() {
                         event.keystroke.ime_key = Some(text.clone());
-                        handled = callback(PlatformInput::KeyDown(event));
+                        handled = !callback(PlatformInput::KeyDown(event)).propagate;
                     }
                 }
             }
@@ -1204,7 +1217,7 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
             let is_held = event.is_held;
 
             if let Some(callback) = callback.as_mut() {
-                handled = callback(PlatformInput::KeyDown(event));
+                handled = !callback(PlatformInput::KeyDown(event)).propagate;
             }
 
             if !handled && is_held {

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

@@ -1,7 +1,7 @@
 use crate::{
-    AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, GlobalPixels, Pixels,
-    PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point,
-    Size, TestPlatform, TileId, WindowAppearance, WindowParams,
+    AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult,
+    GlobalPixels, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler,
+    PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, WindowParams,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -20,7 +20,7 @@ pub(crate) struct TestWindowState {
     platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
     pub(crate) should_close_handler: Option<Box<dyn FnMut() -> bool>>,
-    input_callback: Option<Box<dyn FnMut(PlatformInput) -> bool>>,
+    input_callback: Option<Box<dyn FnMut(PlatformInput) -> DispatchEventResult>>,
     active_status_change_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     moved_callback: Option<Box<dyn FnMut()>>,
@@ -102,7 +102,7 @@ impl TestWindow {
         drop(lock);
         let result = callback(event);
         self.0.lock().input_callback = Some(callback);
-        result
+        !result.propagate
     }
 }
 
@@ -111,6 +111,10 @@ impl PlatformWindow for TestWindow {
         self.0.lock().bounds
     }
 
+    fn is_maximized(&self) -> bool {
+        unimplemented!()
+    }
+
     fn content_size(&self) -> Size<Pixels> {
         self.bounds().size.into()
     }
@@ -120,7 +124,7 @@ impl PlatformWindow for TestWindow {
     }
 
     fn titlebar_height(&self) -> Pixels {
-        unimplemented!()
+        32.0.into()
     }
 
     fn appearance(&self) -> WindowAppearance {
@@ -208,7 +212,7 @@ impl PlatformWindow for TestWindow {
 
     fn on_request_frame(&self, _callback: Box<dyn FnMut()>) {}
 
-    fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>) {
         self.0.lock().input_callback = Some(callback)
     }
 

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

@@ -5,9 +5,9 @@ use crate::{
 };
 use anyhow::{anyhow, Context, Ok, Result};
 use collections::HashMap;
+use cosmic_text::Font as CosmicTextFont;
 use cosmic_text::{
-    fontdb::Query, Attrs, AttrsList, BufferLine, CacheKey, Family, Font as CosmicTextFont,
-    FontSystem, SwashCache,
+    fontdb::Query, Attrs, AttrsList, BufferLine, CacheKey, Family, FontSystem, SwashCache,
 };
 use parking_lot::{RwLock, RwLockUpgradableReadGuard};
 use pathfinder_geometry::{
@@ -31,10 +31,6 @@ struct WindowsTextSystemState {
 impl WindowsTextSystem {
     pub(crate) fn new() -> Self {
         let mut font_system = FontSystem::new();
-
-        // todo(windows) make font loading non-blocking
-        font_system.db_mut().load_system_fonts();
-
         Self(RwLock::new(WindowsTextSystemState {
             font_system,
             swash_cache: SwashCache::new(),
@@ -222,10 +218,11 @@ impl WindowsTextSystemState {
             .get_font_matches(Attrs::new().family(cosmic_text::Family::Name(name)));
         for font in family.as_ref() {
             let font = self.font_system.get_font(*font).unwrap();
-            if font.as_swash().charmap().map('m') == 0 {
-                self.font_system.db_mut().remove_face(font.id());
-                continue;
-            };
+            // TODO: figure out why this is causing fluent icons from loading
+            // if font.as_swash().charmap().map('m') == 0 {
+            //     self.font_system.db_mut().remove_face(font.id());
+            //     continue;
+            // };
 
             let font_id = FontId(self.fonts.len());
             font_ids.push(font_id);

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

@@ -1,4 +1,9 @@
-use windows::Win32::Foundation::{LPARAM, WPARAM};
+use windows::Win32::{
+    Foundation::{HWND, LPARAM, WPARAM},
+    UI::WindowsAndMessaging::{
+        GetWindowLongPtrW, GetWindowLongW, SetWindowLongPtrW, SetWindowLongW, WINDOW_LONG_PTR_INDEX,
+    },
+};
 
 pub(crate) trait HiLoWord {
     fn hiword(&self) -> u16;
@@ -42,3 +47,29 @@ impl HiLoWord for LPARAM {
         (self.0 & 0xFFFF) as i16
     }
 }
+
+pub(crate) unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize {
+    #[cfg(target_pointer_width = "64")]
+    unsafe {
+        GetWindowLongPtrW(hwnd, nindex)
+    }
+    #[cfg(target_pointer_width = "32")]
+    unsafe {
+        GetWindowLongW(hwnd, nindex) as isize
+    }
+}
+
+pub(crate) unsafe fn set_window_long(
+    hwnd: HWND,
+    nindex: WINDOW_LONG_PTR_INDEX,
+    dwnewlong: isize,
+) -> isize {
+    #[cfg(target_pointer_width = "64")]
+    unsafe {
+        SetWindowLongPtrW(hwnd, nindex, dwnewlong)
+    }
+    #[cfg(target_pointer_width = "32")]
+    unsafe {
+        SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize
+    }
+}

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

@@ -1,6 +1,4 @@
 #![deny(unsafe_op_in_unsafe_fn)]
-// todo(windows): remove
-#![allow(unused_variables)]
 
 use std::{
     any::Any,
@@ -14,6 +12,7 @@ use std::{
     sync::{Arc, Once},
 };
 
+use ::util::ResultExt;
 use blade_graphics as gpu;
 use futures::channel::oneshot::{self, Receiver};
 use itertools::Itertools;
@@ -28,6 +27,7 @@ use windows::{
         System::{Com::*, Ole::*, SystemServices::*},
         UI::{
             Controls::*,
+            HiDpi::*,
             Input::{Ime::*, KeyboardAndMouse::*},
             Shell::*,
             WindowsAndMessaging::*,
@@ -48,6 +48,7 @@ pub(crate) struct WindowsWindowInner {
     callbacks: RefCell<Callbacks>,
     platform_inner: Rc<WindowsPlatformInner>,
     handle: AnyWindowHandle,
+    scale_factor: f32,
 }
 
 impl WindowsWindowInner {
@@ -110,9 +111,48 @@ impl WindowsWindowInner {
             callbacks,
             platform_inner,
             handle,
+            scale_factor: 1.0,
         }
     }
 
+    fn is_maximized(&self) -> bool {
+        let mut placement = WINDOWPLACEMENT::default();
+        placement.length = std::mem::size_of::<WINDOWPLACEMENT>() as u32;
+        if unsafe { GetWindowPlacement(self.hwnd, &mut placement) }.is_ok() {
+            return placement.showCmd == SW_SHOWMAXIMIZED.0 as u32;
+        }
+        return false;
+    }
+
+    fn get_titlebar_rect(&self) -> anyhow::Result<RECT> {
+        let top_and_bottom_borders = 2;
+        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 * self.scale_factor).round() as i32 + top_and_bottom_borders;
+
+        if self.is_maximized() {
+            let dpi = unsafe { GetDpiForWindow(self.hwnd) };
+            height += unsafe { (GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) * 2) as i32 };
+        }
+
+        let mut rect = RECT::default();
+        unsafe { GetClientRect(self.hwnd, &mut rect) }?;
+        rect.bottom = rect.top + height;
+        Ok(rect)
+    }
+
     fn is_virtual_key_pressed(&self, vkey: VIRTUAL_KEY) -> bool {
         unsafe { GetKeyState(vkey.0 as i32) < 0 }
     }
@@ -136,12 +176,30 @@ impl WindowsWindowInner {
     fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
         log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0);
         match msg {
+            WM_ACTIVATE => self.handle_activate_msg(msg, wparam, lparam),
+            WM_CREATE => self.handle_create_msg(lparam),
             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_NCHITTEST => self.handle_hit_test_msg(msg, wparam, lparam),
             WM_PAINT => self.handle_paint_msg(),
             WM_CLOSE => self.handle_close_msg(msg, wparam, lparam),
             WM_DESTROY => self.handle_destroy_msg(),
             WM_MOUSEMOVE => self.handle_mouse_move_msg(lparam, wparam),
+            WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(msg, wparam, lparam),
+            WM_NCLBUTTONDOWN => {
+                self.handle_nc_mouse_down_msg(MouseButton::Left, msg, wparam, lparam)
+            }
+            WM_NCRBUTTONDOWN => {
+                self.handle_nc_mouse_down_msg(MouseButton::Right, msg, wparam, lparam)
+            }
+            WM_NCMBUTTONDOWN => {
+                self.handle_nc_mouse_down_msg(MouseButton::Middle, msg, wparam, lparam)
+            }
+            WM_NCLBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Left, msg, wparam, lparam),
+            WM_NCRBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Right, msg, wparam, lparam),
+            WM_NCMBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Middle, msg, wparam, lparam),
             WM_LBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Left, lparam),
             WM_RBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Right, lparam),
             WM_MBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Middle, lparam),
@@ -224,7 +282,7 @@ impl WindowsWindowInner {
 
     fn handle_paint_msg(&self) -> LRESULT {
         let mut paint_struct = PAINTSTRUCT::default();
-        let hdc = unsafe { BeginPaint(self.hwnd, &mut paint_struct) };
+        let _hdc = unsafe { BeginPaint(self.hwnd, &mut paint_struct) };
         let mut callbacks = self.callbacks.borrow_mut();
         if let Some(request_frame) = callbacks.request_frame.as_mut() {
             request_frame();
@@ -291,7 +349,7 @@ impl WindowsWindowInner {
                 pressed_button,
                 modifiers: self.current_modifiers(),
             };
-            if callback(PlatformInput::MouseMove(event)) {
+            if callback(PlatformInput::MouseMove(event)).default_prevented {
                 return LRESULT(0);
             }
         }
@@ -417,7 +475,7 @@ impl WindowsWindowInner {
             keystroke,
             is_held: lparam.0 & (0x1 << 30) > 0,
         };
-        if func(PlatformInput::KeyDown(event)) {
+        if func(PlatformInput::KeyDown(event)).default_prevented {
             self.invalidate_client_area();
             return LRESULT(0);
         }
@@ -434,14 +492,14 @@ impl WindowsWindowInner {
             return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
         };
         let event = KeyUpEvent { keystroke };
-        if func(PlatformInput::KeyUp(event)) {
+        if func(PlatformInput::KeyUp(event)).default_prevented {
             self.invalidate_client_area();
             return LRESULT(0);
         }
         unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) }
     }
 
-    fn handle_keydown_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+    fn handle_keydown_msg(&self, _msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
         let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
             return LRESULT(1);
         };
@@ -452,14 +510,14 @@ impl WindowsWindowInner {
             keystroke,
             is_held: lparam.0 & (0x1 << 30) > 0,
         };
-        if func(PlatformInput::KeyDown(event)) {
+        if func(PlatformInput::KeyDown(event)).default_prevented {
             self.invalidate_client_area();
             return LRESULT(0);
         }
         LRESULT(1)
     }
 
-    fn handle_keyup_msg(&self, message: u32, wparam: WPARAM) -> LRESULT {
+    fn handle_keyup_msg(&self, _msg: u32, wparam: WPARAM) -> LRESULT {
         let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
             return LRESULT(1);
         };
@@ -467,14 +525,14 @@ impl WindowsWindowInner {
             return LRESULT(1);
         };
         let event = KeyUpEvent { keystroke };
-        if func(PlatformInput::KeyUp(event)) {
+        if func(PlatformInput::KeyUp(event)).default_prevented {
             self.invalidate_client_area();
             return LRESULT(0);
         }
         LRESULT(1)
     }
 
-    fn handle_char_msg(&self, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+    fn handle_char_msg(&self, _msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
         let Some(keystroke) = self.parse_char_msg_keystroke(wparam) else {
             return LRESULT(1);
         };
@@ -487,7 +545,7 @@ impl WindowsWindowInner {
             keystroke,
             is_held: lparam.0 & (0x1 << 30) > 0,
         };
-        if func(PlatformInput::KeyDown(event)) {
+        if func(PlatformInput::KeyDown(event)).default_prevented {
             self.invalidate_client_area();
             return LRESULT(0);
         }
@@ -515,7 +573,7 @@ impl WindowsWindowInner {
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
-            if callback(PlatformInput::MouseDown(event)) {
+            if callback(PlatformInput::MouseDown(event)).default_prevented {
                 return LRESULT(0);
             }
         }
@@ -533,7 +591,7 @@ impl WindowsWindowInner {
                 modifiers: self.current_modifiers(),
                 click_count: 1,
             };
-            if callback(PlatformInput::MouseUp(event)) {
+            if callback(PlatformInput::MouseUp(event)).default_prevented {
                 return LRESULT(0);
             }
         }
@@ -578,7 +636,7 @@ impl WindowsWindowInner {
                 modifiers: self.current_modifiers(),
                 touch_phase: TouchPhase::Moved,
             };
-            if callback(PlatformInput::ScrollWheel(event)) {
+            if callback(PlatformInput::ScrollWheel(event)).default_prevented {
                 return LRESULT(0);
             }
         }
@@ -685,12 +743,237 @@ impl WindowsWindowInner {
         };
         func(input);
     }
+
+    /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
+    fn handle_calc_client_size(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+        if wparam.0 == 0 {
+            return unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
+        }
+
+        let dpi = unsafe { GetDpiForWindow(self.hwnd) };
+
+        let frame_x = unsafe { GetSystemMetricsForDpi(SM_CXFRAME, dpi) };
+        let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
+        let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
+
+        // 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;
+
+        LRESULT(0)
+    }
+
+    fn handle_activate_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+        if let Some(titlebar_rect) = self.get_titlebar_rect().log_err() {
+            unsafe { InvalidateRect(self.hwnd, Some(&titlebar_rect), FALSE) };
+        }
+        return unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
+    }
+
+    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),
+        });
+
+        // Inform the application of the frame change to force redrawing with the new
+        // client area that is extended into the title bar
+        unsafe {
+            SetWindowPos(
+                self.hwnd,
+                HWND::default(),
+                size_rect.left,
+                size_rect.top,
+                width,
+                height,
+                SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE,
+            )
+            .log_err()
+        };
+        LRESULT(0)
+    }
+
+    fn handle_dpi_changed_msg(&self, _msg: u32, _wparam: WPARAM, _lparam: LPARAM) -> LRESULT {
+        LRESULT(1)
+    }
+
+    fn handle_hit_test_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+        // default handler for resize areas
+        let hit = unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
+        if matches!(
+            hit.0 as u32,
+            HTNOWHERE
+                | HTRIGHT
+                | HTLEFT
+                | HTTOPLEFT
+                | HTTOP
+                | HTTOPRIGHT
+                | HTBOTTOMRIGHT
+                | HTBOTTOM
+                | HTBOTTOMLEFT
+        ) {
+            return hit;
+        }
+
+        let dpi = unsafe { GetDpiForWindow(self.hwnd) };
+        let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
+        let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
+
+        let mut cursor_point = POINT {
+            x: lparam.signed_loword().into(),
+            y: lparam.signed_hiword().into(),
+        };
+        unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
+        if cursor_point.y > 0 && cursor_point.y < frame_y + padding {
+            return LRESULT(HTTOP as _);
+        }
+
+        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) };
+                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 {
+                    return LRESULT(HTMAXBUTTON as _);
+                } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 3 {
+                    return LRESULT(HTMINBUTTON as _);
+                }
+
+                return LRESULT(HTCAPTION as _);
+            }
+        }
+
+        LRESULT(HTCLIENT as _)
+    }
+
+    fn handle_nc_mouse_move_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
+        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 event = MouseMoveEvent {
+                position: Point { x, y },
+                pressed_button: None,
+                modifiers: self.current_modifiers(),
+            };
+            if callback(PlatformInput::MouseMove(event)).default_prevented {
+                return LRESULT(0);
+            }
+        }
+        drop(callbacks);
+        unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) }
+    }
+
+    fn handle_nc_mouse_down_msg(
+        &self,
+        button: MouseButton,
+        msg: u32,
+        wparam: WPARAM,
+        lparam: LPARAM,
+    ) -> LRESULT {
+        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 x = Pixels::from(cursor_point.x as f32);
+            let y = Pixels::from(cursor_point.y as f32);
+            let event = MouseDownEvent {
+                button: button.clone(),
+                position: Point { x, y },
+                modifiers: self.current_modifiers(),
+                click_count: 1,
+            };
+            if callback(PlatformInput::MouseDown(event)).default_prevented {
+                return LRESULT(0);
+            }
+        }
+        drop(callbacks);
+
+        match wparam.0 as u32 {
+            // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc
+            HTMINBUTTON | HTMAXBUTTON | HTCLOSE => LRESULT(0),
+            _ => unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) },
+        }
+    }
+
+    fn handle_nc_mouse_up_msg(
+        &self,
+        button: MouseButton,
+        msg: u32,
+        wparam: WPARAM,
+        lparam: LPARAM,
+    ) -> LRESULT {
+        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 x = Pixels::from(cursor_point.x as f32);
+            let y = Pixels::from(cursor_point.y as f32);
+            let event = MouseUpEvent {
+                button,
+                position: Point { x, y },
+                modifiers: self.current_modifiers(),
+                click_count: 1,
+            };
+            if callback(PlatformInput::MouseUp(event)).default_prevented {
+                return LRESULT(0);
+            }
+        }
+        drop(callbacks);
+
+        if button == MouseButton::Left {
+            match wparam.0 as u32 {
+                HTMINBUTTON => unsafe {
+                    ShowWindowAsync(self.hwnd, SW_MINIMIZE);
+                    return LRESULT(0);
+                },
+                HTMAXBUTTON => unsafe {
+                    if self.is_maximized() {
+                        ShowWindowAsync(self.hwnd, SW_NORMAL);
+                    } else {
+                        ShowWindowAsync(self.hwnd, SW_MAXIMIZE);
+                    }
+                    return LRESULT(0);
+                },
+                HTCLOSE => unsafe {
+                    PostMessageW(self.hwnd, WM_CLOSE, WPARAM::default(), LPARAM::default())
+                        .log_err();
+                    return LRESULT(0);
+                },
+                _ => {}
+            };
+        }
+
+        unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) }
+    }
 }
 
 #[derive(Default)]
 struct Callbacks {
     request_frame: Option<Box<dyn FnMut()>>,
-    input: Option<Box<dyn FnMut(crate::PlatformInput) -> bool>>,
+    input: Option<Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>>,
     active_status_change: Option<Box<dyn FnMut(bool)>>,
     resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
     fullscreen: Option<Box<dyn FnMut(bool)>>,
@@ -718,7 +1001,6 @@ impl WindowsWindow {
         handle: AnyWindowHandle,
         options: WindowParams,
     ) -> Self {
-        let dwexstyle = WINDOW_EX_STYLE::default();
         let classname = register_wnd_class();
         let windowname = HSTRING::from(
             options
@@ -728,7 +1010,7 @@ impl WindowsWindow {
                 .map(|title| title.as_ref())
                 .unwrap_or(""),
         );
-        let dwstyle = WS_OVERLAPPEDWINDOW & !WS_VISIBLE;
+        let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
         let x = options.bounds.origin.x.0 as i32;
         let y = options.bounds.origin.y.0 as i32;
         let nwidth = options.bounds.size.width.0 as i32;
@@ -744,7 +1026,7 @@ impl WindowsWindow {
         let lpparam = Some(&context as *const _ as *const _);
         unsafe {
             CreateWindowExW(
-                dwexstyle,
+                WS_EX_APPWINDOW,
                 classname,
                 &windowname,
                 dwstyle,
@@ -785,7 +1067,7 @@ impl WindowsWindow {
     }
 
     fn maximize(&self) {
-        unsafe { ShowWindow(self.inner.hwnd, SW_MAXIMIZE) };
+        unsafe { ShowWindowAsync(self.inner.hwnd, SW_MAXIMIZE) };
     }
 }
 
@@ -826,6 +1108,10 @@ impl PlatformWindow for WindowsWindow {
         }
     }
 
+    fn is_maximized(&self) -> bool {
+        self.inner.is_maximized()
+    }
+
     // todo(windows)
     fn content_size(&self) -> Size<Pixels> {
         let size = self.inner.size.get();
@@ -837,12 +1123,12 @@ impl PlatformWindow for WindowsWindow {
 
     // todo(windows)
     fn scale_factor(&self) -> f32 {
-        1.0
+        self.inner.scale_factor
     }
 
-    // todo(windows)
     fn titlebar_height(&self) -> Pixels {
-        20.0.into()
+        let titlebar_rect = self.inner.get_titlebar_rect().unwrap();
+        ((titlebar_rect.bottom - titlebar_rect.top) as f64).into()
     }
 
     // todo(windows)
@@ -952,8 +1238,9 @@ impl PlatformWindow for WindowsWindow {
         Some(done_rx)
     }
 
-    // todo(windows)
-    fn activate(&self) {}
+    fn activate(&self) {
+        unsafe { ShowWindowAsync(self.inner.hwnd, SW_NORMAL) };
+    }
 
     // todo(windows)
     fn set_title(&mut self, title: &str) {
@@ -963,16 +1250,18 @@ impl PlatformWindow for WindowsWindow {
     }
 
     // todo(windows)
-    fn set_edited(&mut self, edited: bool) {}
+    fn set_edited(&mut self, _edited: bool) {}
 
     // todo(windows)
     fn show_character_palette(&self) {}
 
-    // todo(windows)
-    fn minimize(&self) {}
+    fn minimize(&self) {
+        unsafe { ShowWindowAsync(self.inner.hwnd, SW_MINIMIZE) };
+    }
 
-    // todo(windows)
-    fn zoom(&self) {}
+    fn zoom(&self) {
+        unsafe { ShowWindowAsync(self.inner.hwnd, SW_MAXIMIZE) };
+    }
 
     // todo(windows)
     fn toggle_fullscreen(&self) {}
@@ -988,7 +1277,7 @@ impl PlatformWindow for WindowsWindow {
     }
 
     // todo(windows)
-    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> bool>) {
+    fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
         self.inner.callbacks.borrow_mut().input = Some(callback);
     }
 
@@ -1028,7 +1317,7 @@ impl PlatformWindow for WindowsWindow {
     }
 
     // todo(windows)
-    fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool {
+    fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
         true
     }
 
@@ -1046,6 +1335,7 @@ impl PlatformWindow for WindowsWindow {
 #[implement(IDropTarget)]
 struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
 
+#[allow(non_snake_case)]
 impl IDropTarget_Impl for WindowsDragDropHandler {
     fn DragEnter(
         &self,
@@ -1159,6 +1449,7 @@ fn register_wnd_class() -> PCWSTR {
             lpfnWndProc: Some(wnd_proc),
             hCursor: unsafe { LoadCursorW(None, IDC_ARROW).ok().unwrap() },
             lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
+            style: CS_HREDRAW | CS_VREDRAW,
             ..Default::default()
         };
         unsafe { RegisterClassW(&wc) };
@@ -1216,28 +1507,6 @@ pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option<Rc<WindowsWindowInner>>
     }
 }
 
-unsafe fn get_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX) -> isize {
-    #[cfg(target_pointer_width = "64")]
-    unsafe {
-        GetWindowLongPtrW(hwnd, nindex)
-    }
-    #[cfg(target_pointer_width = "32")]
-    unsafe {
-        GetWindowLongW(hwnd, nindex) as isize
-    }
-}
-
-unsafe fn set_window_long(hwnd: HWND, nindex: WINDOW_LONG_PTR_INDEX, dwnewlong: isize) -> isize {
-    #[cfg(target_pointer_width = "64")]
-    unsafe {
-        SetWindowLongPtrW(hwnd, nindex, dwnewlong)
-    }
-    #[cfg(target_pointer_width = "32")]
-    unsafe {
-        SetWindowLongW(hwnd, nindex, dwnewlong as i32) as isize
-    }
-}
-
 fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
     match code {
         // VK_0 - VK_9

crates/gpui/src/window.rs 🔗

@@ -477,7 +477,7 @@ impl Window {
                 handle
                     .update(&mut cx, |_, cx| cx.dispatch_event(event))
                     .log_err()
-                    .unwrap_or(false)
+                    .unwrap_or(DispatchEventResult::default())
             })
         });
 
@@ -531,6 +531,12 @@ impl Window {
     }
 }
 
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
+pub(crate) struct DispatchEventResult {
+    pub propagate: bool,
+    pub default_prevented: bool,
+}
+
 /// Indicates which region of the window is visible. Content falling outside of this mask will not be
 /// rendered. Currently, only rectangular content masks are supported, but we give the mask its own type
 /// to leave room to support more complex shapes in the future.
@@ -644,6 +650,17 @@ impl<'a> WindowContext<'a> {
         style
     }
 
+    /// Get the platform window titlebar height
+    pub fn titlebar_height(&self) -> Pixels {
+        self.window.platform_window.titlebar_height()
+    }
+
+    /// Check if the platform window is maximized
+    /// On some platforms (namely Windows) this is different than the bounds being the size of the display
+    pub fn is_maximized(&self) -> bool {
+        self.window.platform_window.is_maximized()
+    }
+
     /// Dispatch the given action on the currently focused element.
     pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
         let focus_handle = self.focused();
@@ -1044,10 +1061,11 @@ impl<'a> WindowContext<'a> {
     /// You can create a keystroke with Keystroke::parse("").
     pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool {
         let keystroke = keystroke.with_simulated_ime();
-        if self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent {
+        let result = self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent {
             keystroke: keystroke.clone(),
             is_held: false,
-        })) {
+        }));
+        if !result.propagate {
             return true;
         }
 
@@ -1080,7 +1098,7 @@ impl<'a> WindowContext<'a> {
 
     /// Dispatch a mouse or keyboard event on the window.
     #[profiling::function]
-    pub fn dispatch_event(&mut self, event: PlatformInput) -> bool {
+    pub fn dispatch_event(&mut self, event: PlatformInput) -> DispatchEventResult {
         self.window.last_input_timestamp.set(Instant::now());
         // Handlers may set this to false by calling `stop_propagation`.
         self.app.propagate_event = true;
@@ -1168,7 +1186,10 @@ impl<'a> WindowContext<'a> {
             self.dispatch_key_event(any_key_event);
         }
 
-        !self.app.propagate_event
+        DispatchEventResult {
+            propagate: self.app.propagate_event,
+            default_prevented: self.window.default_prevented,
+        }
     }
 
     fn dispatch_mouse_event(&mut self, event: &dyn Any) {

crates/ui/src/components.rs 🔗

@@ -9,6 +9,7 @@ mod indicator;
 mod keybinding;
 mod label;
 mod list;
+mod platform_titlebar;
 mod popover;
 mod popover_menu;
 mod right_click_menu;
@@ -31,6 +32,7 @@ pub use indicator::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;
+pub use platform_titlebar::*;
 pub use popover::*;
 pub use popover_menu::*;
 pub use right_click_menu::*;

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

@@ -0,0 +1,226 @@
+// allowing due to multiple platform conditional code
+#![allow(unused_imports)]
+
+use gpui::{
+    div,
+    prelude::FluentBuilder,
+    px, AnyElement, Div, Element, ElementId, Fill, InteractiveElement, Interactivity, IntoElement,
+    ParentElement, Pixels, RenderOnce, Rgba, Stateful, StatefulInteractiveElement, StyleRefinement,
+    Styled,
+    WindowAppearance::{Dark, Light, VibrantDark, VibrantLight},
+    WindowContext,
+};
+use smallvec::SmallVec;
+
+use crate::h_flex;
+
+pub enum PlatformStyle {
+    Linux,
+    Windows,
+    MacOs,
+}
+
+impl PlatformStyle {
+    pub fn platform() -> Self {
+        if cfg!(windows) {
+            Self::Windows
+        } else if cfg!(macos) {
+            Self::MacOs
+        } else {
+            Self::Linux
+        }
+    }
+
+    pub fn windows(&self) -> bool {
+        matches!(self, Self::Windows)
+    }
+
+    pub fn macos(&self) -> bool {
+        matches!(self, Self::MacOs)
+    }
+}
+
+#[derive(IntoElement)]
+pub struct PlatformTitlebar {
+    platform: PlatformStyle,
+    titlebar_bg: Option<Fill>,
+    content: Stateful<Div>,
+    children: SmallVec<[AnyElement; 2]>,
+}
+
+impl Styled for PlatformTitlebar {
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.content.style()
+    }
+}
+
+impl PlatformTitlebar {
+    /// Change the platform style used
+    pub fn with_platform_style(self, style: PlatformStyle) -> Self {
+        Self {
+            platform: style,
+            ..self
+        }
+    }
+
+    fn titlebar_top_padding(&self, cx: &WindowContext) -> Pixels {
+        if self.platform.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.0)
+        } else {
+            px(0.0)
+        }
+    }
+
+    fn windows_caption_button_width(_cx: &WindowContext) -> Pixels {
+        // todo(windows): get padding from win32 api, need HWND from window context somehow
+        // should be GetSystemMetricsForDpi(SM_CXSIZE, dpi)
+        px(36.0)
+    }
+
+    fn render_window_controls_right(&self, cx: &mut WindowContext) -> impl Element {
+        if self.platform.windows() {
+            let btn_height = cx.titlebar_height() - self.titlebar_top_padding(cx);
+            let close_btn_hover_color = Rgba {
+                r: 232.0 / 255.0,
+                g: 17.0 / 255.0,
+                b: 32.0 / 255.0,
+                a: 1.0,
+            };
+
+            let btn_hover_color = match cx.appearance() {
+                Light | VibrantLight => Rgba {
+                    r: 0.1,
+                    g: 0.1,
+                    b: 0.1,
+                    a: 0.2,
+                },
+                Dark | VibrantDark => Rgba {
+                    r: 0.9,
+                    g: 0.9,
+                    b: 0.9,
+                    a: 0.1,
+                },
+            };
+
+            fn windows_caption_btn(
+                id: &'static str,
+                icon_text: &'static str,
+                hover_color: Rgba,
+                cx: &WindowContext,
+            ) -> Stateful<Div> {
+                let mut active_color = hover_color;
+                active_color.a *= 0.2;
+                h_flex()
+                    .id(id)
+                    .h_full()
+                    .justify_center()
+                    .content_center()
+                    .items_center()
+                    .w(PlatformTitlebar::windows_caption_button_width(cx))
+                    .hover(|style| style.bg(hover_color))
+                    .active(|style| style.bg(active_color))
+                    .child(icon_text)
+            }
+
+            div()
+                .id("caption-buttons-windows")
+                .flex()
+                .flex_row()
+                .justify_center()
+                .content_stretch()
+                .max_h(btn_height)
+                .min_h(btn_height)
+                .font("Segoe Fluent Icons")
+                .text_size(gpui::Pixels(10.0))
+                .children(vec![
+                    windows_caption_btn("minimize", "\u{e921}", btn_hover_color, cx), // minimize icon
+                    windows_caption_btn(
+                        "maximize",
+                        if cx.is_maximized() {
+                            "\u{e923}" // restore icon
+                        } else {
+                            "\u{e922}" // maximize icon
+                        },
+                        btn_hover_color,
+                        cx,
+                    ),
+                    windows_caption_btn("close", "\u{e8bb}", close_btn_hover_color, cx), // close icon
+                ])
+        } else {
+            div().id("caption-buttons-windows")
+        }
+    }
+
+    /// Sets the background color of titlebar.
+    pub fn titlebar_bg<F>(mut self, fill: F) -> Self
+    where
+        F: Into<Fill>,
+        Self: Sized,
+    {
+        self.titlebar_bg = Some(fill.into());
+        self
+    }
+}
+
+pub fn platform_titlebar(id: impl Into<ElementId>) -> PlatformTitlebar {
+    let id = id.into();
+    PlatformTitlebar {
+        platform: PlatformStyle::platform(),
+        titlebar_bg: None,
+        content: div().id(id.clone()),
+        children: SmallVec::new(),
+    }
+}
+
+impl RenderOnce for PlatformTitlebar {
+    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+        let titlebar_height = cx.titlebar_height();
+        let titlebar_top_padding = self.titlebar_top_padding(cx);
+        let window_controls_right = self.render_window_controls_right(cx);
+        let macos = self.platform.macos();
+        h_flex()
+            .id("titlebar")
+            .w_full()
+            .pt(titlebar_top_padding)
+            .max_h(titlebar_height)
+            .min_h(titlebar_height)
+            .map(|mut this| {
+                this.style().background = self.titlebar_bg;
+
+                if macos {
+                    if !cx.is_fullscreen() {
+                        // Use pixels here instead of a rem-based size because the macOS traffic
+                        // lights are a static size, and don't scale with the rest of the UI.
+                        return this.pl(px(80.));
+                    }
+                }
+
+                this
+            })
+            .content_stretch()
+            .child(
+                self.content
+                    .flex()
+                    .flex_row()
+                    .w_full()
+                    .id("titlebar-content")
+                    .children(self.children),
+            )
+            .child(window_controls_right)
+    }
+}
+
+impl InteractiveElement for PlatformTitlebar {
+    fn interactivity(&mut self) -> &mut Interactivity {
+        self.content.interactivity()
+    }
+}
+impl StatefulInteractiveElement for PlatformTitlebar {}
+
+impl ParentElement for PlatformTitlebar {
+    fn extend(&mut self, elements: impl Iterator<Item = AnyElement>) {
+        self.children.extend(elements)
+    }
+}

crates/workspace/src/workspace.rs 🔗

@@ -26,7 +26,7 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, canvas, div, impl_actions, point, px, size, Action, AnyElement, AnyModel, AnyView,
+    actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
     AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
     DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
     FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke,
@@ -4742,10 +4742,6 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
     Some(size((width as f64).into(), (height as f64).into()))
 }
 
-pub fn titlebar_height(cx: &mut WindowContext) -> Pixels {
-    (1.75 * cx.rem_size()).max(px(32.))
-}
-
 struct DisconnectedOverlay;
 
 impl Element for DisconnectedOverlay {
@@ -4759,7 +4755,7 @@ impl Element for DisconnectedOverlay {
             .bg(background)
             .absolute()
             .left_0()
-            .top(titlebar_height(cx))
+            .top(cx.titlebar_height())
             .size_full()
             .flex()
             .items_center()