diff --git a/desktop/electron/ipc/capabilities.test.ts b/desktop/electron/ipc/capabilities.test.ts index 1ceff77d..eb94899a 100644 --- a/desktop/electron/ipc/capabilities.test.ts +++ b/desktop/electron/ipc/capabilities.test.ts @@ -26,9 +26,7 @@ describe('Electron IPC capabilities', () => { expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, undefined)).toBe(true) expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, {})).toBe(false) expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, undefined)).toBe(true) - expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: -2 })).toBe(true) - expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: Number.NaN })).toBe(false) - expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: -2, extra: true })).toBe(false) + expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, { deltaX: 4, deltaY: -2 })).toBe(false) expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalWrite, { sessionId: 1, data: 'pwd\n' })).toBe(true) expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalWrite, { sessionId: '1', data: 'pwd\n' })).toBe(false) expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalSpawn, { cols: 80, rows: 24, cwd: '/tmp' })).toBe(true) diff --git a/desktop/electron/ipc/capabilities.ts b/desktop/electron/ipc/capabilities.ts index 55783673..6f65566b 100644 --- a/desktop/electron/ipc/capabilities.ts +++ b/desktop/electron/ipc/capabilities.ts @@ -56,17 +56,6 @@ const boundsPayload: Validator = value => && typeof value.width === 'number' && typeof value.height === 'number' -const windowDragMovePayload: Validator = value => - value === undefined - || ( - isRecord(value) - && hasOnlyKeys(value, ['deltaX', 'deltaY']) - && typeof value.deltaX === 'number' - && Number.isFinite(value.deltaX) - && typeof value.deltaY === 'number' - && Number.isFinite(value.deltaY) - ) - const urlWithOptionalBounds: Validator = value => isRecord(value) && typeof value.url === 'string' @@ -102,7 +91,7 @@ export const ELECTRON_IPC_VALIDATORS = { [ELECTRON_IPC_CHANNELS.windowMinimize]: noPayload, [ELECTRON_IPC_CHANNELS.windowToggleMaximize]: noPayload, [ELECTRON_IPC_CHANNELS.windowClose]: noPayload, - [ELECTRON_IPC_CHANNELS.windowStartDragging]: windowDragMovePayload, + [ELECTRON_IPC_CHANNELS.windowStartDragging]: noPayload, [ELECTRON_IPC_CHANNELS.windowRequestAttention]: noPayload, [ELECTRON_IPC_CHANNELS.windowFocus]: noPayload, [ELECTRON_IPC_CHANNELS.windowIsMaximized]: noPayload, diff --git a/desktop/electron/main.ts b/desktop/electron/main.ts index aa072fa8..ecf22a0a 100644 --- a/desktop/electron/main.ts +++ b/desktop/electron/main.ts @@ -297,19 +297,7 @@ function registerIpcHandlers() { else window.maximize() }) registerHandler(ELECTRON_IPC_CHANNELS.windowClose, event => currentWindow(event).close()) - registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, (event, payload) => { - if (!payload || typeof payload !== 'object') return undefined - const { deltaX, deltaY } = payload as { deltaX?: number, deltaY?: number } - if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return undefined - const window = currentWindow(event) - if (window.isMaximized()) window.unmaximize() - const bounds = window.getBounds() - window.setPosition( - Math.round(bounds.x + deltaX!), - Math.round(bounds.y + deltaY!), - ) - return undefined - }) + registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, () => undefined) registerHandler(ELECTRON_IPC_CHANNELS.windowRequestAttention, event => currentWindow(event).flashFrame(true)) registerHandler(ELECTRON_IPC_CHANNELS.windowFocus, event => currentWindow(event).focus()) registerHandler(ELECTRON_IPC_CHANNELS.windowIsMaximized, event => currentWindow(event).isMaximized()) diff --git a/desktop/src/hooks/useElectronWindowDragRegions.test.tsx b/desktop/src/hooks/useElectronWindowDragRegions.test.tsx index d4a98ce7..09901205 100644 --- a/desktop/src/hooks/useElectronWindowDragRegions.test.tsx +++ b/desktop/src/hooks/useElectronWindowDragRegions.test.tsx @@ -56,15 +56,15 @@ describe('useElectronWindowDragRegions', () => { delete document.documentElement.dataset.desktopDragMode }) - it('uses the manual drag fallback on Windows only', () => { - expect(shouldUseManualWindowDrag('Win32')).toBe(true) + it('uses native app-region dragging on every platform', () => { + expect(shouldUseManualWindowDrag('Win32')).toBe(false) expect(shouldUseManualWindowDrag('MacIntel')).toBe(false) expect(shouldUseManualWindowDrag('Linux x86_64')).toBe(false) }) - it('moves the native window while dragging a desktop drag region', async () => { + it('leaves Windows drag regions on the native app-region path', async () => { render() - expect(document.documentElement.dataset.desktopDragMode).toBe('manual') + expect(document.documentElement.dataset.desktopDragMode).toBeUndefined() fireEvent.mouseDown(screen.getByTestId('blank-space'), { button: 0, @@ -77,10 +77,7 @@ describe('useElectronWindowDragRegions', () => { }) await waitFor(() => { - expect(desktopHostMock.host.window.startDragging).toHaveBeenCalledWith({ - deltaX: 30, - deltaY: 30, - }) + expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled() }) fireEvent.mouseUp(window) @@ -89,10 +86,10 @@ describe('useElectronWindowDragRegions', () => { screenY: 210, }) - expect(desktopHostMock.host.window.startDragging).toHaveBeenCalledTimes(1) + expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled() }) - it('keeps buttons and tab reorder targets out of the window drag fallback', () => { + it('does not route buttons or tab reorder targets through legacy drag IPC', () => { render() fireEvent.mouseDown(screen.getByRole('button', { name: 'Button' }), { diff --git a/desktop/src/hooks/useElectronWindowDragRegions.ts b/desktop/src/hooks/useElectronWindowDragRegions.ts index c125fd50..efaceb32 100644 --- a/desktop/src/hooks/useElectronWindowDragRegions.ts +++ b/desktop/src/hooks/useElectronWindowDragRegions.ts @@ -1,86 +1,9 @@ -import { useEffect } from 'react' -import { getDesktopHost } from '../lib/desktopHost' - -const DRAG_REGION_SELECTOR = '[data-desktop-drag-region]' -const MANUAL_DRAG_MODE = 'manual' -const NO_DRAG_SELECTOR = [ - '[data-desktop-no-drag-region]', - 'button', - 'input', - 'textarea', - 'select', - 'a', - '[role="button"]', - '[draggable="true"]', - '.tab-bar-interactive', - '.tab-bar-interactive *', -].join(',') - export function shouldUseManualWindowDrag(platform = typeof navigator === 'undefined' ? '' : navigator.platform) { - return /Win/i.test(platform) -} - -export function isDesktopDragStartTarget(target: EventTarget | null): target is Element { - if (!(target instanceof Element)) return false - if (target.closest(NO_DRAG_SELECTOR)) return false - return Boolean(target.closest(DRAG_REGION_SELECTOR)) + void platform + return false } export function useElectronWindowDragRegions() { - useEffect(() => { - const host = getDesktopHost() - if (!host.isDesktop || !host.capabilities?.windowControls || !host.window?.startDragging) return - if (!shouldUseManualWindowDrag()) return - - const previousDragMode = document.documentElement.dataset.desktopDragMode - document.documentElement.dataset.desktopDragMode = MANUAL_DRAG_MODE - - let dragging = false - let lastScreenX = 0 - let lastScreenY = 0 - - const stopDragging = () => { - dragging = false - } - - const handleMouseDown = (event: MouseEvent) => { - if (event.button !== 0) return - if (!isDesktopDragStartTarget(event.target)) return - dragging = true - lastScreenX = event.screenX - lastScreenY = event.screenY - event.preventDefault() - } - - const handleMouseMove = (event: MouseEvent) => { - if (!dragging) return - const deltaX = event.screenX - lastScreenX - const deltaY = event.screenY - lastScreenY - if (deltaX === 0 && deltaY === 0) return - - lastScreenX = event.screenX - lastScreenY = event.screenY - void host.window.startDragging({ deltaX, deltaY }).catch(error => { - console.error('Window drag fallback failed', error) - stopDragging() - }) - } - - document.addEventListener('mousedown', handleMouseDown, true) - window.addEventListener('mousemove', handleMouseMove, true) - window.addEventListener('mouseup', stopDragging, true) - window.addEventListener('blur', stopDragging) - - return () => { - if (previousDragMode === undefined) { - delete document.documentElement.dataset.desktopDragMode - } else { - document.documentElement.dataset.desktopDragMode = previousDragMode - } - document.removeEventListener('mousedown', handleMouseDown, true) - window.removeEventListener('mousemove', handleMouseMove, true) - window.removeEventListener('mouseup', stopDragging, true) - window.removeEventListener('blur', stopDragging) - } - }, []) + // Electron's native app-region dragging is the supported frameless-window path. + // The previous Windows JS delta fallback could resize frameless windows while moving them. } diff --git a/desktop/src/lib/desktopHost/electronHost.test.ts b/desktop/src/lib/desktopHost/electronHost.test.ts index ab2bbf65..bd4884ca 100644 --- a/desktop/src/lib/desktopHost/electronHost.test.ts +++ b/desktop/src/lib/desktopHost/electronHost.test.ts @@ -45,19 +45,16 @@ describe('electron desktop host', () => { expect(host.capabilities.windowControls).toBe(true) }) - it('forwards drag-region fallback movement through the window dragging IPC channel', async () => { + it('keeps the legacy window dragging IPC channel payload-free', async () => { const invoke = vi.fn().mockResolvedValue(undefined) const host = createElectronHost({ invoke, subscribe: vi.fn(), }) - await host.window.startDragging({ deltaX: 12, deltaY: -8 }) + await host.window.startDragging() - expect(invoke).toHaveBeenCalledWith(ELECTRON_IPC_CHANNELS.windowStartDragging, { - deltaX: 12, - deltaY: -8, - }) + expect(invoke).toHaveBeenCalledWith(ELECTRON_IPC_CHANNELS.windowStartDragging, undefined) }) it('opens dedicated trace windows through a narrow IPC channel', async () => { diff --git a/desktop/src/lib/desktopHost/electronHost.ts b/desktop/src/lib/desktopHost/electronHost.ts index 3db16684..bb04587a 100644 --- a/desktop/src/lib/desktopHost/electronHost.ts +++ b/desktop/src/lib/desktopHost/electronHost.ts @@ -118,7 +118,7 @@ export function createElectronHost(bridge: ElectronHostBridge): DesktopHost { minimize: () => invoke(ELECTRON_IPC_CHANNELS.windowMinimize), toggleMaximize: () => invoke(ELECTRON_IPC_CHANNELS.windowToggleMaximize), close: () => invoke(ELECTRON_IPC_CHANNELS.windowClose), - startDragging: input => invoke(ELECTRON_IPC_CHANNELS.windowStartDragging, input), + startDragging: () => invoke(ELECTRON_IPC_CHANNELS.windowStartDragging), requestAttention: () => invoke(ELECTRON_IPC_CHANNELS.windowRequestAttention), focus: () => invoke(ELECTRON_IPC_CHANNELS.windowFocus), isMaximized: () => invoke(ELECTRON_IPC_CHANNELS.windowIsMaximized), diff --git a/desktop/src/lib/desktopHost/types.ts b/desktop/src/lib/desktopHost/types.ts index 2a6500c7..0fb99a9f 100644 --- a/desktop/src/lib/desktopHost/types.ts +++ b/desktop/src/lib/desktopHost/types.ts @@ -130,11 +130,6 @@ export type PreviewHostMessage = PreviewCaptureMessage | PreviewPickerMessage export type AppModeConfig = SettingsAppModeConfig -export type WindowDragMoveInput = { - deltaX: number - deltaY: number -} - export type AppModeSetInput = { mode: SettingsAppMode portableDir: string | null @@ -192,7 +187,7 @@ export type DesktopHost = { minimize(): Promise toggleMaximize(): Promise close(): Promise - startDragging(input?: WindowDragMoveInput): Promise + startDragging(): Promise requestAttention(): Promise focus(): Promise isMaximized(): Promise diff --git a/desktop/src/theme/globals.css b/desktop/src/theme/globals.css index 853f0f69..53fc18ff 100644 --- a/desktop/src/theme/globals.css +++ b/desktop/src/theme/globals.css @@ -959,14 +959,10 @@ html, body, #root { } /* Desktop drag region */ -html:not([data-desktop-drag-mode="manual"]) [data-desktop-drag-region] { +[data-desktop-drag-region] { app-region: drag; -webkit-app-region: drag; } -html[data-desktop-drag-mode="manual"] [data-desktop-drag-region] { - app-region: no-drag; - -webkit-app-region: no-drag; -} button, input, textarea, select, a, [role="button"] { app-region: no-drag; -webkit-app-region: no-drag;