mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
Remove the Windows renderer-side drag delta fallback and keep frameless window movement on Electron app-region handling. Reject drag movement payloads on the legacy IPC channel so window drags cannot mutate bounds through repeated setPosition calls. Tested: cd desktop && bun run test --run src/hooks/useElectronWindowDragRegions.test.tsx electron/ipc/capabilities.test.ts src/lib/desktopHost/electronHost.test.ts Tested: bun run check:desktop Tested: bun run check:native Confidence: high Scope-risk: narrow
This commit is contained in:
parent
978dfb2efb
commit
c97bd55a57
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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(<Harness />)
|
||||
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(<Harness />)
|
||||
|
||||
fireEvent.mouseDown(screen.getByRole('button', { name: 'Button' }), {
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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<void>
|
||||
toggleMaximize(): Promise<void>
|
||||
close(): Promise<void>
|
||||
startDragging(input?: WindowDragMoveInput): Promise<void>
|
||||
startDragging(): Promise<void>
|
||||
requestAttention(): Promise<void>
|
||||
focus(): Promise<void>
|
||||
isMaximized(): Promise<boolean>
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user