From 328aa2cc95f7278b45f18038e97d2d16b217f675 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 15 Mar 2024 10:40:58 -0700 Subject: [PATCH] Cross-platform titlebar (#9405) This PR reverts https://github.com/zed-industries/zed/pull/9392 and fixes the regressions that led to the reversion. Release Notes: - N/A --------- Co-authored-by: Ezekiel Warren --- Cargo.toml | 11 + crates/collab_ui/src/collab_titlebar_item.rs | 35 +- crates/gpui/src/platform.rs | 18 +- .../gpui/src/platform/linux/wayland/window.rs | 16 +- crates/gpui/src/platform/linux/x11/window.rs | 16 +- crates/gpui/src/platform/mac/status_item.rs | 4 - crates/gpui/src/platform/mac/window.rs | 24 +- crates/gpui/src/platform/test/window.rs | 25 +- crates/gpui/src/platform/windows.rs | 2 + crates/gpui/src/platform/windows/display.rs | 3 +- .../gpui/src/platform/windows/text_system.rs | 17 +- crates/gpui/src/platform/windows/util.rs | 26 ++ crates/gpui/src/platform/windows/window.rs | 382 +++++++++++++++--- crates/gpui/src/window.rs | 34 +- crates/ui/src/components.rs | 2 + crates/ui/src/components/platform_titlebar.rs | 228 +++++++++++ crates/workspace/src/workspace.rs | 32 +- 17 files changed, 719 insertions(+), 156 deletions(-) create mode 100644 crates/ui/src/components/platform_titlebar.rs diff --git a/Cargo.toml b/Cargo.toml index 041c3214a1..1e4c2bcd55 100644 --- a/Cargo.toml +++ b/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", diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index f2aa5c1184..4ef63c5a14 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/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,17 @@ impl Render for CollabTitlebarItem { let project_id = self.project.read(cx).remote_id(); let workspace = self.workspace.upgrade(); - h_flex() - .id("titlebar") + 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(); + } + }) + }) .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(); - } - }) // left side .child( h_flex() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 77f73a6956..5244ca65cf 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -1,6 +1,6 @@ // todo(linux): remove #![cfg_attr(target_os = "linux", allow(dead_code))] -// todo("windows"): remove +// todo(windows): remove #![cfg_attr(windows, allow(dead_code))] mod app_menu; @@ -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; @@ -170,9 +170,9 @@ unsafe impl Send for DisplayId {} pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn bounds(&self) -> Bounds; + fn is_maximized(&self) -> bool; fn content_size(&self) -> Size; fn scale_factor(&self) -> f32; - fn titlebar_height(&self) -> Pixels; fn appearance(&self) -> WindowAppearance; fn display(&self) -> Rc; fn mouse_position(&self) -> Point; @@ -196,7 +196,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn toggle_fullscreen(&self); fn is_fullscreen(&self) -> bool; fn on_request_frame(&self, callback: Box); - fn on_input(&self, callback: Box bool>); + fn on_input(&self, callback: Box DispatchEventResult>); fn on_active_status_change(&self, callback: Box); fn on_resize(&self, callback: Box, f32)>); fn on_fullscreen(&self, callback: Box); @@ -206,9 +206,11 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_appearance_changed(&self, callback: Box); fn is_topmost_for_position(&self, position: Point) -> bool; fn draw(&self, scene: &Scene); - fn sprite_atlas(&self) -> Arc; + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::HWND; + #[cfg(any(test, feature = "test-support"))] fn as_test(&mut self) -> Option<&mut TestWindow> { None diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 897a32a420..f7e3cdbe1e 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -29,7 +29,7 @@ use crate::{ #[derive(Default)] pub(crate) struct Callbacks { request_frame: Option>, - input: Option bool>>, + input: Option crate::DispatchEventResult>>, active_status_change: Option>, resize: Option, f32)>>, fullscreen: Option>, @@ -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 { + false + } + fn content_size(&self) -> Size { let inner = self.0.inner.borrow_mut(); Size { @@ -291,11 +296,6 @@ impl PlatformWindow for WaylandWindow { self.0.inner.borrow_mut().scale } - // todo(linux) - fn titlebar_height(&self) -> Pixels { - unimplemented!() - } - // todo(linux) fn appearance(&self) -> WindowAppearance { WindowAppearance::Light @@ -378,7 +378,7 @@ impl PlatformWindow for WaylandWindow { self.0.callbacks.borrow_mut().request_frame = Some(callback); } - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box crate::DispatchEventResult>) { self.0.callbacks.borrow_mut().input = Some(callback); } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 3b07b5c587..95c17624b4 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -31,7 +31,7 @@ use super::X11Display; #[derive(Default)] struct Callbacks { request_frame: Option>, - input: Option bool>>, + input: Option crate::DispatchEventResult>>, active_status_change: Option>, resize: Option, f32)>>, fullscreen: Option>, @@ -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 { + false + } + fn content_size(&self) -> Size { self.0.inner.borrow_mut().content_size() } @@ -341,11 +346,6 @@ impl PlatformWindow for X11Window { self.0.inner.borrow_mut().scale_factor } - // todo(linux) - fn titlebar_height(&self) -> Pixels { - unimplemented!() - } - // todo(linux) fn appearance(&self) -> WindowAppearance { WindowAppearance::Light @@ -451,7 +451,7 @@ impl PlatformWindow for X11Window { self.0.callbacks.borrow_mut().request_frame = Some(callback); } - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box crate::DispatchEventResult>) { self.0.callbacks.borrow_mut().input = Some(callback); } diff --git a/crates/gpui/src/platform/mac/status_item.rs b/crates/gpui/src/platform/mac/status_item.rs index 39df3f15c0..21cc86090c 100644 --- a/crates/gpui/src/platform/mac/status_item.rs +++ b/crates/gpui/src/platform/mac/status_item.rs @@ -182,10 +182,6 @@ impl platform::Window for StatusItem { self.0.borrow().scale_factor() } - fn titlebar_height(&self) -> f32 { - 0. - } - fn appearance(&self) -> platform::Appearance { unsafe { let appearance: id = diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 8240b21101..c5767ad27a 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -324,7 +324,7 @@ struct MacWindowState { renderer: renderer::Renderer, kind: WindowKind, request_frame_callback: Option>, - event_callback: Option bool>>, + event_callback: Option crate::DispatchEventResult>>, activate_callback: Option>, resize_callback: Option, f32)>>, fullscreen_callback: Option>, @@ -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,10 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().bounds() } + fn is_maximized(&self) -> bool { + self.0.as_ref().lock().is_maximized() + } + fn content_size(&self) -> Size { self.0.as_ref().lock().content_size() } @@ -724,10 +736,6 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().scale_factor() } - fn titlebar_height(&self) -> Pixels { - self.0.as_ref().lock().titlebar_height() - } - fn appearance(&self) -> WindowAppearance { unsafe { let appearance: id = msg_send![self.0.lock().native_window, effectiveAppearance]; @@ -968,7 +976,7 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().request_frame_callback = Some(callback); } - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box crate::DispatchEventResult>) { self.0.as_ref().lock().event_callback = Some(callback); } @@ -1191,7 +1199,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 +1212,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 { diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index f9150b409a..b0b25c7a33 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/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, sprite_atlas: Arc, pub(crate) should_close_handler: Option bool>>, - input_callback: Option bool>>, + input_callback: Option DispatchEventResult>>, active_status_change_callback: Option>, resize_callback: Option, f32)>>, moved_callback: Option>, @@ -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 { + false + } + fn content_size(&self) -> Size { self.bounds().size.into() } @@ -119,10 +123,6 @@ impl PlatformWindow for TestWindow { 2.0 } - fn titlebar_height(&self) -> Pixels { - unimplemented!() - } - fn appearance(&self) -> WindowAppearance { WindowAppearance::Light } @@ -208,7 +208,7 @@ impl PlatformWindow for TestWindow { fn on_request_frame(&self, _callback: Box) {} - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box DispatchEventResult>) { self.0.lock().input_callback = Some(callback) } @@ -249,6 +249,11 @@ impl PlatformWindow for TestWindow { fn as_test(&mut self) -> Option<&mut TestWindow> { Some(self) } + + #[cfg(target_os = "windows")] + fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + unimplemented!() + } } pub(crate) struct TestAtlasState { diff --git a/crates/gpui/src/platform/windows.rs b/crates/gpui/src/platform/windows.rs index 4347b9169a..c2bc5dbdbd 100644 --- a/crates/gpui/src/platform/windows.rs +++ b/crates/gpui/src/platform/windows.rs @@ -11,3 +11,5 @@ pub(crate) use platform::*; pub(crate) use text_system::*; pub(crate) use util::*; pub(crate) use window::*; + +pub(crate) use windows::Win32::Foundation::HWND; diff --git a/crates/gpui/src/platform/windows/display.rs b/crates/gpui/src/platform/windows/display.rs index 5c67cb80b3..6eff3c7870 100644 --- a/crates/gpui/src/platform/windows/display.rs +++ b/crates/gpui/src/platform/windows/display.rs @@ -1,7 +1,6 @@ -use std::rc::Rc; - use itertools::Itertools; use smallvec::SmallVec; +use std::rc::Rc; use uuid::Uuid; use windows::Win32::{Foundation::*, Graphics::Gdi::*}; diff --git a/crates/gpui/src/platform/windows/text_system.rs b/crates/gpui/src/platform/windows/text_system.rs index 7f6c356d0a..f68c74c7f7 100644 --- a/crates/gpui/src/platform/windows/text_system.rs +++ b/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); diff --git a/crates/gpui/src/platform/windows/util.rs b/crates/gpui/src/platform/windows/util.rs index e310965577..7093f564ca 100644 --- a/crates/gpui/src/platform/windows/util.rs +++ b/crates/gpui/src/platform/windows/util.rs @@ -42,3 +42,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 + } +} diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index cbb0a472a6..06c821982f 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/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, platform_inner: Rc, 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::() 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 { + 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>, - input: Option bool>>, + input: Option DispatchEventResult>>, active_status_change: Option>, resize: Option, f32)>>, fullscreen: Option>, @@ -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 { let size = self.inner.size.get(); @@ -837,12 +1123,7 @@ impl PlatformWindow for WindowsWindow { // todo(windows) fn scale_factor(&self) -> f32 { - 1.0 - } - - // todo(windows) - fn titlebar_height(&self) -> Pixels { - 20.0.into() + self.inner.scale_factor } // todo(windows) @@ -952,8 +1233,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 +1245,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 +1272,7 @@ impl PlatformWindow for WindowsWindow { } // todo(windows) - fn on_input(&self, callback: Box bool>) { + fn on_input(&self, callback: Box DispatchEventResult>) { self.inner.callbacks.borrow_mut().input = Some(callback); } @@ -1028,7 +1312,7 @@ impl PlatformWindow for WindowsWindow { } // todo(windows) - fn is_topmost_for_position(&self, position: Point) -> bool { + fn is_topmost_for_position(&self, _position: Point) -> bool { true } @@ -1041,11 +1325,16 @@ impl PlatformWindow for WindowsWindow { fn sprite_atlas(&self) -> Arc { self.inner.renderer.borrow().sprite_atlas().clone() } + + fn get_raw_handle(&self) -> HWND { + self.inner.hwnd + } } #[implement(IDropTarget)] struct WindowsDragDropHandler(pub Rc); +#[allow(non_snake_case)] impl IDropTarget_Impl for WindowsDragDropHandler { fn DragEnter( &self, @@ -1159,6 +1448,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 +1506,6 @@ pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option> } } -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 { match code { // VK_0 - VK_9 diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e6591e291d..062f174c03 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -520,7 +520,7 @@ impl Window { handle .update(&mut cx, |_, cx| cx.dispatch_event(event)) .log_err() - .unwrap_or(false) + .unwrap_or(DispatchEventResult::default()) }) }); @@ -574,6 +574,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. @@ -687,6 +693,12 @@ impl<'a> WindowContext<'a> { style } + /// 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) { let focus_handle = self.focused(); @@ -1087,10 +1099,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; } @@ -1123,7 +1136,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; @@ -1211,7 +1224,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) { @@ -1683,6 +1699,14 @@ impl<'a> WindowContext<'a> { } } +#[cfg(target_os = "windows")] +impl WindowContext<'_> { + /// Returns the raw HWND handle for the window. + pub fn get_raw_handle(&self) -> windows::Win32::Foundation::HWND { + self.window.platform_window.get_raw_handle() + } +} + impl Context for WindowContext<'_> { type Result = T; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 0848ac74df..c78774f2b8 100644 --- a/crates/ui/src/components.rs +++ b/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::*; diff --git a/crates/ui/src/components/platform_titlebar.rs b/crates/ui/src/components/platform_titlebar.rs new file mode 100644 index 0000000000..93a274c9b8 --- /dev/null +++ b/crates/ui/src/components/platform_titlebar.rs @@ -0,0 +1,228 @@ +// allowing due to multiple platform conditional code +#![allow(unused_imports)] + +use gpui::{ + div, + prelude::FluentBuilder, + px, transparent_black, 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, +} + +pub fn titlebar_height(cx: &mut WindowContext) -> Pixels { + (1.75 * cx.rem_size()).max(px(32.)) +} + +impl PlatformStyle { + pub fn platform() -> Self { + if cfg!(target_os = "windows") { + Self::Windows + } else if cfg!(target_os = "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: Fill, + content: Stateful
, + 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 = titlebar_height(cx) - 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
{ + 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(px(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(mut self, fill: F) -> Self + where + F: Into, + Self: Sized, + { + self.titlebar_bg = fill.into(); + self + } +} + +pub fn platform_titlebar(id: impl Into) -> PlatformTitlebar { + let id = id.into(); + PlatformTitlebar { + platform: PlatformStyle::platform(), + titlebar_bg: transparent_black().into(), + content: div().id(id.clone()), + children: SmallVec::new(), + } +} + +impl RenderOnce for PlatformTitlebar { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let titlebar_height = titlebar_height(cx); + 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) + .h(titlebar_height) + .map(|this| { + if cx.is_fullscreen() { + this.pl_2() + } else if macos { + // 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.)) + } else { + this.pl_2() + } + }) + .bg(self.titlebar_bg) + .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) { + self.children.extend(elements) + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7edb2aa8c6..16dc059435 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -26,13 +26,11 @@ use futures::{ Future, FutureExt, StreamExt, }; use gpui::{ - actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyView, AnyWeakView, - AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, DragMoveEvent, Element, - ElementContext, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, Global, - GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke, LayoutId, ManagedView, - Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, - SharedString, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, WindowHandle, WindowOptions, + actions, canvas, impl_actions, point, size, Action, AnyElement, AnyView, AnyWeakView, + AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DragMoveEvent, Entity as _, EntityId, + EventEmitter, FocusHandle, FocusableView, Global, GlobalPixels, KeyContext, Keystroke, + LayoutId, ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, + Size, Subscription, Task, View, WeakView, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -72,7 +70,11 @@ use task::SpawnInTerminal; use theme::{ActiveTheme, SystemAppearance, ThemeSettings}; pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; -use ui::{px, Label}; +use ui::{ + div, Context as _, Div, Element, ElementContext, InteractiveElement as _, IntoElement, Label, + ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _, + WindowContext, +}; use util::ResultExt; use uuid::Uuid; pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -418,6 +420,7 @@ impl AppState { pub fn test(cx: &mut AppContext) -> Arc { use node_runtime::FakeNodeRuntime; use settings::SettingsStore; + use ui::Context as _; if !cx.has_global::() { let settings_store = SettingsStore::test(cx); @@ -4793,10 +4796,6 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { 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 { @@ -4810,7 +4809,7 @@ impl Element for DisconnectedOverlay { .bg(background) .absolute() .left_0() - .top(titlebar_height(cx)) + .top(ui::titlebar_height(cx)) .size_full() .flex() .items_center() @@ -4866,7 +4865,10 @@ mod tests { }, }; use fs::FakeFs; - use gpui::{px, DismissEvent, Empty, TestAppContext, VisualTestContext}; + use gpui::{ + px, DismissEvent, Empty, EventEmitter, FocusHandle, FocusableView, Render, TestAppContext, + VisualTestContext, + }; use project::{Project, ProjectEntryId}; use serde_json::json; use settings::SettingsStore; @@ -5843,6 +5845,8 @@ mod tests { } mod register_project_item_tests { + use ui::Context as _; + use super::*; const TEST_PNG_KIND: &str = "TestPngItemView";