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
This commit is contained in:
Amr Bashir 2024-06-05 19:03:22 +03:00 committed by GitHub
parent fafc238f72
commit f29b788110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 319 additions and 160 deletions

View File

@ -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.

View File

@ -1988,8 +1988,6 @@ pub struct WindowWrapper {
webviews: Vec<WebviewWrapper>,
window_event_listeners: WindowEventListeners,
#[cfg(windows)]
is_window_fullscreen: bool,
#[cfg(windows)]
is_window_transparent: bool,
#[cfg(windows)]
surface: Option<softbuffer::Surface<Arc<Window>, Arc<Window>>>,
@ -2773,7 +2771,15 @@ fn handle_user_message<T: UserEvent>(
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<T: UserEvent>(
} 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<T: UserEvent>(
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<T: UserEvent>(
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<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
#[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<T: UserEvent, F: Fn(RawWindow) + Send + 'static>(
webviews,
window_event_listeners,
#[cfg(windows)]
is_window_fullscreen,
#[cfg(windows)]
is_window_transparent,
#[cfg(windows)]
surface,
@ -3818,11 +3812,6 @@ fn create_webview<T: UserEvent>(
.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<T: UserEvent>(
.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<T: UserEvent>(
ipc_handler: Option<WebviewIpcHandler<T, Wry<T>>>,
) -> Box<IpcHandler> {
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 {

View File

@ -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<T: crate::UserEvent>(
context: crate::Context<T>,
window_id: crate::WindowId,
request: &http::Request<String>,
) -> 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::<WNDCLASSEXW>() 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<bool> {
let mut placement = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() 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 _,