mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-20 13:53:32 +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, undefined)).toBe(true)
|
||||||
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, {})).toBe(false)
|
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowClose, {})).toBe(false)
|
||||||
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.windowStartDragging, undefined)).toBe(true)
|
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: -2 })).toBe(false)
|
||||||
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.terminalWrite, { sessionId: 1, data: 'pwd\n' })).toBe(true)
|
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.terminalWrite, { sessionId: '1', data: 'pwd\n' })).toBe(false)
|
||||||
expect(validateElectronIpcPayload(ELECTRON_IPC_CHANNELS.terminalSpawn, { cols: 80, rows: 24, cwd: '/tmp' })).toBe(true)
|
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.width === 'number'
|
||||||
&& typeof value.height === '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 =>
|
const urlWithOptionalBounds: Validator = value =>
|
||||||
isRecord(value)
|
isRecord(value)
|
||||||
&& typeof value.url === 'string'
|
&& typeof value.url === 'string'
|
||||||
@ -102,7 +91,7 @@ export const ELECTRON_IPC_VALIDATORS = {
|
|||||||
[ELECTRON_IPC_CHANNELS.windowMinimize]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowMinimize]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowToggleMaximize]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowToggleMaximize]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowClose]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowClose]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowStartDragging]: windowDragMovePayload,
|
[ELECTRON_IPC_CHANNELS.windowStartDragging]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowRequestAttention]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowRequestAttention]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowFocus]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowFocus]: noPayload,
|
||||||
[ELECTRON_IPC_CHANNELS.windowIsMaximized]: noPayload,
|
[ELECTRON_IPC_CHANNELS.windowIsMaximized]: noPayload,
|
||||||
|
|||||||
@ -297,19 +297,7 @@ function registerIpcHandlers() {
|
|||||||
else window.maximize()
|
else window.maximize()
|
||||||
})
|
})
|
||||||
registerHandler(ELECTRON_IPC_CHANNELS.windowClose, event => currentWindow(event).close())
|
registerHandler(ELECTRON_IPC_CHANNELS.windowClose, event => currentWindow(event).close())
|
||||||
registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, (event, payload) => {
|
registerHandler(ELECTRON_IPC_CHANNELS.windowStartDragging, () => undefined)
|
||||||
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.windowRequestAttention, event => currentWindow(event).flashFrame(true))
|
registerHandler(ELECTRON_IPC_CHANNELS.windowRequestAttention, event => currentWindow(event).flashFrame(true))
|
||||||
registerHandler(ELECTRON_IPC_CHANNELS.windowFocus, event => currentWindow(event).focus())
|
registerHandler(ELECTRON_IPC_CHANNELS.windowFocus, event => currentWindow(event).focus())
|
||||||
registerHandler(ELECTRON_IPC_CHANNELS.windowIsMaximized, event => currentWindow(event).isMaximized())
|
registerHandler(ELECTRON_IPC_CHANNELS.windowIsMaximized, event => currentWindow(event).isMaximized())
|
||||||
|
|||||||
@ -56,15 +56,15 @@ describe('useElectronWindowDragRegions', () => {
|
|||||||
delete document.documentElement.dataset.desktopDragMode
|
delete document.documentElement.dataset.desktopDragMode
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses the manual drag fallback on Windows only', () => {
|
it('uses native app-region dragging on every platform', () => {
|
||||||
expect(shouldUseManualWindowDrag('Win32')).toBe(true)
|
expect(shouldUseManualWindowDrag('Win32')).toBe(false)
|
||||||
expect(shouldUseManualWindowDrag('MacIntel')).toBe(false)
|
expect(shouldUseManualWindowDrag('MacIntel')).toBe(false)
|
||||||
expect(shouldUseManualWindowDrag('Linux x86_64')).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 />)
|
render(<Harness />)
|
||||||
expect(document.documentElement.dataset.desktopDragMode).toBe('manual')
|
expect(document.documentElement.dataset.desktopDragMode).toBeUndefined()
|
||||||
|
|
||||||
fireEvent.mouseDown(screen.getByTestId('blank-space'), {
|
fireEvent.mouseDown(screen.getByTestId('blank-space'), {
|
||||||
button: 0,
|
button: 0,
|
||||||
@ -77,10 +77,7 @@ describe('useElectronWindowDragRegions', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(desktopHostMock.host.window.startDragging).toHaveBeenCalledWith({
|
expect(desktopHostMock.host.window.startDragging).not.toHaveBeenCalled()
|
||||||
deltaX: 30,
|
|
||||||
deltaY: 30,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
fireEvent.mouseUp(window)
|
fireEvent.mouseUp(window)
|
||||||
@ -89,10 +86,10 @@ describe('useElectronWindowDragRegions', () => {
|
|||||||
screenY: 210,
|
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 />)
|
render(<Harness />)
|
||||||
|
|
||||||
fireEvent.mouseDown(screen.getByRole('button', { name: 'Button' }), {
|
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) {
|
export function shouldUseManualWindowDrag(platform = typeof navigator === 'undefined' ? '' : navigator.platform) {
|
||||||
return /Win/i.test(platform)
|
void platform
|
||||||
}
|
return false
|
||||||
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useElectronWindowDragRegions() {
|
export function useElectronWindowDragRegions() {
|
||||||
useEffect(() => {
|
// Electron's native app-region dragging is the supported frameless-window path.
|
||||||
const host = getDesktopHost()
|
// The previous Windows JS delta fallback could resize frameless windows while moving them.
|
||||||
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)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,19 +45,16 @@ describe('electron desktop host', () => {
|
|||||||
expect(host.capabilities.windowControls).toBe(true)
|
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 invoke = vi.fn().mockResolvedValue(undefined)
|
||||||
const host = createElectronHost({
|
const host = createElectronHost({
|
||||||
invoke,
|
invoke,
|
||||||
subscribe: vi.fn(),
|
subscribe: vi.fn(),
|
||||||
})
|
})
|
||||||
|
|
||||||
await host.window.startDragging({ deltaX: 12, deltaY: -8 })
|
await host.window.startDragging()
|
||||||
|
|
||||||
expect(invoke).toHaveBeenCalledWith(ELECTRON_IPC_CHANNELS.windowStartDragging, {
|
expect(invoke).toHaveBeenCalledWith(ELECTRON_IPC_CHANNELS.windowStartDragging, undefined)
|
||||||
deltaX: 12,
|
|
||||||
deltaY: -8,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens dedicated trace windows through a narrow IPC channel', async () => {
|
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),
|
minimize: () => invoke(ELECTRON_IPC_CHANNELS.windowMinimize),
|
||||||
toggleMaximize: () => invoke(ELECTRON_IPC_CHANNELS.windowToggleMaximize),
|
toggleMaximize: () => invoke(ELECTRON_IPC_CHANNELS.windowToggleMaximize),
|
||||||
close: () => invoke(ELECTRON_IPC_CHANNELS.windowClose),
|
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),
|
requestAttention: () => invoke(ELECTRON_IPC_CHANNELS.windowRequestAttention),
|
||||||
focus: () => invoke(ELECTRON_IPC_CHANNELS.windowFocus),
|
focus: () => invoke(ELECTRON_IPC_CHANNELS.windowFocus),
|
||||||
isMaximized: () => invoke(ELECTRON_IPC_CHANNELS.windowIsMaximized),
|
isMaximized: () => invoke(ELECTRON_IPC_CHANNELS.windowIsMaximized),
|
||||||
|
|||||||
@ -130,11 +130,6 @@ export type PreviewHostMessage = PreviewCaptureMessage | PreviewPickerMessage
|
|||||||
|
|
||||||
export type AppModeConfig = SettingsAppModeConfig
|
export type AppModeConfig = SettingsAppModeConfig
|
||||||
|
|
||||||
export type WindowDragMoveInput = {
|
|
||||||
deltaX: number
|
|
||||||
deltaY: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export type AppModeSetInput = {
|
export type AppModeSetInput = {
|
||||||
mode: SettingsAppMode
|
mode: SettingsAppMode
|
||||||
portableDir: string | null
|
portableDir: string | null
|
||||||
@ -192,7 +187,7 @@ export type DesktopHost = {
|
|||||||
minimize(): Promise<void>
|
minimize(): Promise<void>
|
||||||
toggleMaximize(): Promise<void>
|
toggleMaximize(): Promise<void>
|
||||||
close(): Promise<void>
|
close(): Promise<void>
|
||||||
startDragging(input?: WindowDragMoveInput): Promise<void>
|
startDragging(): Promise<void>
|
||||||
requestAttention(): Promise<void>
|
requestAttention(): Promise<void>
|
||||||
focus(): Promise<void>
|
focus(): Promise<void>
|
||||||
isMaximized(): Promise<boolean>
|
isMaximized(): Promise<boolean>
|
||||||
|
|||||||
@ -959,14 +959,10 @@ html, body, #root {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Desktop drag region */
|
/* Desktop drag region */
|
||||||
html:not([data-desktop-drag-mode="manual"]) [data-desktop-drag-region] {
|
[data-desktop-drag-region] {
|
||||||
app-region: drag;
|
app-region: drag;
|
||||||
-webkit-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"] {
|
button, input, textarea, select, a, [role="button"] {
|
||||||
app-region: no-drag;
|
app-region: no-drag;
|
||||||
-webkit-app-region: no-drag;
|
-webkit-app-region: no-drag;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user