From f29b78811080bc8313459f34545152d939c62bf6 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Wed, 5 Jun 2024 19:03:22 +0300 Subject: [PATCH] fix(core/wry): implement resizing natively on Windows (#9862) closes #7388 closes #9510 closes #9464 ref #9268 ref #9053 ref #8770 ref #8750 ref #4012 --- .changes/undecorated-resizing.md | 10 + core/tauri-runtime-wry/src/lib.rs | 54 +-- .../src/undecorated_resizing.rs | 415 ++++++++++++------ 3 files changed, 319 insertions(+), 160 deletions(-) create mode 100644 .changes/undecorated-resizing.md diff --git a/.changes/undecorated-resizing.md b/.changes/undecorated-resizing.md new file mode 100644 index 000000000..e13e22073 --- /dev/null +++ b/.changes/undecorated-resizing.md @@ -0,0 +1,10 @@ +--- +"tauri": "patch:bug" +"tauri-runtime-wry": "patch:bug" +--- + +On Windows, handle resizing undecorated windows natively which improves performance and fixes a couple of annoyances with previous JS implementation: +- No more cursor flickering when moving the cursor across an edge. +- Can resize from top even when `data-tauri-drag-region` element exists there. +- Upon starting rezing, clicks don't go through elements behind it so no longer accidental clicks. + diff --git a/core/tauri-runtime-wry/src/lib.rs b/core/tauri-runtime-wry/src/lib.rs index 7102dce43..d0f7af88e 100644 --- a/core/tauri-runtime-wry/src/lib.rs +++ b/core/tauri-runtime-wry/src/lib.rs @@ -1988,8 +1988,6 @@ pub struct WindowWrapper { webviews: Vec, window_event_listeners: WindowEventListeners, #[cfg(windows)] - is_window_fullscreen: bool, - #[cfg(windows)] is_window_transparent: bool, #[cfg(windows)] surface: Option, Arc>>, @@ -2773,7 +2771,15 @@ fn handle_user_message( WindowMessage::Destroy => { panic!("cannot handle `WindowMessage::Destroy` on the main thread") } - WindowMessage::SetDecorations(decorations) => window.set_decorations(decorations), + WindowMessage::SetDecorations(decorations) => { + window.set_decorations(decorations); + #[cfg(windows)] + if decorations { + undecorated_resizing::detach_resize_handler(window.hwnd()); + } else { + undecorated_resizing::attach_resize_handler(window.hwnd()); + } + } WindowMessage::SetShadow(_enable) => { #[cfg(windows)] window.set_undecorated_shadow(_enable); @@ -2806,10 +2812,6 @@ fn handle_user_message( } else { window.set_fullscreen(None) } - #[cfg(windows)] - if let Some(w) = windows.0.borrow_mut().get_mut(&id) { - w.is_window_fullscreen = fullscreen; - } } WindowMessage::SetFocus => { window.set_focus(); @@ -3197,8 +3199,6 @@ fn handle_user_message( Message::CreateRawWindow(window_id, handler, sender) => { let (label, builder) = handler(); - #[cfg(windows)] - let is_window_fullscreen = builder.window.fullscreen.is_some(); #[cfg(windows)] let is_window_transparent = builder.window.transparent; @@ -3232,8 +3232,6 @@ fn handle_user_message( window_event_listeners: Default::default(), webviews: Vec::new(), #[cfg(windows)] - is_window_fullscreen, - #[cfg(windows)] is_window_transparent, #[cfg(windows)] surface, @@ -3577,8 +3575,6 @@ fn create_window( #[cfg(windows)] let is_window_transparent = window_builder.inner.window.transparent; - #[cfg(windows)] - let is_window_fullscreen = window_builder.inner.window.fullscreen.is_some(); #[cfg(target_os = "macos")] { @@ -3727,8 +3723,6 @@ fn create_window( webviews, window_event_listeners, #[cfg(windows)] - is_window_fullscreen, - #[cfg(windows)] is_window_transparent, #[cfg(windows)] surface, @@ -3818,11 +3812,6 @@ fn create_webview( .with_accept_first_mouse(webview_attributes.accept_first_mouse) .with_hotkeys_zoom(webview_attributes.zoom_hotkeys_enabled); - #[cfg(windows)] - if kind == WebviewKind::WindowContent { - webview_builder = webview_builder.with_initialization_script(undecorated_resizing::SCRIPT); - } - if webview_attributes.drag_drop_handler_enabled { let proxy = context.proxy.clone(); let window_id_ = window_id.clone(); @@ -4054,15 +4043,19 @@ fn create_webview( .build() .map_err(|e| Error::CreateWebview(Box::new(e)))?; - #[cfg(any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ))] if kind == WebviewKind::WindowContent { + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] undecorated_resizing::attach_resize_handler(&webview); + #[cfg(windows)] + if window.is_resizable() && !window.is_decorated() { + undecorated_resizing::attach_resize_handler(window.hwnd()); + } } #[cfg(windows)] @@ -4127,13 +4120,6 @@ fn create_ipc_handler( ipc_handler: Option>>, ) -> Box { Box::new(move |request| { - #[cfg(windows)] - if _kind == WebviewKind::WindowContent - && undecorated_resizing::handle_request(context.clone(), *window_id.lock().unwrap(), &request) - { - return; - } - if let Some(handler) = &ipc_handler { handler( DetachedWebview { diff --git a/core/tauri-runtime-wry/src/undecorated_resizing.rs b/core/tauri-runtime-wry/src/undecorated_resizing.rs index 5ec301d7b..fbf619ad2 100644 --- a/core/tauri-runtime-wry/src/undecorated_resizing.rs +++ b/core/tauri-runtime-wry/src/undecorated_resizing.rs @@ -26,10 +26,6 @@ pub use self::gtk::*; #[cfg(windows)] pub use self::windows::*; -#[cfg(windows)] -type WindowDimensions = u32; -#[cfg(not(windows))] -type WindowDimensions = i32; #[cfg(windows)] type WindowPositions = i32; #[cfg(not(windows))] @@ -49,27 +45,22 @@ enum HitTestResult { NoWhere, } +#[allow(clippy::too_many_arguments)] fn hit_test( - width: WindowDimensions, - height: WindowDimensions, - x: WindowPositions, - y: WindowPositions, + left: WindowPositions, + top: WindowPositions, + right: WindowPositions, + bottom: WindowPositions, + cx: WindowPositions, + cy: WindowPositions, border_x: WindowPositions, border_y: WindowPositions, ) -> HitTestResult { - #[cfg(windows)] - let (top, left) = (0, 0); - #[cfg(not(windows))] - let (top, left) = (0., 0.); - - let bottom = top + height as WindowPositions; - let right = left + width as WindowPositions; - #[rustfmt::skip] - let result = (LEFT * (x < left + border_x) as isize) - | (RIGHT * (x >= right - border_x) as isize) - | (TOP * (y < top + border_y) as isize) - | (BOTTOM * (y >= bottom - border_y) as isize); + let result = (LEFT * (cx < left + border_x) as isize) + | (RIGHT * (cx >= right - border_x) as isize) + | (TOP * (cy < top + border_y) as isize) + | (BOTTOM * (cy >= bottom - border_y) as isize); match result { CLIENT => HitTestResult::Client, @@ -89,117 +80,285 @@ fn hit_test( mod windows { use super::{hit_test, HitTestResult}; - use tao::window::{CursorIcon, ResizeDirection, Window}; - use windows::Win32::UI::WindowsAndMessaging::{ - GetSystemMetrics, SM_CXFRAME, SM_CXPADDEDBORDER, SM_CYFRAME, - }; - - const MESSAGE_MOUSEMOVE: &str = "__internal_on_mousemove__|"; - const MESSAGE_MOUSEDOWN: &str = "__internal_on_mousedown__|"; - pub const SCRIPT: &str = r#" -;(function () { - document.addEventListener('mousemove', (e) => { - window.ipc.postMessage( - `__internal_on_mousemove__|${e.clientX},${e.clientY}` - ) - }) - document.addEventListener('mousedown', (e) => { - if (e.button === 0) { - window.ipc.postMessage( - `__internal_on_mousedown__|${e.clientX},${e.clientY}` - ) - } - }) -})() -"#; + use windows::core::*; + use windows::Win32::System::LibraryLoader::*; + use windows::Win32::UI::WindowsAndMessaging::*; + use windows::Win32::{Foundation::*, UI::Shell::SetWindowSubclass}; + use windows::Win32::{Graphics::Gdi::*, UI::Shell::DefSubclassProc}; impl HitTestResult { - fn drag_resize_window(&self, window: &Window) { - self.change_cursor(window); - let edge = match self { - HitTestResult::Left => ResizeDirection::West, - HitTestResult::Right => ResizeDirection::East, - HitTestResult::Top => ResizeDirection::North, - HitTestResult::Bottom => ResizeDirection::South, - HitTestResult::TopLeft => ResizeDirection::NorthWest, - HitTestResult::TopRight => ResizeDirection::NorthEast, - HitTestResult::BottomLeft => ResizeDirection::SouthWest, - HitTestResult::BottomRight => ResizeDirection::SouthEast, - - // if not on an edge, don't start resizing - _ => return, - }; - let _ = window.drag_resize_window(edge); - } - - fn change_cursor(&self, window: &Window) { - let cursor = match self { - HitTestResult::Left => CursorIcon::WResize, - HitTestResult::Right => CursorIcon::EResize, - HitTestResult::Top => CursorIcon::NResize, - HitTestResult::Bottom => CursorIcon::SResize, - HitTestResult::TopLeft => CursorIcon::NwResize, - HitTestResult::TopRight => CursorIcon::NeResize, - HitTestResult::BottomLeft => CursorIcon::SwResize, - HitTestResult::BottomRight => CursorIcon::SeResize, - - // if not on an edge, don't change the cursor, otherwise we cause flickering - _ => return, - }; - window.set_cursor_icon(cursor); + fn to_win32(self) -> i32 { + match self { + HitTestResult::Left => HTLEFT as _, + HitTestResult::Right => HTRIGHT as _, + HitTestResult::Top => HTTOP as _, + HitTestResult::Bottom => HTBOTTOM as _, + HitTestResult::TopLeft => HTTOPLEFT as _, + HitTestResult::TopRight => HTTOPRIGHT as _, + HitTestResult::BottomLeft => HTBOTTOMLEFT as _, + HitTestResult::BottomRight => HTBOTTOMRIGHT as _, + _ => HTTRANSPARENT, + } } } - // Returns whether handled or not - pub fn handle_request( - context: crate::Context, - window_id: crate::WindowId, - request: &http::Request, - ) -> bool { - if let Some(args) = request.body().strip_prefix(MESSAGE_MOUSEMOVE) { - if let Some(window) = context.main_thread.windows.0.borrow().get(&window_id) { - if let Some(w) = window.inner.as_ref() { - if !w.is_decorated() - && w.is_resizable() - && !w.is_maximized() - && !window.is_window_fullscreen - { - let (x, y) = args.split_once(',').unwrap(); - let (x, y) = (x.parse().unwrap(), y.parse().unwrap()); - let size = w.inner_size(); - let padded_border = unsafe { GetSystemMetrics(SM_CXPADDEDBORDER) }; - let border_x = unsafe { GetSystemMetrics(SM_CXFRAME) + padded_border }; - let border_y = unsafe { GetSystemMetrics(SM_CYFRAME) + padded_border }; - hit_test(size.width, size.height, x, y, border_x, border_y).change_cursor(w); - } - } - } + const CLASS_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_BORDERS"); + const WINDOW_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_WINDOW"); - return true; - } - if let Some(args) = request.body().strip_prefix(MESSAGE_MOUSEDOWN) { - if let Some(window) = context.main_thread.windows.0.borrow().get(&window_id) { - if let Some(w) = window.inner.as_ref() { - if !w.is_decorated() - && w.is_resizable() - && !w.is_maximized() - && !window.is_window_fullscreen - { - let (x, y) = args.split_once(',').unwrap(); - let (x, y) = (x.parse().unwrap(), y.parse().unwrap()); - let size = w.inner_size(); - let padded_border = unsafe { GetSystemMetrics(SM_CXPADDEDBORDER) }; - let border_x = unsafe { GetSystemMetrics(SM_CXFRAME) + padded_border }; - let border_y = unsafe { GetSystemMetrics(SM_CYFRAME) + padded_border }; - hit_test(size.width, size.height, x, y, border_x, border_y).drag_resize_window(w); - } - } - } + pub fn attach_resize_handler(hwnd: isize) { + let parent = HWND(hwnd); - return true; + let child = unsafe { FindWindowExW(parent, HWND::default(), CLASS_NAME, WINDOW_NAME) }; + if child != HWND::default() { + return; } - false + let class = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: WNDCLASS_STYLES::default(), + lpfnWndProc: Some(drag_resize_window_proc), + cbClsExtra: 0, + cbWndExtra: 0, + hInstance: unsafe { HINSTANCE(GetModuleHandleW(PCWSTR::null()).unwrap_or_default().0) }, + hIcon: HICON::default(), + hCursor: HCURSOR::default(), + hbrBackground: HBRUSH::default(), + lpszMenuName: PCWSTR::null(), + lpszClassName: CLASS_NAME, + hIconSm: HICON::default(), + }; + + unsafe { RegisterClassExW(&class) }; + + let mut rect = RECT::default(); + unsafe { GetClientRect(parent, &mut rect).unwrap() }; + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let drag_window = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE::default(), + CLASS_NAME, + WINDOW_NAME, + WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS, + 0, + 0, + width, + height, + parent, + HMENU::default(), + GetModuleHandleW(PCWSTR::null()).unwrap_or_default(), + None, + ) + }; + + unsafe { + set_drag_hwnd_rgn(drag_window, width, height); + + let _ = SetWindowPos( + drag_window, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOSIZE, + ); + + let _ = SetWindowSubclass( + parent, + Some(subclass_parent), + (WM_USER + 1) as _, + drag_window.0 as _, + ); + } + } + + unsafe extern "system" fn subclass_parent( + parent: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + _: usize, + child: usize, + ) -> LRESULT { + if msg == WM_SIZE { + let child = HWND(child as _); + + if is_maximized(parent).unwrap_or(false) { + let _ = SetWindowPos( + child, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + } else { + let mut rect = RECT::default(); + if GetClientRect(parent, &mut rect).is_ok() { + let width = rect.right - rect.left; + let height = rect.bottom - rect.top; + + let _ = SetWindowPos( + child, + HWND_TOP, + 0, + 0, + width, + height, + SWP_ASYNCWINDOWPOS | SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOMOVE, + ); + + set_drag_hwnd_rgn(child, width, height); + } + } + } + + DefSubclassProc(parent, msg, wparam, lparam) + } + + unsafe extern "system" fn drag_resize_window_proc( + child: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, + ) -> LRESULT { + match msg { + WM_NCHITTEST => { + let parent = GetParent(child); + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + let padded_border = GetSystemMetrics(SM_CXPADDEDBORDER); + let border_x = GetSystemMetrics(SM_CXFRAME) + padded_border; + let border_y = GetSystemMetrics(SM_CYFRAME) + padded_border; + + let res = hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ); + + return LRESULT(res.to_win32() as _); + } + + WM_NCLBUTTONDOWN => { + let parent = GetParent(child); + let style = GetWindowLongPtrW(parent, GWL_STYLE); + let style = WINDOW_STYLE(style as u32); + + let is_resizable = (style & WS_SIZEBOX).0 != 0; + if !is_resizable { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let mut rect = RECT::default(); + if GetWindowRect(child, &mut rect).is_err() { + return DefWindowProcW(child, msg, wparam, lparam); + } + + let (cx, cy) = (GET_X_LPARAM(lparam) as i32, GET_Y_LPARAM(lparam) as i32); + + let padded_border = GetSystemMetrics(SM_CXPADDEDBORDER); + let border_x = GetSystemMetrics(SM_CXFRAME) + padded_border; + let border_y = GetSystemMetrics(SM_CYFRAME) + padded_border; + + let res = hit_test( + rect.left, + rect.top, + rect.right, + rect.bottom, + cx, + cy, + border_x, + border_y, + ); + + if res != HitTestResult::NoWhere { + let points = POINTS { + x: cx as i16, + y: cy as i16, + }; + + let _ = PostMessageW( + parent, + WM_NCLBUTTONDOWN, + WPARAM(res.to_win32() as _), + LPARAM(&points as *const _ as _), + ); + } + + return LRESULT(0); + } + + _ => {} + } + + DefWindowProcW(child, msg, wparam, lparam) + } + + pub fn detach_resize_handler(hwnd: isize) { + let hwnd = HWND(hwnd); + + let child = unsafe { FindWindowExW(hwnd, HWND::default(), CLASS_NAME, WINDOW_NAME) }; + if child == HWND::default() { + return; + } + + let _ = unsafe { DestroyWindow(child) }; + } + + unsafe fn set_drag_hwnd_rgn(hwnd: HWND, width: i32, height: i32) { + let padded_border = GetSystemMetrics(SM_CXPADDEDBORDER); + let border_x = GetSystemMetrics(SM_CXFRAME) + padded_border; + let border_y = GetSystemMetrics(SM_CYFRAME) + padded_border; + + let hrgn1 = CreateRectRgn(0, 0, width, height); + let hrgn2 = CreateRectRgn(border_x, border_y, width - border_x, height - border_y); + CombineRgn(hrgn1, hrgn1, hrgn2, RGN_DIFF); + SetWindowRgn(hwnd, hrgn1, true); + } + + fn is_maximized(window: HWND) -> windows::core::Result { + let mut placement = WINDOWPLACEMENT { + length: std::mem::size_of::() as u32, + ..WINDOWPLACEMENT::default() + }; + unsafe { GetWindowPlacement(window, &mut placement)? }; + Ok(placement.showCmd == SW_MAXIMIZE.0 as u32) + } + + /// Implementation of the `GET_X_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_X_LPARAM(lparam: LPARAM) -> i16 { + ((lparam.0 as usize) & 0xFFFF) as u16 as i16 + } + + /// Implementation of the `GET_Y_LPARAM` macro. + #[allow(non_snake_case)] + #[inline] + fn GET_Y_LPARAM(lparam: LPARAM) -> i16 { + (((lparam.0 as usize) & 0xFFFF_0000) >> 16) as u16 as i16 } } @@ -255,8 +414,10 @@ mod gtk { let (client_x, client_y) = (root_x - window_x as f64, root_y - window_y as f64); let border = window.scale_factor() * BORDERLESS_RESIZE_INSET; let edge = hit_test( - window.width(), - window.height(), + 0.0, + 0.0, + window.width() as f64, + window.height() as f64, client_x, client_y, border as _, @@ -294,8 +455,10 @@ mod gtk { let (client_x, client_y) = (root_x - window_x as f64, root_y - window_y as f64); let border = window.scale_factor() * BORDERLESS_RESIZE_INSET; let edge = hit_test( - window.width(), - window.height(), + 0.0, + 0.0, + window.width() as f64, + window.height() as f64, client_x, client_y, border as _,