Browse Source

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
Amr Bashir 1 year ago
parent
commit
f29b788110

+ 10 - 0
.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.
+

+ 20 - 34
core/tauri-runtime-wry/src/lib.rs

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

+ 283 - 120
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 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,
+      }
     }
+  }
+
+  const CLASS_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_BORDERS");
+  const WINDOW_NAME: PCWSTR = w!("TAURI_DRAG_RESIZE_WINDOW");
+
+  pub fn attach_resize_handler(hwnd: isize) {
+    let parent = HWND(hwnd);
 
-    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);
+    let child = unsafe { FindWindowExW(parent, HWND::default(), CLASS_NAME, WINDOW_NAME) };
+    if child != HWND::default() {
+      return;
+    }
+
+    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 _,
+      );
     }
   }
 
-  // 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);
-          }
+  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);
         }
       }
-
-      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);
-          }
+
+    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);
       }
 
-      return true;
+      _ => {}
     }
 
-    false
+    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 _,