| 1 | import { useEffect } from "react"; |
| 2 | |
| 3 | const RESIZE_CURSORS = new Set([ |
| 4 | "e-resize", |
| 5 | "n-resize", |
| 6 | "ne-resize", |
| 7 | "nw-resize", |
| 8 | "s-resize", |
| 9 | "se-resize", |
| 10 | "sw-resize", |
| 11 | "w-resize", |
| 12 | ]); |
| 13 | |
| 14 | /** |
| 15 | * WailsWailsFlags mirrors the `window.wails.flags` object injected by the |
| 16 | * Wails v2 runtime (see internal/frontend/runtime/desktop/main.js). |
| 17 | */ |
| 18 | interface WailsFlags { |
| 19 | enableResize: boolean; |
| 20 | resizeEdge: string | undefined; |
| 21 | borderThickness: number; |
| 22 | defaultCursor: string | null; |
| 23 | cssDragProperty: string; |
| 24 | cssDragValue: string; |
| 25 | cssDropProperty: string; |
| 26 | cssDropValue: string; |
| 27 | shouldDrag: boolean; |
| 28 | deferDragToMouseMove: boolean; |
| 29 | disableScrollbarDrag: boolean; |
| 30 | disableDefaultContextMenu: boolean; |
| 31 | enableWailsDragAndDrop: boolean; |
| 32 | } |
| 33 | |
| 34 | interface WailsWindow { |
| 35 | wails: { |
| 36 | flags: WailsFlags; |
| 37 | Callback: (msg: string) => void; |
| 38 | EventsNotify: (msg: string) => void; |
| 39 | }; |
| 40 | } |
| 41 | |
| 42 | declare global { |
| 43 | interface Window { |
| 44 | wails?: WailsWindow["wails"]; |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | /** |
| 49 | * useWailsResizeFix |
| 50 | * |
| 51 | * Workaround for a Wails v2.12 frameless-window resize detection bug where |
| 52 | * `window.outerWidth/outerHeight` (device-independent pixels) is compared |
| 53 | * with `e.clientX/e.clientY` (CSS pixels). When WebView2 ZoomFactor ≠ 1 |
| 54 | * these coordinate spaces diverge, causing the right/bottom resize zone to |
| 55 | * extend far inward or disappear entirely. |
| 56 | * |
| 57 | * This hook disables the built-in mousemove handler (which uses the wrong |
| 58 | * coordinate space) and replaces it with one that uses `window.innerWidth` |
| 59 | * and `window.innerHeight` — both in CSS pixels, matching `e.clientX/Y`. |
| 60 | * |
| 61 | * The Wails mousedown handler still works because it only reads |
| 62 | * `window.wails.flags.resizeEdge`, which we continue to set here. |
| 63 | * |
| 64 | * --- Maximised-window guard --- |
| 65 | * |
| 66 | * Maximised state cannot be inferred safely from viewport dimensions: a |
| 67 | * manually sized or FancyZones-managed window can also fill the work area. |
| 68 | * Instead, this hook consumes the native maximise state shared with the Windows |
| 69 | * titlebar controls. Mousemove stays synchronous and never performs IPC. |
| 70 | * |
| 71 | * When maximised, edge detection is skipped and any stale resize cursor is |
| 72 | * replaced with an explicit default cursor. This also covers startup restores |
| 73 | * where the native cursor is already stuck but Wails' `resizeEdge` flag was |
| 74 | * never populated. |
| 75 | * |
| 76 | * Upstream fix: https://github.com/wailsapp/wails/issues/4590 (Wails v3 |
| 77 | * sidestepped by clamping zoom ≥ 1.0). Once Wails v2 ships a proper fix |
| 78 | * this hook can be deleted. |
| 79 | * |
| 80 | * @example |
| 81 | * // In App.tsx or any component mounted for the app's lifetime: |
| 82 | * useWailsResizeFix(desktopPlatform === "windows"); |
| 83 | */ |
| 84 | export function useWailsResizeFix(enabled: boolean, maximised = false): void { |
| 85 | useEffect(() => { |
| 86 | if (!enabled) return; |
| 87 | const wails = window.wails; |
| 88 | if (!wails) return; // not inside a Wails webview → no-op |
| 89 | |
| 90 | const flags = wails.flags; |
| 91 | const bt = flags.borderThickness ?? 6; |
| 92 | const previousEnableResize = flags.enableResize; |
| 93 | const previousResizeEdge = flags.resizeEdge; |
| 94 | const previousCursor = document.documentElement.style.cursor; |
| 95 | |
| 96 | // Prefer Wails' remembered cursor. A resize-shaped inline cursor at mount |
| 97 | // is stale state, not the application's default. |
| 98 | const rememberedCursor = flags.defaultCursor ?? previousCursor; |
| 99 | const defaultCursor = RESIZE_CURSORS.has(rememberedCursor) ? "" : rememberedCursor; |
| 100 | const restoredCursor = defaultCursor || "default"; |
| 101 | |
| 102 | const clearResizeState = () => { |
| 103 | flags.resizeEdge = undefined; |
| 104 | if (document.documentElement.style.cursor !== restoredCursor) { |
| 105 | document.documentElement.style.cursor = restoredCursor; |
| 106 | } |
| 107 | }; |
| 108 | |
| 109 | // Normalise stale startup state immediately, before the first native state |
| 110 | // query completes. A normal window will restore the correct edge on its next |
| 111 | // mousemove; a maximised window keeps the default cursor. |
| 112 | if (maximised || previousResizeEdge !== undefined || RESIZE_CURSORS.has(previousCursor)) { |
| 113 | clearResizeState(); |
| 114 | } |
| 115 | |
| 116 | const onMouseMove = (e: MouseEvent) => { |
| 117 | if (maximised) { |
| 118 | clearResizeState(); |
| 119 | return; |
| 120 | } |
| 121 | |
| 122 | // Both operands in CSS pixels — the bug fix. |
| 123 | const iw = window.innerWidth; |
| 124 | const ih = window.innerHeight; |
| 125 | const cx = e.clientX; |
| 126 | const cy = e.clientY; |
| 127 | |
| 128 | const right = iw - cx < bt; |
| 129 | const left = cx < bt; |
| 130 | const top = cy < bt; |
| 131 | const bottom = ih - cy < bt; |
| 132 | |
| 133 | let edge: string | undefined; |
| 134 | if (right && bottom) edge = "se-resize"; |
| 135 | else if (left && bottom) edge = "sw-resize"; |
| 136 | else if (right && top) edge = "ne-resize"; |
| 137 | else if (left && top) edge = "nw-resize"; |
| 138 | else if (right) edge = "e-resize"; |
| 139 | else if (left) edge = "w-resize"; |
| 140 | else if (top) edge = "n-resize"; |
| 141 | else if (bottom) edge = "s-resize"; |
| 142 | |
| 143 | if (edge !== flags.resizeEdge) { |
| 144 | flags.resizeEdge = edge; |
| 145 | document.documentElement.style.cursor = edge ?? restoredCursor; |
| 146 | } |
| 147 | }; |
| 148 | |
| 149 | // Disable Wails' built-in mousemove handler (the one that uses outerWidth). |
| 150 | flags.enableResize = false; |
| 151 | window.addEventListener("mousemove", onMouseMove); |
| 152 | |
| 153 | return () => { |
| 154 | window.removeEventListener("mousemove", onMouseMove); |
| 155 | flags.enableResize = previousEnableResize; |
| 156 | flags.resizeEdge = previousResizeEdge; |
| 157 | document.documentElement.style.cursor = previousCursor; |
| 158 | }; |
| 159 | }, [enabled, maximised]); |
| 160 | } |
| 161 |