refactor: migrate to VS Code titlebar drag region pattern

This commit is contained in:
Mohamed Boudra
2026-04-01 16:00:33 +07:00
parent 953f7898e7
commit 58d72bea87
20 changed files with 963 additions and 610 deletions
@@ -1,28 +0,0 @@
import { describe, expect, it } from "vitest";
import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates";
describe("readFiniteScreenPoint", () => {
it("returns finite screen coordinates", () => {
expect(readFiniteScreenPoint({ screenX: 1280, screenY: 720 })).toEqual({
screenX: 1280,
screenY: 720,
});
});
it("rejects NaN screen coordinates", () => {
expect(readFiniteScreenPoint({ screenX: Number.NaN, screenY: 720 })).toBeNull();
expect(readFiniteScreenPoint({ screenX: 1280, screenY: Number.NaN })).toBeNull();
});
it("rejects infinite screen coordinates", () => {
expect(readFiniteScreenPoint({ screenX: Number.POSITIVE_INFINITY, screenY: 720 })).toBeNull();
expect(readFiniteScreenPoint({ screenX: 1280, screenY: Number.NEGATIVE_INFINITY })).toBeNull();
});
it("rejects missing screen coordinates", () => {
expect(readFiniteScreenPoint(undefined)).toBeNull();
expect(readFiniteScreenPoint({ screenX: 1280 })).toBeNull();
expect(readFiniteScreenPoint({ screenY: 720 })).toBeNull();
});
});
@@ -1,27 +0,0 @@
export type DesktopWindowScreenPoint = {
screenX: number;
screenY: number;
};
type ScreenPointInput =
| {
screenX?: unknown;
screenY?: unknown;
}
| null
| undefined;
function isFiniteCoordinate(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
export function readFiniteScreenPoint(input: ScreenPointInput): DesktopWindowScreenPoint | null {
if (!isFiniteCoordinate(input?.screenX) || !isFiniteCoordinate(input?.screenY)) {
return null;
}
return {
screenX: input.screenX,
screenY: input.screenY,
};
}
@@ -1,40 +0,0 @@
import { describe, expect, it } from "vitest";
import { isInteractiveDesktopDragTarget } from "./desktop-window";
function createTarget(input: { matchesSelector: (selector: string) => boolean }): EventTarget {
return {
closest: (selector: string) => (input.matchesSelector(selector) ? ({} as Element) : null),
} as unknown as EventTarget;
}
describe("isInteractiveDesktopDragTarget", () => {
it("treats focusable pressables as interactive drag exemptions", () => {
const target = createTarget({
matchesSelector: (selector) => selector.includes("[tabindex]"),
});
expect(isInteractiveDesktopDragTarget(target)).toBe(true);
});
it("treats semantic button targets as interactive drag exemptions", () => {
const target = createTarget({
matchesSelector: (selector) => selector.includes("[role='button']"),
});
expect(isInteractiveDesktopDragTarget(target)).toBe(true);
});
it("returns false for non-interactive targets", () => {
const target = createTarget({
matchesSelector: () => false,
});
expect(isInteractiveDesktopDragTarget(target)).toBe(false);
});
it("returns false when the target does not support closest()", () => {
expect(isInteractiveDesktopDragTarget(null)).toBe(false);
expect(isInteractiveDesktopDragTarget({} as EventTarget)).toBe(false);
});
});
+2 -115
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState } from "react";
import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native";
import { useEffect, useMemo, useState } from "react";
import { Platform } from "react-native";
import {
getIsElectronRuntimeMac,
getIsElectronRuntime,
@@ -9,120 +9,7 @@ import {
DESKTOP_WINDOW_CONTROLS_HEIGHT,
} from "@/constants/layout";
import { getDesktopWindow } from "@/desktop/electron/window";
import { isElectronRuntime } from "@/desktop/host";
import { usePanelStore } from "@/stores/panel-store";
import { readFiniteScreenPoint } from "./desktop-window-drag-coordinates";
export async function toggleMaximize() {
const win = getDesktopWindow();
if (win && typeof win.toggleMaximize === "function") {
try {
await win.toggleMaximize();
} catch (error) {
console.warn("[DesktopWindow] toggleMaximize failed", error);
}
}
}
// ---------------------------------------------------------------------------
// Manual window dragging via pointer events.
// Mirrors the Tauri implementation: single pointerdown handler with
// double-click-to-maximize via timing, closest() for interactive check,
// and pointer capture for move tracking.
// ---------------------------------------------------------------------------
const INTERACTIVE_SELECTOR =
"button, a, input, textarea, select, " +
"[role='button'], [role='link'], [role='textbox'], [role='combobox'], " +
"[role='tab'], [role='switch'], [role='checkbox'], [role='slider'], " +
"[role='menuitem'], [tabindex], [contenteditable='true']";
const DOUBLE_CLICK_MS = 300;
type DesktopDragViewProps = Pick<
ViewProps,
"onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel"
>;
export function isInteractiveDesktopDragTarget(target: unknown): boolean {
const candidate = target as unknown as { closest?: (selector: string) => Element | null } | null;
if (!candidate || typeof candidate.closest !== "function") {
return false;
}
return Boolean(candidate.closest(INTERACTIVE_SELECTOR));
}
export function useDesktopDragHandlers(): DesktopDragViewProps {
const isDragging = useRef(false);
const lastPointerDownAt = useRef(0);
const isActive = Platform.OS === "web" && isElectronRuntime();
useEffect(() => {
if (!isActive) return;
function handleBlur() {
if (!isDragging.current) return;
isDragging.current = false;
getDesktopWindow()?.endMove?.();
}
window.addEventListener("blur", handleBlur);
return () => window.removeEventListener("blur", handleBlur);
}, [isActive]);
return useMemo((): DesktopDragViewProps => {
if (!isActive) return {};
function stopDrag(e: RNPointerEvent) {
if (!isDragging.current) return;
isDragging.current = false;
// On web, currentTarget is a DOM Element (typed as HostInstance in RN)
const el = e.currentTarget as unknown as Element | null;
if (el && "releasePointerCapture" in el) {
el.releasePointerCapture(e.nativeEvent.pointerId);
}
getDesktopWindow()?.endMove?.();
}
return {
onPointerDown: (e: RNPointerEvent) => {
if (e.nativeEvent.button !== 0) return;
if (isInteractiveDesktopDragTarget(e.target)) return;
e.preventDefault();
const now = Date.now();
if (now - lastPointerDownAt.current < DOUBLE_CLICK_MS) {
lastPointerDownAt.current = 0;
void toggleMaximize();
return;
}
lastPointerDownAt.current = now;
const win = getDesktopWindow();
if (!win?.startMove) return;
const screenPoint = readFiniteScreenPoint(e.nativeEvent);
if (!screenPoint) return;
isDragging.current = true;
const el = e.currentTarget as unknown as Element;
el.setPointerCapture(e.nativeEvent.pointerId);
win.startMove(screenPoint.screenX, screenPoint.screenY);
},
onPointerMove: (e: RNPointerEvent) => {
if (!isDragging.current) return;
const screenPoint = readFiniteScreenPoint(e.nativeEvent);
if (!screenPoint) {
stopDrag(e);
return;
}
getDesktopWindow()?.moving?.(screenPoint.screenX, screenPoint.screenY);
},
onPointerUp: stopDrag,
onPointerCancel: stopDrag,
};
}, [isActive]);
}
type RawWindowControlsPadding = {
left: number;