windows: better looking titlebar (#9053)

~~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>
This commit is contained in:
Ezekiel Warren 2024-03-14 17:20:30 -07:00 committed by GitHub
parent 6bbc5e2efa
commit 948b3827c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 694 additions and 122 deletions

View File

@ -343,6 +343,17 @@ features = [
"Win32_Globalization", "Win32_Globalization",
"Win32_Graphics_DirectComposition", "Win32_Graphics_DirectComposition",
"Win32_Graphics_Gdi", "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_Security",
"Win32_Storage_FileSystem", "Win32_Storage_FileSystem",
"Win32_System_Com", "Win32_System_Com",

View File

@ -13,12 +13,12 @@ use rpc::proto;
use std::sync::Arc; use std::sync::Arc;
use theme::ActiveTheme; use theme::ActiveTheme;
use ui::{ use ui::{
h_flex, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator, Button, ButtonLike, h_flex, platform_titlebar, popover_menu, prelude::*, Avatar, AvatarAudioStatusIndicator,
ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconButton, IconName, TintColor, Tooltip,
}; };
use util::ResultExt; use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; 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_PROJECT_NAME_LENGTH: usize = 40;
const MAX_BRANCH_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 project_id = self.project.read(cx).remote_id();
let workspace = self.workspace.upgrade(); let workspace = self.workspace.upgrade();
h_flex() platform_titlebar("collab-titlebar")
.id("titlebar") .titlebar_bg(cx.theme().colors().title_bar_background)
.justify_between() // note: on windows titlebar behaviour is handled by the platform implementation
.w_full() .when(cfg!(not(windows)), |this| {
.h(titlebar_height(cx)) this.on_click(|event, 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 { if event.up.click_count == 2 {
cx.zoom_window(); cx.zoom_window();
} }
}) })
})
.px_2()
.justify_between()
// left side // left side
.child( .child(
h_flex() h_flex()

View File

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

View File

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

View File

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

View File

@ -324,7 +324,7 @@ struct MacWindowState {
renderer: renderer::Renderer, renderer: renderer::Renderer,
kind: WindowKind, kind: WindowKind,
request_frame_callback: Option<Box<dyn FnMut()>>, 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)>>, activate_callback: Option<Box<dyn FnMut(bool)>>,
resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>, resize_callback: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
fullscreen_callback: Option<Box<dyn FnMut(bool)>>, fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
@ -411,6 +411,14 @@ impl MacWindowState {
self.display_link = None; 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 { fn is_fullscreen(&self) -> bool {
unsafe { unsafe {
let style_mask = self.native_window.styleMask(); let style_mask = self.native_window.styleMask();
@ -716,6 +724,11 @@ impl PlatformWindow for MacWindow {
self.0.as_ref().lock().bounds() 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> { fn content_size(&self) -> Size<Pixels> {
self.0.as_ref().lock().content_size() 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); 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); 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()); window_state.lock().previous_keydown_inserted_text = Some(text.clone());
if let Some(callback) = callback.as_mut() { if let Some(callback) = callback.as_mut() {
event.keystroke.ime_key = Some(text.clone()); 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; let is_held = event.is_held;
if let Some(callback) = callback.as_mut() { if let Some(callback) = callback.as_mut() {
handled = callback(PlatformInput::KeyDown(event)); handled = !callback(PlatformInput::KeyDown(event)).propagate;
} }
if !handled && is_held { if !handled && is_held {

View File

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

View File

@ -1,7 +1,6 @@
use std::rc::Rc;
use itertools::Itertools; use itertools::Itertools;
use smallvec::SmallVec; use smallvec::SmallVec;
use std::rc::Rc;
use uuid::Uuid; use uuid::Uuid;
use windows::Win32::{ use windows::Win32::{
Foundation::{BOOL, LPARAM, POINT, RECT}, Foundation::{BOOL, LPARAM, POINT, RECT},

View File

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

View File

@ -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 { pub(crate) trait HiLoWord {
fn hiword(&self) -> u16; fn hiword(&self) -> u16;
@ -42,3 +47,29 @@ impl HiLoWord for LPARAM {
(self.0 & 0xFFFF) as i16 (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
}
}

View File

@ -1,6 +1,4 @@
#![deny(unsafe_op_in_unsafe_fn)] #![deny(unsafe_op_in_unsafe_fn)]
// todo(windows): remove
#![allow(unused_variables)]
use std::{ use std::{
any::Any, any::Any,
@ -14,6 +12,7 @@ use std::{
sync::{Arc, Once}, sync::{Arc, Once},
}; };
use ::util::ResultExt;
use blade_graphics as gpu; use blade_graphics as gpu;
use futures::channel::oneshot::{self, Receiver}; use futures::channel::oneshot::{self, Receiver};
use itertools::Itertools; use itertools::Itertools;
@ -28,6 +27,7 @@ use windows::{
System::{Com::*, Ole::*, SystemServices::*}, System::{Com::*, Ole::*, SystemServices::*},
UI::{ UI::{
Controls::*, Controls::*,
HiDpi::*,
Input::{Ime::*, KeyboardAndMouse::*}, Input::{Ime::*, KeyboardAndMouse::*},
Shell::*, Shell::*,
WindowsAndMessaging::*, WindowsAndMessaging::*,
@ -48,6 +48,7 @@ pub(crate) struct WindowsWindowInner {
callbacks: RefCell<Callbacks>, callbacks: RefCell<Callbacks>,
platform_inner: Rc<WindowsPlatformInner>, platform_inner: Rc<WindowsPlatformInner>,
handle: AnyWindowHandle, handle: AnyWindowHandle,
scale_factor: f32,
} }
impl WindowsWindowInner { impl WindowsWindowInner {
@ -110,9 +111,48 @@ impl WindowsWindowInner {
callbacks, callbacks,
platform_inner, platform_inner,
handle, 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 { fn is_virtual_key_pressed(&self, vkey: VIRTUAL_KEY) -> bool {
unsafe { GetKeyState(vkey.0 as i32) < 0 } unsafe { GetKeyState(vkey.0 as i32) < 0 }
} }
@ -136,12 +176,30 @@ impl WindowsWindowInner {
fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0); log::debug!("msg: {msg}, wparam: {}, lparam: {}", wparam.0, lparam.0);
match msg { 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_MOVE => self.handle_move_msg(lparam),
WM_SIZE => self.handle_size_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_PAINT => self.handle_paint_msg(),
WM_CLOSE => self.handle_close_msg(msg, wparam, lparam), WM_CLOSE => self.handle_close_msg(msg, wparam, lparam),
WM_DESTROY => self.handle_destroy_msg(), WM_DESTROY => self.handle_destroy_msg(),
WM_MOUSEMOVE => self.handle_mouse_move_msg(lparam, wparam), 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_LBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Left, lparam),
WM_RBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Right, lparam), WM_RBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Right, lparam),
WM_MBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Middle, lparam), WM_MBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Middle, lparam),
@ -224,7 +282,7 @@ impl WindowsWindowInner {
fn handle_paint_msg(&self) -> LRESULT { fn handle_paint_msg(&self) -> LRESULT {
let mut paint_struct = PAINTSTRUCT::default(); 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(); let mut callbacks = self.callbacks.borrow_mut();
if let Some(request_frame) = callbacks.request_frame.as_mut() { if let Some(request_frame) = callbacks.request_frame.as_mut() {
request_frame(); request_frame();
@ -291,7 +349,7 @@ impl WindowsWindowInner {
pressed_button, pressed_button,
modifiers: self.current_modifiers(), modifiers: self.current_modifiers(),
}; };
if callback(PlatformInput::MouseMove(event)) { if callback(PlatformInput::MouseMove(event)).default_prevented {
return LRESULT(0); return LRESULT(0);
} }
} }
@ -417,7 +475,7 @@ impl WindowsWindowInner {
keystroke, keystroke,
is_held: lparam.0 & (0x1 << 30) > 0, is_held: lparam.0 & (0x1 << 30) > 0,
}; };
if func(PlatformInput::KeyDown(event)) { if func(PlatformInput::KeyDown(event)).default_prevented {
self.invalidate_client_area(); self.invalidate_client_area();
return LRESULT(0); return LRESULT(0);
} }
@ -434,14 +492,14 @@ impl WindowsWindowInner {
return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) }; return unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) };
}; };
let event = KeyUpEvent { keystroke }; let event = KeyUpEvent { keystroke };
if func(PlatformInput::KeyUp(event)) { if func(PlatformInput::KeyUp(event)).default_prevented {
self.invalidate_client_area(); self.invalidate_client_area();
return LRESULT(0); return LRESULT(0);
} }
unsafe { DefWindowProcW(self.hwnd, message, wparam, lparam) } 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 { let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
return LRESULT(1); return LRESULT(1);
}; };
@ -452,14 +510,14 @@ impl WindowsWindowInner {
keystroke, keystroke,
is_held: lparam.0 & (0x1 << 30) > 0, is_held: lparam.0 & (0x1 << 30) > 0,
}; };
if func(PlatformInput::KeyDown(event)) { if func(PlatformInput::KeyDown(event)).default_prevented {
self.invalidate_client_area(); self.invalidate_client_area();
return LRESULT(0); return LRESULT(0);
} }
LRESULT(1) 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 { let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
return LRESULT(1); return LRESULT(1);
}; };
@ -467,14 +525,14 @@ impl WindowsWindowInner {
return LRESULT(1); return LRESULT(1);
}; };
let event = KeyUpEvent { keystroke }; let event = KeyUpEvent { keystroke };
if func(PlatformInput::KeyUp(event)) { if func(PlatformInput::KeyUp(event)).default_prevented {
self.invalidate_client_area(); self.invalidate_client_area();
return LRESULT(0); return LRESULT(0);
} }
LRESULT(1) 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 { let Some(keystroke) = self.parse_char_msg_keystroke(wparam) else {
return LRESULT(1); return LRESULT(1);
}; };
@ -487,7 +545,7 @@ impl WindowsWindowInner {
keystroke, keystroke,
is_held: lparam.0 & (0x1 << 30) > 0, is_held: lparam.0 & (0x1 << 30) > 0,
}; };
if func(PlatformInput::KeyDown(event)) { if func(PlatformInput::KeyDown(event)).default_prevented {
self.invalidate_client_area(); self.invalidate_client_area();
return LRESULT(0); return LRESULT(0);
} }
@ -515,7 +573,7 @@ impl WindowsWindowInner {
modifiers: self.current_modifiers(), modifiers: self.current_modifiers(),
click_count: 1, click_count: 1,
}; };
if callback(PlatformInput::MouseDown(event)) { if callback(PlatformInput::MouseDown(event)).default_prevented {
return LRESULT(0); return LRESULT(0);
} }
} }
@ -533,7 +591,7 @@ impl WindowsWindowInner {
modifiers: self.current_modifiers(), modifiers: self.current_modifiers(),
click_count: 1, click_count: 1,
}; };
if callback(PlatformInput::MouseUp(event)) { if callback(PlatformInput::MouseUp(event)).default_prevented {
return LRESULT(0); return LRESULT(0);
} }
} }
@ -578,7 +636,7 @@ impl WindowsWindowInner {
modifiers: self.current_modifiers(), modifiers: self.current_modifiers(),
touch_phase: TouchPhase::Moved, touch_phase: TouchPhase::Moved,
}; };
if callback(PlatformInput::ScrollWheel(event)) { if callback(PlatformInput::ScrollWheel(event)).default_prevented {
return LRESULT(0); return LRESULT(0);
} }
} }
@ -685,12 +743,237 @@ impl WindowsWindowInner {
}; };
func(input); 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)] #[derive(Default)]
struct Callbacks { struct Callbacks {
request_frame: Option<Box<dyn FnMut()>>, 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)>>, active_status_change: Option<Box<dyn FnMut(bool)>>,
resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>, resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
fullscreen: Option<Box<dyn FnMut(bool)>>, fullscreen: Option<Box<dyn FnMut(bool)>>,
@ -718,7 +1001,6 @@ impl WindowsWindow {
handle: AnyWindowHandle, handle: AnyWindowHandle,
options: WindowParams, options: WindowParams,
) -> Self { ) -> Self {
let dwexstyle = WINDOW_EX_STYLE::default();
let classname = register_wnd_class(); let classname = register_wnd_class();
let windowname = HSTRING::from( let windowname = HSTRING::from(
options options
@ -728,7 +1010,7 @@ impl WindowsWindow {
.map(|title| title.as_ref()) .map(|title| title.as_ref())
.unwrap_or(""), .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 x = options.bounds.origin.x.0 as i32;
let y = options.bounds.origin.y.0 as i32; let y = options.bounds.origin.y.0 as i32;
let nwidth = options.bounds.size.width.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 _); let lpparam = Some(&context as *const _ as *const _);
unsafe { unsafe {
CreateWindowExW( CreateWindowExW(
dwexstyle, WS_EX_APPWINDOW,
classname, classname,
&windowname, &windowname,
dwstyle, dwstyle,
@ -785,7 +1067,7 @@ impl WindowsWindow {
} }
fn maximize(&self) { 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) // todo(windows)
fn content_size(&self) -> Size<Pixels> { fn content_size(&self) -> Size<Pixels> {
let size = self.inner.size.get(); let size = self.inner.size.get();
@ -837,12 +1123,12 @@ impl PlatformWindow for WindowsWindow {
// todo(windows) // todo(windows)
fn scale_factor(&self) -> f32 { fn scale_factor(&self) -> f32 {
1.0 self.inner.scale_factor
} }
// todo(windows)
fn titlebar_height(&self) -> Pixels { 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) // todo(windows)
@ -952,8 +1238,9 @@ impl PlatformWindow for WindowsWindow {
Some(done_rx) Some(done_rx)
} }
// todo(windows) fn activate(&self) {
fn activate(&self) {} unsafe { ShowWindowAsync(self.inner.hwnd, SW_NORMAL) };
}
// todo(windows) // todo(windows)
fn set_title(&mut self, title: &str) { fn set_title(&mut self, title: &str) {
@ -963,16 +1250,18 @@ impl PlatformWindow for WindowsWindow {
} }
// todo(windows) // todo(windows)
fn set_edited(&mut self, edited: bool) {} fn set_edited(&mut self, _edited: bool) {}
// todo(windows) // todo(windows)
fn show_character_palette(&self) {} 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) // todo(windows)
fn toggle_fullscreen(&self) {} fn toggle_fullscreen(&self) {}
@ -988,7 +1277,7 @@ impl PlatformWindow for WindowsWindow {
} }
// todo(windows) // 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); self.inner.callbacks.borrow_mut().input = Some(callback);
} }
@ -1028,7 +1317,7 @@ impl PlatformWindow for WindowsWindow {
} }
// todo(windows) // todo(windows)
fn is_topmost_for_position(&self, position: Point<Pixels>) -> bool { fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
true true
} }
@ -1046,6 +1335,7 @@ impl PlatformWindow for WindowsWindow {
#[implement(IDropTarget)] #[implement(IDropTarget)]
struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>); struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
#[allow(non_snake_case)]
impl IDropTarget_Impl for WindowsDragDropHandler { impl IDropTarget_Impl for WindowsDragDropHandler {
fn DragEnter( fn DragEnter(
&self, &self,
@ -1159,6 +1449,7 @@ fn register_wnd_class() -> PCWSTR {
lpfnWndProc: Some(wnd_proc), lpfnWndProc: Some(wnd_proc),
hCursor: unsafe { LoadCursorW(None, IDC_ARROW).ok().unwrap() }, hCursor: unsafe { LoadCursorW(None, IDC_ARROW).ok().unwrap() },
lpszClassName: PCWSTR(CLASS_NAME.as_ptr()), lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
style: CS_HREDRAW | CS_VREDRAW,
..Default::default() ..Default::default()
}; };
unsafe { RegisterClassW(&wc) }; 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> { fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
match code { match code {
// VK_0 - VK_9 // VK_0 - VK_9

View File

@ -477,7 +477,7 @@ impl Window {
handle handle
.update(&mut cx, |_, cx| cx.dispatch_event(event)) .update(&mut cx, |_, cx| cx.dispatch_event(event))
.log_err() .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 /// 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 /// 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. /// to leave room to support more complex shapes in the future.
@ -644,6 +650,17 @@ impl<'a> WindowContext<'a> {
style 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. /// Dispatch the given action on the currently focused element.
pub fn dispatch_action(&mut self, action: Box<dyn Action>) { pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
let focus_handle = self.focused(); let focus_handle = self.focused();
@ -1044,10 +1061,11 @@ impl<'a> WindowContext<'a> {
/// You can create a keystroke with Keystroke::parse(""). /// You can create a keystroke with Keystroke::parse("").
pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool { pub fn dispatch_keystroke(&mut self, keystroke: Keystroke) -> bool {
let keystroke = keystroke.with_simulated_ime(); let keystroke = keystroke.with_simulated_ime();
if self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent { let result = self.dispatch_event(PlatformInput::KeyDown(KeyDownEvent {
keystroke: keystroke.clone(), keystroke: keystroke.clone(),
is_held: false, is_held: false,
})) { }));
if !result.propagate {
return true; return true;
} }
@ -1080,7 +1098,7 @@ impl<'a> WindowContext<'a> {
/// Dispatch a mouse or keyboard event on the window. /// Dispatch a mouse or keyboard event on the window.
#[profiling::function] #[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()); self.window.last_input_timestamp.set(Instant::now());
// Handlers may set this to false by calling `stop_propagation`. // Handlers may set this to false by calling `stop_propagation`.
self.app.propagate_event = true; self.app.propagate_event = true;
@ -1168,7 +1186,10 @@ impl<'a> WindowContext<'a> {
self.dispatch_key_event(any_key_event); 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) { fn dispatch_mouse_event(&mut self, event: &dyn Any) {

View File

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

View File

@ -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)
}
}

View File

@ -26,7 +26,7 @@ use futures::{
Future, FutureExt, StreamExt, Future, FutureExt, StreamExt,
}; };
use gpui::{ 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, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle, DragMoveEvent, Element, ElementContext, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, Global, GlobalPixels, InteractiveElement, IntoElement, KeyContext, Keystroke, 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())) 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; struct DisconnectedOverlay;
impl Element for DisconnectedOverlay { impl Element for DisconnectedOverlay {
@ -4759,7 +4755,7 @@ impl Element for DisconnectedOverlay {
.bg(background) .bg(background)
.absolute() .absolute()
.left_0() .left_0()
.top(titlebar_height(cx)) .top(cx.titlebar_height())
.size_full() .size_full()
.flex() .flex()
.items_center() .items_center()