diff --git a/src/gl/macos.rs b/src/gl/macos.rs index 0610879..215dd94 100644 --- a/src/gl/macos.rs +++ b/src/gl/macos.rs @@ -11,6 +11,7 @@ use cocoa::appkit::{ NSOpenGLProfileVersionLegacy, NSOpenGLView, NSView, }; use cocoa::base::{id, nil, YES}; +use cocoa::foundation::NSSize; use core_foundation::base::TCFType; use core_foundation::bundle::{CFBundleGetBundleWithIdentifier, CFBundleGetFunctionPointerForName}; @@ -134,6 +135,14 @@ impl GlContext { let () = msg_send![self.view, setNeedsDisplay: YES]; } } + + /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. + pub(crate) fn resize(&self, size: NSSize) { + unsafe { NSView::setFrameSize(self.view, size) }; + unsafe { + let _: () = msg_send![self.view, setNeedsDisplay: YES]; + } + } } impl Drop for GlContext { diff --git a/src/gl/mod.rs b/src/gl/mod.rs index a7922b7..adfc12d 100644 --- a/src/gl/mod.rs +++ b/src/gl/mod.rs @@ -106,4 +106,10 @@ impl GlContext { pub fn swap_buffers(&self) { self.context.swap_buffers(); } + + /// On macOS the `NSOpenGLView` needs to be resized separtely from our main view. + #[cfg(target_os = "macos")] + pub(crate) fn resize(&self, size: cocoa::foundation::NSSize) { + self.context.resize(size); + } } diff --git a/src/macos/view.rs b/src/macos/view.rs index abcef68..52c9311 100644 --- a/src/macos/view.rs +++ b/src/macos/view.rs @@ -234,18 +234,23 @@ extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel, _: id) { let ns_window: *mut Object = msg_send![this, window]; let scale_factor: f64 = - if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) as f64 }; + if ns_window.is_null() { 1.0 } else { NSWindow::backingScaleFactor(ns_window) }; let state: &mut WindowState = WindowState::from_field(this); let bounds: NSRect = msg_send![this, bounds]; - let window_info = WindowInfo::from_logical_size( + let new_window_info = WindowInfo::from_logical_size( Size::new(bounds.size.width, bounds.size.height), scale_factor, ); - state.trigger_event(Event::Window(WindowEvent::Resized(window_info))); + // Only send the event when the window's size has actually changed to be in line with the + // other platform implementations + if new_window_info.physical_size() != state.window_info.physical_size() { + state.window_info = new_window_info; + state.trigger_event(Event::Window(WindowEvent::Resized(new_window_info))); + } } } @@ -364,6 +369,6 @@ extern "C" fn scroll_wheel(this: &Object, _: Sel, event: id) { state.trigger_event(Event::Mouse(MouseEvent::WheelScrolled { delta, - modifiers: make_modifiers(modifiers) + modifiers: make_modifiers(modifiers), })); } diff --git a/src/macos/window.rs b/src/macos/window.rs index 0875377..fd67de7 100644 --- a/src/macos/window.rs +++ b/src/macos/window.rs @@ -5,10 +5,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use cocoa::appkit::{ - NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, NSWindow, - NSWindowStyleMask, + NSApp, NSApplication, NSApplicationActivationPolicyRegular, NSBackingStoreBuffered, NSView, + NSWindow, NSWindowStyleMask, }; -use cocoa::base::{id, nil, NO}; +use cocoa::base::{id, nil, NO, YES}; use cocoa::foundation::{NSAutoreleasePool, NSPoint, NSRect, NSSize, NSString}; use core_foundation::runloop::{ CFRunLoop, CFRunLoopTimer, CFRunLoopTimerContext, __CFRunLoopTimer, kCFRunLoopDefaultMode, @@ -20,7 +20,7 @@ use objc::{msg_send, runtime::Object, sel, sel_impl}; use raw_window_handle::{AppKitHandle, HasRawWindowHandle, RawWindowHandle}; use crate::{ - Event, EventStatus, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, + Event, EventStatus, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; @@ -124,6 +124,13 @@ impl Window { { let pool = unsafe { NSAutoreleasePool::new(nil) }; + let scaling = match options.scale { + WindowScalePolicy::ScaleFactor(scale) => scale, + WindowScalePolicy::SystemScaleFactor => 1.0, + }; + + let window_info = WindowInfo::from_logical_size(options.size, scaling); + let handle = if let RawWindowHandle::AppKit(handle) = parent.raw_window_handle() { handle } else { @@ -144,7 +151,7 @@ impl Window { .map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)), }; - let window_handle = Self::init(true, window, build); + let window_handle = Self::init(true, window, window_info, build); unsafe { let _: id = msg_send![handle.ns_view as *mut Object, addSubview: ns_view]; @@ -164,6 +171,13 @@ impl Window { { let pool = unsafe { NSAutoreleasePool::new(nil) }; + let scaling = match options.scale { + WindowScalePolicy::ScaleFactor(scale) => scale, + WindowScalePolicy::SystemScaleFactor => 1.0, + }; + + let window_info = WindowInfo::from_logical_size(options.size, scaling); + let ns_view = unsafe { create_view(&options) }; let window = Window { @@ -178,7 +192,7 @@ impl Window { .map(|gl_config| Self::create_gl_context(None, ns_view, gl_config)), }; - let window_handle = Self::init(true, window, build); + let window_handle = Self::init(true, window, window_info, build); unsafe { let () = msg_send![pool, drain]; @@ -215,10 +229,7 @@ impl Window { let rect = NSRect::new( NSPoint::new(0.0, 0.0), - NSSize::new( - window_info.logical_size().width as f64, - window_info.logical_size().height as f64, - ), + NSSize::new(window_info.logical_size().width, window_info.logical_size().height), ); let ns_window = unsafe { @@ -254,7 +265,7 @@ impl Window { .map(|gl_config| Self::create_gl_context(Some(ns_window), ns_view, gl_config)), }; - let _ = Self::init(false, window, build); + let _ = Self::init(false, window, window_info, build); unsafe { ns_window.setContentView_(ns_view); @@ -266,7 +277,9 @@ impl Window { } } - fn init(parented: bool, mut window: Window, build: B) -> WindowHandle + fn init( + parented: bool, mut window: Window, window_info: WindowInfo, build: B, + ) -> WindowHandle where H: WindowHandler + 'static, B: FnOnce(&mut crate::Window) -> H, @@ -285,6 +298,7 @@ impl Window { keyboard_state: KeyboardState::new(), frame_timer: None, retain_count_after_build, + window_info, _parent_handle: parent_handle, })); @@ -302,6 +316,28 @@ impl Window { self.close_requested = true; } + pub fn resize(&mut self, size: Size) { + // NOTE: macOS gives you a personal rave if you pass in fractional pixels here. Even though + // the size is in fractional pixels. + let size = NSSize::new(size.width.round(), size.height.round()); + + unsafe { NSView::setFrameSize(self.ns_view, size) }; + unsafe { + let _: () = msg_send![self.ns_view, setNeedsDisplay: YES]; + } + + // When using OpenGL the `NSOpenGLView` needs to be resized separately? Why? Because macOS. + #[cfg(feature = "opengl")] + if let Some(gl_context) = &self.gl_context { + gl_context.resize(size); + } + + // If this is a standalone window then we'll also need to resize the window itself + if let Some(ns_window) = self.ns_window { + unsafe { NSWindow::setContentSize_(ns_window, size) }; + } + } + #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&GlContext> { self.gl_context.as_ref() @@ -325,6 +361,8 @@ pub(super) struct WindowState { frame_timer: Option, _parent_handle: Option, pub retain_count_after_build: usize, + /// The last known window info for this window. + pub window_info: WindowInfo, } impl WindowState { diff --git a/src/win/window.rs b/src/win/window.rs index c7fa822..83ff6d2 100644 --- a/src/win/window.rs +++ b/src/win/window.rs @@ -16,21 +16,21 @@ use winapi::um::winuser::{ XBUTTON1, XBUTTON2, }; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; +use std::collections::VecDeque; use std::ffi::{c_void, OsStr}; use std::marker::PhantomData; use std::os::windows::ffi::OsStrExt; use std::ptr::null_mut; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::Arc; +use std::rc::Rc; use raw_window_handle::{HasRawWindowHandle, RawWindowHandle, Win32Handle}; const BV_WINDOW_MUST_CLOSE: UINT = WM_USER + 1; use crate::{ - Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, WindowHandler, - WindowInfo, WindowOpenOptions, WindowScalePolicy, + Event, MouseButton, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, + WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; use super::keyboard::KeyboardState; @@ -61,7 +61,7 @@ const WIN_FRAME_TIMER: usize = 4242; pub struct WindowHandle { hwnd: Option, - is_open: Arc, + is_open: Rc>, // Ensure handle is !Send _phantom: PhantomData<*mut ()>, @@ -77,7 +77,7 @@ impl WindowHandle { } pub fn is_open(&self) -> bool { - self.is_open.load(Ordering::Relaxed) + self.is_open.get() } } @@ -95,16 +95,16 @@ unsafe impl HasRawWindowHandle for WindowHandle { } struct ParentHandle { - is_open: Arc, + is_open: Rc>, } impl ParentHandle { pub fn new(hwnd: HWND) -> (Self, WindowHandle) { - let is_open = Arc::new(AtomicBool::new(true)); + let is_open = Rc::new(Cell::new(true)); let handle = WindowHandle { hwnd: Some(hwnd), - is_open: Arc::clone(&is_open), + is_open: Rc::clone(&is_open), _phantom: PhantomData::default(), }; @@ -114,7 +114,7 @@ impl ParentHandle { impl Drop for ParentHandle { fn drop(&mut self) { - self.is_open.store(false, Ordering::Relaxed); + self.is_open.set(false); } } @@ -126,232 +126,295 @@ unsafe extern "system" fn wnd_proc( return 0; } - let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut RefCell; + let window_state_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut WindowState; if !window_state_ptr.is_null() { - match msg { - WM_MOUSEMOVE => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); + let result = wnd_proc_inner(hwnd, msg, wparam, lparam, &*window_state_ptr); - let x = (lparam & 0xFFFF) as i16 as i32; - let y = ((lparam >> 16) & 0xFFFF) as i16 as i32; + // If any of the above event handlers caused tasks to be pushed to the deferred tasks list, + // then we'll try to handle them now + loop { + // NOTE: This is written like this instead of using a `while let` loop to avoid exending + // the borrow of `window_state.deferred_tasks` into the call of + // `window_state.handle_deferred_task()` since that may also generate additional + // messages. + let task = match (*window_state_ptr).deferred_tasks.borrow_mut().pop_front() { + Some(task) => task, + None => break, + }; - let physical_pos = PhyPoint { x, y }; - let logical_pos = physical_pos.to_logical(&window_state.window_info); - let event = Event::Mouse(MouseEvent::CursorMoved { - position: logical_pos, - modifiers: window_state.keyboard_state.get_modifiers_from_mouse_wparam(wparam), - }); + (*window_state_ptr).handle_deferred_task(task); + } - window_state.handler.on_event(&mut window, event); + // NOTE: This is not handled in `wnd_proc_inner` because of the deferred task loop above + if msg == WM_NCDESTROY { + unregister_wnd_class((*window_state_ptr).window_class); + SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); + drop(Box::from_raw(window_state_ptr)); + } - return 0; - } - WM_MOUSEWHEEL | WM_MOUSEHWHEEL => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let value = (wparam >> 16) as i16; - let value = value as i32; - let value = value as f32 / WHEEL_DELTA as f32; - - let event = Event::Mouse(MouseEvent::WheelScrolled { - delta: if msg == WM_MOUSEWHEEL { - ScrollDelta::Lines { x: 0.0, y: value } - } else { - ScrollDelta::Lines { x: value, y: 0.0 } - }, - modifiers: window_state.keyboard_state.get_modifiers_from_mouse_wparam(wparam), - }); - - window_state.handler.on_event(&mut window, event); - - return 0; - } - WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN - | WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let mut mouse_button_counter = window_state.mouse_button_counter; - - let button = match msg { - WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left), - WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle), - WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right), - WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) { - XBUTTON1 => Some(MouseButton::Back), - XBUTTON2 => Some(MouseButton::Forward), - _ => None, - }, - _ => None, - }; - - if let Some(button) = button { - let event = match msg { - WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => { - // Capture the mouse cursor on button down - mouse_button_counter = mouse_button_counter.saturating_add(1); - SetCapture(hwnd); - MouseEvent::ButtonPressed { - button, - modifiers: window_state - .keyboard_state - .get_modifiers_from_mouse_wparam(wparam), - } - } - WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => { - // Release the mouse cursor capture when all buttons are released - mouse_button_counter = mouse_button_counter.saturating_sub(1); - if mouse_button_counter == 0 { - ReleaseCapture(); - } - - MouseEvent::ButtonReleased { - button, - modifiers: window_state - .keyboard_state - .get_modifiers_from_mouse_wparam(wparam), - } - } - _ => { - unreachable!() - } - }; - - window_state.mouse_button_counter = mouse_button_counter; - - window_state.handler.on_event(&mut window, Event::Mouse(event)); - } - } - WM_TIMER => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - if wparam == WIN_FRAME_TIMER { - window_state.handler.on_frame(&mut window); - } - return 0; - } - WM_CLOSE => { - // Make sure to release the borrow before the DefWindowProc call - { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - window_state - .handler - .on_event(&mut window, Event::Window(WindowEvent::WillClose)); - } - - // DestroyWindow(hwnd); - // return 0; - return DefWindowProcW(hwnd, msg, wparam, lparam); - } - WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP - | WM_INPUTLANGCHANGE => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let opt_event = - window_state.keyboard_state.process_message(hwnd, msg, wparam, lparam); - - if let Some(event) = opt_event { - window_state.handler.on_event(&mut window, Event::Keyboard(event)); - } - - if msg != WM_SYSKEYDOWN { - return 0; - } - } - WM_SIZE => { - let mut window_state = (*window_state_ptr).borrow_mut(); - let mut window = window_state.create_window(hwnd); - let mut window = crate::Window::new(&mut window); - - let width = (lparam & 0xFFFF) as u16 as u32; - let height = ((lparam >> 16) & 0xFFFF) as u16 as u32; - - window_state.window_info = WindowInfo::from_physical_size( - PhySize { width, height }, - window_state.window_info.scale(), - ); - - let window_info = window_state.window_info; - - window_state - .handler - .on_event(&mut window, Event::Window(WindowEvent::Resized(window_info))); - } - WM_DPICHANGED => { - let mut window_state = (*window_state_ptr).borrow_mut(); - - // To avoid weirdness with the realtime borrow checker. - let new_rect = { - if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy { - let dpi = (wparam & 0xFFFF) as u16 as u32; - let scale_factor = dpi as f64 / 96.0; - - window_state.window_info = WindowInfo::from_logical_size( - window_state.window_info.logical_size(), - scale_factor, - ); - - Some(( - RECT { - left: 0, - top: 0, - // todo: check if usize fits into i32 - right: window_state.window_info.physical_size().width as i32, - bottom: window_state.window_info.physical_size().height as i32, - }, - window_state.dw_style, - )) - } else { - None - } - }; - if let Some((mut new_rect, dw_style)) = new_rect { - // Convert this desired "client rectangle" size to the actual "window rectangle" - // size (Because of course you have to do that). - AdjustWindowRectEx(&mut new_rect, dw_style, 0, 0); - - // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, - // which we can then send the user the new scale factor. - SetWindowPos( - hwnd, - hwnd, - new_rect.left as i32, - new_rect.top as i32, - new_rect.right - new_rect.left, - new_rect.bottom - new_rect.top, - SWP_NOZORDER | SWP_NOMOVE, - ); - } - } - WM_NCDESTROY => { - let window_state = Box::from_raw(window_state_ptr); - unregister_wnd_class(window_state.borrow().window_class); - SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0); - } - _ => { - if msg == BV_WINDOW_MUST_CLOSE { - DestroyWindow(hwnd); - return 0; - } - } + // The actual custom window proc has been moved to another function so we can always handle + // the deferred tasks regardless of whether the custom window proc returns early or not + if let Some(result) = result { + return result; } } DefWindowProcW(hwnd, msg, wparam, lparam) } +/// Our custom `wnd_proc` handler. If the result contains a value, then this is returned after +/// handling any deferred tasks. otherwise the default window procedure is invoked. +unsafe fn wnd_proc_inner( + hwnd: HWND, msg: UINT, wparam: WPARAM, lparam: LPARAM, window_state: &WindowState, +) -> Option { + match msg { + WM_MOUSEMOVE => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let x = (lparam & 0xFFFF) as i16 as i32; + let y = ((lparam >> 16) & 0xFFFF) as i16 as i32; + + let physical_pos = PhyPoint { x, y }; + let logical_pos = physical_pos.to_logical(&window_state.window_info.borrow()); + let event = Event::Mouse(MouseEvent::CursorMoved { + position: logical_pos, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + }); + + window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event); + + Some(0) + } + WM_MOUSEWHEEL | WM_MOUSEHWHEEL => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let value = (wparam >> 16) as i16; + let value = value as i32; + let value = value as f32 / WHEEL_DELTA as f32; + + let event = Event::Mouse(MouseEvent::WheelScrolled { + delta: if msg == WM_MOUSEWHEEL { + ScrollDelta::Lines { x: 0.0, y: value } + } else { + ScrollDelta::Lines { x: value, y: 0.0 } + }, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + }); + + window_state.handler.borrow_mut().as_mut().unwrap().on_event(&mut window, event); + + Some(0) + } + WM_LBUTTONDOWN | WM_LBUTTONUP | WM_MBUTTONDOWN | WM_MBUTTONUP | WM_RBUTTONDOWN + | WM_RBUTTONUP | WM_XBUTTONDOWN | WM_XBUTTONUP => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let mut mouse_button_counter = window_state.mouse_button_counter.get(); + + let button = match msg { + WM_LBUTTONDOWN | WM_LBUTTONUP => Some(MouseButton::Left), + WM_MBUTTONDOWN | WM_MBUTTONUP => Some(MouseButton::Middle), + WM_RBUTTONDOWN | WM_RBUTTONUP => Some(MouseButton::Right), + WM_XBUTTONDOWN | WM_XBUTTONUP => match GET_XBUTTON_WPARAM(wparam) { + XBUTTON1 => Some(MouseButton::Back), + XBUTTON2 => Some(MouseButton::Forward), + _ => None, + }, + _ => None, + }; + + if let Some(button) = button { + let event = match msg { + WM_LBUTTONDOWN | WM_MBUTTONDOWN | WM_RBUTTONDOWN | WM_XBUTTONDOWN => { + // Capture the mouse cursor on button down + mouse_button_counter = mouse_button_counter.saturating_add(1); + SetCapture(hwnd); + MouseEvent::ButtonPressed { + button, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + } + } + WM_LBUTTONUP | WM_MBUTTONUP | WM_RBUTTONUP | WM_XBUTTONUP => { + // Release the mouse cursor capture when all buttons are released + mouse_button_counter = mouse_button_counter.saturating_sub(1); + if mouse_button_counter == 0 { + ReleaseCapture(); + } + + MouseEvent::ButtonReleased { + button, + modifiers: window_state + .keyboard_state + .borrow() + .get_modifiers_from_mouse_wparam(wparam), + } + } + _ => { + unreachable!() + } + }; + + window_state.mouse_button_counter.set(mouse_button_counter); + + window_state + .handler + .borrow_mut() + .as_mut() + .unwrap() + .on_event(&mut window, Event::Mouse(event)); + } + + None + } + WM_TIMER => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + if wparam == WIN_FRAME_TIMER { + window_state.handler.borrow_mut().as_mut().unwrap().on_frame(&mut window); + } + + Some(0) + } + WM_CLOSE => { + // Make sure to release the borrow before the DefWindowProc call + { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + window_state + .handler + .borrow_mut() + .as_mut() + .unwrap() + .on_event(&mut window, Event::Window(WindowEvent::WillClose)); + } + + // DestroyWindow(hwnd); + // Some(0) + Some(DefWindowProcW(hwnd, msg, wparam, lparam)) + } + WM_CHAR | WM_SYSCHAR | WM_KEYDOWN | WM_SYSKEYDOWN | WM_KEYUP | WM_SYSKEYUP + | WM_INPUTLANGCHANGE => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let opt_event = + window_state.keyboard_state.borrow_mut().process_message(hwnd, msg, wparam, lparam); + + if let Some(event) = opt_event { + window_state + .handler + .borrow_mut() + .as_mut() + .unwrap() + .on_event(&mut window, Event::Keyboard(event)); + } + + if msg != WM_SYSKEYDOWN { + Some(0) + } else { + None + } + } + WM_SIZE => { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + let width = (lparam & 0xFFFF) as u16 as u32; + let height = ((lparam >> 16) & 0xFFFF) as u16 as u32; + + let new_window_info = { + let mut window_info = window_state.window_info.borrow_mut(); + let new_window_info = + WindowInfo::from_physical_size(PhySize { width, height }, window_info.scale()); + + // Only send the event if anything changed + if window_info.physical_size() == new_window_info.physical_size() { + return None; + } + + *window_info = new_window_info; + + new_window_info + }; + + window_state + .handler + .borrow_mut() + .as_mut() + .unwrap() + .on_event(&mut window, Event::Window(WindowEvent::Resized(new_window_info))); + + None + } + WM_DPICHANGED => { + // To avoid weirdness with the realtime borrow checker. + let new_rect = { + if let WindowScalePolicy::SystemScaleFactor = window_state.scale_policy { + let dpi = (wparam & 0xFFFF) as u16 as u32; + let scale_factor = dpi as f64 / 96.0; + + let mut window_info = window_state.window_info.borrow_mut(); + *window_info = + WindowInfo::from_logical_size(window_info.logical_size(), scale_factor); + + Some(( + RECT { + left: 0, + top: 0, + // todo: check if usize fits into i32 + right: window_info.physical_size().width as i32, + bottom: window_info.physical_size().height as i32, + }, + window_state.dw_style, + )) + } else { + None + } + }; + if let Some((mut new_rect, dw_style)) = new_rect { + // Convert this desired "client rectangle" size to the actual "window rectangle" + // size (Because of course you have to do that). + AdjustWindowRectEx(&mut new_rect, dw_style, 0, 0); + + // Windows makes us resize the window manually. This will trigger another `WM_SIZE` event, + // which we can then send the user the new scale factor. + SetWindowPos( + hwnd, + hwnd, + new_rect.left, + new_rect.top, + new_rect.right - new_rect.left, + new_rect.bottom - new_rect.top, + SWP_NOZORDER | SWP_NOMOVE, + ); + } + + None + } + // NOTE: `WM_NCDESTROY` is handled in the outer function because this deallocates the window + // state + BV_WINDOW_MUST_CLOSE => { + DestroyWindow(hwnd); + Some(0) + } + _ => None, + } +} + unsafe fn register_wnd_class() -> ATOM { // We generate a unique name for the new window class to prevent name collisions let class_name_str = format!("Baseview-{}", generate_guid()); @@ -378,40 +441,93 @@ unsafe fn unregister_wnd_class(wnd_class: ATOM) { UnregisterClassW(wnd_class as _, null_mut()); } +/// All data associated with the window. This uses internal mutability so the outer struct doesn't +/// need to be mutably borrowed. Mutably borrowing the entire `WindowState` can be problematic +/// because of the Windows message loops' reentrant nature. Care still needs to be taken to prevent +/// `handler` from indirectly triggering other events that would also need to be handled using +/// `handler`. struct WindowState { + /// The HWND belonging to this window. The window's actual state is stored in the `WindowState` + /// struct associated with this HWND through `unsafe { GetWindowLongPtrW(self.hwnd, + /// GWLP_USERDATA) } as *const WindowState`. + pub hwnd: HWND, window_class: ATOM, - window_info: WindowInfo, + window_info: RefCell, _parent_handle: Option, - keyboard_state: KeyboardState, - mouse_button_counter: usize, - handler: Box, + keyboard_state: RefCell, + mouse_button_counter: Cell, + // Initialized late so the `Window` can hold a reference to this `WindowState` + handler: RefCell>>, scale_policy: WindowScalePolicy, dw_style: u32, + /// Tasks that should be executed at the end of `wnd_proc`. This is needed to avoid mutably + /// borrowing the fields from `WindowState` more than once. For instance, when the window + /// handler requests a resize in response to a keyboard event, the window state will already be + /// borrowed in `wnd_proc`. So the `resize()` function below cannot also mutably borrow that + /// window state at the same time. + pub deferred_tasks: RefCell>, + #[cfg(feature = "opengl")] - gl_context: Arc>, + pub gl_context: Option, } impl WindowState { - #[cfg(not(feature = "opengl"))] - fn create_window(&self, hwnd: HWND) -> Window { - Window { hwnd } + fn create_window(&self) -> Window { + Window { state: self } } - #[cfg(feature = "opengl")] - fn create_window(&self, hwnd: HWND) -> Window { - Window { hwnd, gl_context: self.gl_context.clone() } + /// Handle a deferred task as described in [`Self::deferred_tasks + pub(self) fn handle_deferred_task(&self, task: WindowTask) { + match task { + WindowTask::Resize(size) => { + let window_info = { + let mut window_info = self.window_info.borrow_mut(); + let scaling = window_info.scale(); + *window_info = WindowInfo::from_logical_size(size, scaling); + + *window_info + }; + + // If the window is a standalone window then the size needs to include the window + // decorations + let mut rect = RECT { + left: 0, + top: 0, + right: window_info.physical_size().width as i32, + bottom: window_info.physical_size().height as i32, + }; + unsafe { + AdjustWindowRectEx(&mut rect, self.dw_style, 0, 0); + SetWindowPos( + self.hwnd, + self.hwnd, + 0, + 0, + rect.right - rect.left, + rect.bottom - rect.top, + SWP_NOZORDER | SWP_NOMOVE, + ) + }; + } + } } } -pub struct Window { - hwnd: HWND, - - #[cfg(feature = "opengl")] - gl_context: Arc>, +/// Tasks that must be deferred until the end of [`wnd_proc()`] to avoid reentrant `WindowState` +/// borrows. See the docstring on [`WindowState::deferred_tasks`] for more information. +#[derive(Debug, Clone)] +enum WindowTask { + /// Resize the window to the given size. The size is in logical pixels. DPI scaling is applied + /// automatically. + Resize(Size), } -impl Window { +pub struct Window<'a> { + state: &'a WindowState, +} + +impl Window<'_> { pub fn open_parented(parent: &P, options: WindowOpenOptions, build: B) -> WindowHandle where P: HasRawWindowHandle, @@ -527,38 +643,43 @@ impl Window { // todo: manage error ^ #[cfg(feature = "opengl")] - let gl_context: Arc> = Arc::new(options.gl_config.map(|gl_config| { + let gl_context: Option = options.gl_config.map(|gl_config| { let mut handle = Win32Handle::empty(); handle.hwnd = hwnd as *mut c_void; let handle = RawWindowHandleWrapper { handle: RawWindowHandle::Win32(handle) }; GlContext::create(&handle, gl_config).expect("Could not create OpenGL context") - })); - - #[cfg(not(feature = "opengl"))] - let handler = Box::new(build(&mut crate::Window::new(&mut Window { hwnd }))); - #[cfg(feature = "opengl")] - let handler = Box::new(build(&mut crate::Window::new(&mut Window { - hwnd, - gl_context: gl_context.clone(), - }))); + }); let (parent_handle, window_handle) = ParentHandle::new(hwnd); let parent_handle = if parented { Some(parent_handle) } else { None }; - let mut window_state = Box::new(RefCell::new(WindowState { + let window_state = Box::new(WindowState { + hwnd, window_class, - window_info, + window_info: RefCell::new(window_info), _parent_handle: parent_handle, - keyboard_state: KeyboardState::new(), - mouse_button_counter: 0, - handler, + keyboard_state: RefCell::new(KeyboardState::new()), + mouse_button_counter: Cell::new(0), + // The Window refers to this `WindowState`, so this `handler` needs to be + // initialized later + handler: RefCell::new(None), scale_policy: options.scale, dw_style: flags, + deferred_tasks: RefCell::new(VecDeque::with_capacity(4)), + #[cfg(feature = "opengl")] gl_context, - })); + }); + + let handler = { + let mut window = window_state.create_window(); + let mut window = crate::Window::new(&mut window); + + build(&mut window) + }; + *window_state.handler.borrow_mut() = Some(Box::new(handler)); // Only works on Windows 10 unfortunately. SetProcessDpiAwarenessContext( @@ -571,19 +692,17 @@ impl Window { let dpi = GetDpiForWindow(hwnd); let scale_factor = dpi as f64 / 96.0; - let mut window_state = window_state.get_mut(); - if window_state.window_info.scale() != scale_factor { - window_state.window_info = WindowInfo::from_logical_size( - window_state.window_info.logical_size(), - scale_factor, - ); + let mut window_info = window_state.window_info.borrow_mut(); + if window_info.scale() != scale_factor { + *window_info = + WindowInfo::from_logical_size(window_info.logical_size(), scale_factor); Some(RECT { left: 0, top: 0, // todo: check if usize fits into i32 - right: window_state.window_info.physical_size().width as i32, - bottom: window_state.window_info.physical_size().height as i32, + right: window_info.physical_size().width as i32, + bottom: window_info.physical_size().height as i32, }) } else { None @@ -605,8 +724,8 @@ impl Window { SetWindowPos( hwnd, hwnd, - new_rect.left as i32, - new_rect.top as i32, + new_rect.left, + new_rect.top, new_rect.right - new_rect.left, new_rect.bottom - new_rect.top, SWP_NOZORDER | SWP_NOMOVE, @@ -619,20 +738,27 @@ impl Window { pub fn close(&mut self) { unsafe { - PostMessageW(self.hwnd, BV_WINDOW_MUST_CLOSE, 0, 0); + PostMessageW(self.state.hwnd, BV_WINDOW_MUST_CLOSE, 0, 0); } } + pub fn resize(&mut self, size: Size) { + // To avoid reentrant event handler calls we'll defer the actual resizing until after the + // event has been handled + let task = WindowTask::Resize(size); + self.state.deferred_tasks.borrow_mut().push_back(task); + } + #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&GlContext> { - self.gl_context.as_ref().as_ref() + self.state.gl_context.as_ref() } } -unsafe impl HasRawWindowHandle for Window { +unsafe impl HasRawWindowHandle for Window<'_> { fn raw_window_handle(&self) -> RawWindowHandle { let mut handle = Win32Handle::empty(); - handle.hwnd = self.hwnd as *mut c_void; + handle.hwnd = self.state.hwnd as *mut c_void; RawWindowHandle::Win32(handle) } diff --git a/src/window.rs b/src/window.rs index a0d91f6..c0ef1ac 100644 --- a/src/window.rs +++ b/src/window.rs @@ -4,6 +4,7 @@ use raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; use crate::event::{Event, EventStatus}; use crate::window_open_options::WindowOpenOptions; +use crate::Size; #[cfg(target_os = "macos")] use crate::macos as platform; @@ -53,12 +54,22 @@ pub trait WindowHandler { } pub struct Window<'a> { + #[cfg(target_os = "windows")] + window: &'a mut platform::Window<'a>, + #[cfg(not(target_os = "windows"))] window: &'a mut platform::Window, + // so that Window is !Send on all platforms phantom: PhantomData<*mut ()>, } impl<'a> Window<'a> { + #[cfg(target_os = "windows")] + pub(crate) fn new(window: &'a mut platform::Window<'a>) -> Window<'a> { + Window { window, phantom: PhantomData } + } + + #[cfg(not(target_os = "windows"))] pub(crate) fn new(window: &mut platform::Window) -> Window { Window { window, phantom: PhantomData } } @@ -98,6 +109,12 @@ impl<'a> Window<'a> { self.window.close(); } + /// Resize the window to the given size. The size is always in logical pixels. DPI scaling will + /// automatically be accounted for. + pub fn resize(&mut self, size: Size) { + self.window.resize(size); + } + /// If provided, then an OpenGL context will be created for this window. You'll be able to /// access this context through [crate::Window::gl_context]. #[cfg(feature = "opengl")] diff --git a/src/x11/window.rs b/src/x11/window.rs index 2ad0cad..8056f90 100644 --- a/src/x11/window.rs +++ b/src/x11/window.rs @@ -12,7 +12,7 @@ use xcb::StructPtr; use super::XcbConnection; use crate::{ - Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, WindowEvent, + Event, MouseButton, MouseCursor, MouseEvent, PhyPoint, PhySize, ScrollDelta, Size, WindowEvent, WindowHandler, WindowInfo, WindowOpenOptions, WindowScalePolicy, }; @@ -389,6 +389,24 @@ impl Window { self.close_requested = true; } + pub fn resize(&mut self, size: Size) { + let scaling = self.window_info.scale(); + let new_window_info = WindowInfo::from_logical_size(size, scaling); + + xcb::configure_window( + &self.xcb_connection.conn, + self.window_id, + &[ + (xcb::CONFIG_WINDOW_WIDTH as u16, new_window_info.physical_size().width), + (xcb::CONFIG_WINDOW_HEIGHT as u16, new_window_info.physical_size().height), + ], + ); + self.xcb_connection.conn.flush(); + + // This will trigger a `ConfigureNotify` event which will in turn change `self.window_info` + // and notify the window handler about it + } + #[cfg(feature = "opengl")] pub fn gl_context(&self) -> Option<&crate::gl::GlContext> { self.gl_context.as_ref()