//! Ely's window on Windows: a bare popup owned by the main window, so it stays above it and hides with it, sized and moved as it moves. use std::sync::atomic::{AtomicIsize, Ordering}; use anyhow::bail; use gpui::{Bounds, Pixels, Window}; use raw_window_handle::{HasWindowHandle, RawWindowHandle}; use windows_sys::Win32::{ Foundation::{HWND, LPARAM, LRESULT, RECT, WPARAM}, UI::{ HiDpi::GetDpiForWindow, WindowsAndMessaging::{ CallWindowProcW, GWL_STYLE, GWLP_HWNDPARENT, GWLP_WNDPROC, GetWindowInfo, GetWindowLongPtrW, GetWindowRect, HWND_NOTOPMOST, MA_NOACTIVATE, PostMessageW, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SetWindowLongPtrW, SetWindowPos, USER_DEFAULT_SCREEN_DPI, WINDOWINFO, WM_APP, WM_DPICHANGED, WM_MOUSEACTIVATE, WNDPROC, WS_CAPTION, WS_POPUP, }, }, }; use super::{HEAD, ROOM, WIDTH, left, pane}; /// gpui's window procedure, which hers passes every message on to but a press's activation and her place. static GPUI: AtomicIsize = AtomicIsize::new(0); /// Her place, posted to her window: Windows sends a new size at once, which gpui hears only outside its own updates. const PLACE: u32 = WM_APP; const _: () = assert!( size_of::() == 8, "a place packs two i32 into a word" ); /// Her window and the main one. pub struct Glue { pet: HWND, main: HWND, } impl Glue { pub fn new(pet: &Window, main: &Window) -> anyhow::Result { Self::hold(native(pet)?, native(main)?) } /// Gives her window the main window as owner, a bare popup's frame, none of the topmost place gpui gives a popup, and keeps a press from activating it, so the keys stay with the terminal. fn hold(pet: HWND, main: HWND) -> anyhow::Result { unsafe { SetWindowLongPtrW(pet, GWLP_HWNDPARENT, main as isize); // gpui's popup is overlapped, which Windows gives a caption and a frame. let style = (GetWindowLongPtrW(pet, GWL_STYLE) as u32 & !WS_CAPTION) | WS_POPUP; SetWindowLongPtrW(pet, GWL_STYLE, style as isize); let gpui = SetWindowLongPtrW(pet, GWLP_WNDPROC, inactive as *const () as isize); anyhow::ensure!(gpui != 0, "pet: her window's procedure does not read"); GPUI.store(gpui, Ordering::Relaxed); let flags = SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED; anyhow::ensure!( SetWindowPos(pet, HWND_NOTOPMOST, 0, 0, 0, 0, flags) != 0, "pet: her window keeps the topmost place or its frame" ); } Ok(Self { pet, main }) } /// Stands her on the main window's top edge, as far in as its width allows. pub fn place(&self, main: &Window) -> anyhow::Result<()> { self.put(main.scale_factor(), pane(main)) } /// Posts her box, sized at her own DPI as gpui did not when it opened her off the display, standing at `scale` over `pane`, in the main window's points. fn put(&self, scale: f32, pane: Bounds) -> anyhow::Result<()> { let mut main = WINDOWINFO { cbSize: size_of::() as u32, ..Default::default() }; let (dpi, read) = unsafe { ( GetDpiForWindow(self.pet), GetWindowInfo(self.main, &mut main), ) }; anyhow::ensure!(dpi != 0, "pet: her window's DPI does not read"); anyhow::ensure!(read != 0, "pet: the main window's place does not read"); let own = dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32; let (at, size) = stand(main.rcClient, scale, own, pane); let posted = unsafe { PostMessageW(self.pet, PLACE, pack(at), pack(size) as LPARAM) }; anyhow::ensure!(posted != 0, "pet: her place does not post"); log::debug!("pet: Ely is to stand at {at:?}, {size:?} pixels"); Ok(()) } } /// Her box at `own` scale, its bottom on `pane` at `scale` over the main window's `client` rect: its place and size in pixels. fn stand(client: RECT, scale: f32, own: f32, pane: Bounds) -> ((i32, i32), (i32, i32)) { let size = ( (WIDTH * own).round() as i32, ((HEAD + ROOM) * own).round() as i32, ); let (x, y) = (f32::from(pane.origin.x), f32::from(pane.origin.y)); let at = ( client.left + ((x + left(pane.size.width)) * scale).round() as i32, client.top + (y * scale).round() as i32 - size.1, ); (at, size) } /// Her window's procedure: a press does not activate her window, she takes her place, and at a new DPI keeps her corner; all else is gpui's. unsafe extern "system" fn inactive( window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { let gpui: WNDPROC = unsafe { std::mem::transmute(GPUI.load(Ordering::Relaxed)) }; match message { WM_MOUSEACTIVATE => MA_NOACTIVATE as LRESULT, PLACE => { let ((x, y), (width, height)) = (unpack(wparam), unpack(lparam as WPARAM)); let flags = SWP_NOZORDER | SWP_NOACTIVATE; let null = std::ptr::null_mut(); if unsafe { SetWindowPos(window, null, x, y, width, height, flags) } == 0 { log::error!("pet: her window does not move to {x},{y}"); } 0 } WM_DPICHANGED => { // gpui takes the rect Windows suggests, as a move to another display brings. let (mut before, mut after) = (RECT::default(), RECT::default()); let read = unsafe { GetWindowRect(window, &mut before) }; let answer = unsafe { CallWindowProcW(gpui, window, message, wparam, lparam) }; if read == 0 || unsafe { GetWindowRect(window, &mut after) } == 0 { log::error!("pet: her window's place does not read at a new DPI"); return answer; } let top = before.bottom - (after.bottom - after.top); let flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE; let null = std::ptr::null_mut(); if unsafe { SetWindowPos(window, null, before.left, top, 0, 0, flags) } == 0 { log::error!("pet: her window does not keep its corner at a new DPI"); } else { log::debug!( "pet: at a new DPI Ely keeps her corner at {},{}", before.left, before.bottom ); } answer } _ => unsafe { CallWindowProcW(gpui, window, message, wparam, lparam) }, } } fn pack((low, high): (i32, i32)) -> WPARAM { low as u32 as WPARAM | (high as u32 as WPARAM) << 32 } fn unpack(word: WPARAM) -> (i32, i32) { (word as u32 as i32, (word >> 32) as u32 as i32) } /// The HWND gpui made for `window`. fn native(window: &Window) -> anyhow::Result { let handle = HasWindowHandle::window_handle(window) .map_err(|error| anyhow::anyhow!("pet: no native handle: {error:?}"))?; let RawWindowHandle::Win32(win32) = handle.as_raw() else { bail!("pet: not a Win32 window"); }; Ok(win32.hwnd.get() as HWND) } #[cfg(test)] mod tests { use gpui::{point, px, size}; use windows_sys::{ Win32::UI::WindowsAndMessaging::{ CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, MSG, PM_REMOVE, PeekMessageW, SendMessageW, WS_EX_TOOLWINDOW, WS_EX_TOPMOST, WS_OVERLAPPEDWINDOW, }, w, }; use super::*; /// A hidden window of `style`, at the size Windows picks, as gpui asks for its windows. fn window(style: u32, extended: u32) -> HWND { let window = unsafe { CreateWindowExW( extended, w!("STATIC"), w!(""), style, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null_mut(), std::ptr::null(), ) }; assert!(!window.is_null(), "a test window"); window } fn info(window: HWND) -> WINDOWINFO { let mut info = WINDOWINFO { cbSize: size_of::() as u32, ..Default::default() }; assert_ne!(unsafe { GetWindowInfo(window, &mut info) }, 0, "its info"); info } /// gpui's procedure as far as these tests need it: at a new DPI it takes the rect Windows suggests. unsafe extern "system" fn standin( window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM, ) -> LRESULT { if message == WM_DPICHANGED { let rect = unsafe { *(lparam as *const RECT) }; let (width, height) = (rect.right - rect.left, rect.bottom - rect.top); let flags = SWP_NOZORDER | SWP_NOACTIVATE; let null = std::ptr::null_mut(); unsafe { SetWindowPos(window, null, rect.left, rect.top, width, height, flags) }; return 0; } unsafe { DefWindowProcW(window, message, wparam, lparam) } } #[test] fn her_bottom_stands_on_the_panes_top_edge_whatever_the_two_windows_dpi() { let client = RECT { left: 100, top: 20, right: 1300, bottom: 900, }; let pane = Bounds::new(point(px(10.), px(8.)), size(px(800.), px(600.))); for (scale, own) in [(1.0, 1.0), (1.0, 1.5), (1.5, 1.0), (2.0, 1.25)] { let ((x, y), (width, height)) = stand(client, scale, own, pane); let own_box = ( (WIDTH * own).round() as i32, ((HEAD + ROOM) * own).round() as i32, ); let corner = ( 100 + ((10.0 + left(px(800.))) * scale).round() as i32, 20 + (8.0 * scale).round() as i32, ); assert_eq!( ((width, height), (x, y + height)), (own_box, corner), "the main window at {scale}, hers at {own}" ); } } #[test] fn the_large_framed_popup_gpui_makes_stands_bare_in_her_box_on_the_panes_top_edge() { let main = window(WS_OVERLAPPEDWINDOW, 0); // gpui's popup: style 0, an overlapped window, which Windows gives a caption. let pet = window(0, WS_EX_TOOLWINDOW | WS_EX_TOPMOST); assert_eq!( info(pet).dwStyle & WS_CAPTION, WS_CAPTION, "framed as gpui makes it" ); unsafe { SetWindowLongPtrW(pet, GWLP_WNDPROC, standin as *const () as isize) }; let glue = Glue::hold(pet, main).expect("held"); let pane = Bounds::new(point(px(0.), px(0.)), size(px(380.), px(280.))); glue.put(1.0, pane).expect("placed"); let made = info(pet).rcWindow; let mut message = MSG::default(); while unsafe { PeekMessageW(&mut message, pet, 0, 0, PM_REMOVE) } != 0 { unsafe { DispatchMessageW(&message) }; } let (her, under) = (info(pet), info(main)); let own = unsafe { GetDpiForWindow(pet) } as f32 / 96.0; // A new DPI, whose rect Windows suggests 30 and 40 pixels off, 100 by 60. let at = her.rcWindow; let suggested = RECT { left: at.left + 30, top: at.top + 40, right: at.left + 130, bottom: at.top + 100, }; let pointer = &suggested as *const RECT as LPARAM; unsafe { SendMessageW(pet, WM_DPICHANGED, 144 | 144 << 16, pointer) }; let later = info(pet).rcWindow; unsafe { DestroyWindow(pet); DestroyWindow(main); } let (outer, inner) = (her.rcWindow, her.rcClient); let frame = ( inner.left - outer.left, inner.top - outer.top, outer.right - inner.right, outer.bottom - inner.bottom, ); let drawn = (outer.right - outer.left, outer.bottom - outer.top); let own_box = ( (WIDTH * own).round() as i32, ((HEAD + ROOM) * own).round() as i32, ); let edge = ( under.rcClient.left + left(px(380.)).round() as i32, under.rcClient.top, ); let kept = ( later.left, later.bottom, later.right - later.left, later.bottom - later.top, ); assert_eq!( ( her.dwStyle & WS_CAPTION, frame, drawn, (outer.left, outer.bottom), kept ), ( 0, (0, 0, 0, 0), own_box, edge, (outer.left, outer.bottom, 100, 60) ), "bare, in her own box, her bottom on the pane's top edge, kept there at a new DPI" ); assert_ne!( (made.right - made.left, made.bottom - made.top), own_box, "no size until her window takes the posted place" ); } }