diff --git a/src/workbench/WindowResizeHandles.module.css b/src/workbench/WindowResizeHandles.module.css new file mode 100644 index 000000000..02070d95a --- /dev/null +++ b/src/workbench/WindowResizeHandles.module.css @@ -0,0 +1,68 @@ +/* Invisible resize hit areas hugging the frameless window's edges. */ +.n, +.s, +.e, +.w, +.ne, +.nw, +.se, +.sw { + position: fixed; + z-index: 9999; + pointer-events: auto; +} +.n, +.s { + left: 8px; + right: 8px; + height: 6px; +} +.e, +.w { + top: 8px; + bottom: 8px; + width: 6px; +} +.ne, +.nw, +.se, +.sw { + width: 12px; + height: 12px; +} +.n { + top: 0; + cursor: ns-resize; +} +.s { + bottom: 0; + cursor: ns-resize; +} +.e { + right: 0; + cursor: ew-resize; +} +.w { + left: 0; + cursor: ew-resize; +} +.ne { + top: 0; + right: 0; + cursor: nesw-resize; +} +.nw { + top: 0; + left: 0; + cursor: nwse-resize; +} +.se { + bottom: 0; + right: 0; + cursor: nwse-resize; +} +.sw { + bottom: 0; + left: 0; + cursor: nesw-resize; +} diff --git a/src/workbench/WindowResizeHandles.tsx b/src/workbench/WindowResizeHandles.tsx new file mode 100644 index 000000000..0b5fa9da6 --- /dev/null +++ b/src/workbench/WindowResizeHandles.tsx @@ -0,0 +1,75 @@ +/** + * Frameless-window resize handles: Windows gives undecorated windows no + * resize borders, so the title bar's frameless look leaves the window + * stuck at its launch size. Eight invisible hit areas along the edges + * start a native resize drag, like any other desktop app. + * + * macOS renders its own overlay title bar but still provides resize + * handles, so nothing is added there. + */ +import { useEffect, useState } from "react"; +import { usePlatform } from "@/platform/platformInfo"; +import { isTauri } from "@/platform/ipc"; +import styles from "./WindowResizeHandles.module.css"; + +type Dir = "East" | "North" | "NorthEast" | "NorthWest" | "South" | "SouthEast" | "SouthWest" | "West"; + +const EDGES: { dir: Dir; cls: string }[] = [ + { dir: "North", cls: styles.n! }, + { dir: "South", cls: styles.s! }, + { dir: "East", cls: styles.e! }, + { dir: "West", cls: styles.w! }, + { dir: "NorthEast", cls: styles.ne! }, + { dir: "NorthWest", cls: styles.nw! }, + { dir: "SouthEast", cls: styles.se! }, + { dir: "SouthWest", cls: styles.sw! }, +]; + +async function start(dir: Dir) { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().startResizeDragging(dir); +} + +export function WindowResizeHandles() { + // A frameless window needs them; a decorated one already has native borders. + const frameless = usePlatform((s) => s.framelessTitleBar); + const enabled = isTauri() && frameless; + // A maximized or full-screen window has nothing to resize; the edges belong to the content. + const [sized, setSized] = useState(false); + useEffect(() => { + if (!enabled) return; + let disposed = false; + let unlisten: (() => void) | undefined; + void import("@tauri-apps/api/window").then(async ({ getCurrentWindow }) => { + const w = getCurrentWindow(); + const sync = () => + void Promise.all([w.isMaximized(), w.isFullscreen()]).then(([m, f]) => !disposed && setSized(m || f)); + sync(); + const off = await w.onResized(sync); + if (disposed) off(); + else unlisten = off; + }); + return () => { + disposed = true; + unlisten?.(); + }; + }, [enabled]); + if (!enabled || sized) return null; + return ( + <> + {EDGES.map(({ dir, cls }) => ( +
); }