fix(windows): add manual window resize for SelectionAction window (#11766)

Implement custom window resize functionality for the SelectionAction window
on Windows only. This is a workaround for an Electron bug where native
window resize doesn't work with frame: false + transparent: true.

- Add IPC channel and API for window resize
- Implement resize handler in SelectionService
- Add 8 resize handles (4 edges + 4 corners) in SelectionActionApp
- Only enable on Windows, other platforms use native resize

Bug reference: https://github.com/electron/electron/issues/42738
All workaround code is documented and can be removed once the bug is fixed.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
fullex 2025-12-09 09:03:35 +08:00 committed by GitHub
parent f8c33db450
commit bc00c11a00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 204 additions and 3 deletions

View File

@ -293,6 +293,8 @@ export enum IpcChannel {
Selection_ActionWindowClose = 'selection:action-window-close',
Selection_ActionWindowMinimize = 'selection:action-window-minimize',
Selection_ActionWindowPin = 'selection:action-window-pin',
// [Windows only] Electron bug workaround - can be removed once https://github.com/electron/electron/issues/48554 is fixed
Selection_ActionWindowResize = 'selection:action-window-resize',
Selection_ProcessAction = 'selection:process-action',
Selection_UpdateActionData = 'selection:update-action-data',

View File

@ -1393,6 +1393,50 @@ export class SelectionService {
actionWindow.setAlwaysOnTop(isPinned)
}
/**
* [Windows only] Manual window resize handler
*
* ELECTRON BUG WORKAROUND:
* In Electron, when using `frame: false` + `transparent: true`, the native window
* resize functionality is broken on Windows. This is a known Electron bug.
* See: https://github.com/electron/electron/issues/48554
*
* This method can be removed once the Electron bug is fixed.
*/
public resizeActionWindow(actionWindow: BrowserWindow, deltaX: number, deltaY: number, direction: string): void {
const bounds = actionWindow.getBounds()
const minWidth = 300
const minHeight = 200
let { x, y, width, height } = bounds
// Handle horizontal resize
if (direction.includes('e')) {
width = Math.max(minWidth, width + deltaX)
}
if (direction.includes('w')) {
const newWidth = Math.max(minWidth, width - deltaX)
if (newWidth !== width) {
x = x + (width - newWidth)
width = newWidth
}
}
// Handle vertical resize
if (direction.includes('s')) {
height = Math.max(minHeight, height + deltaY)
}
if (direction.includes('n')) {
const newHeight = Math.max(minHeight, height - deltaY)
if (newHeight !== height) {
y = y + (height - newHeight)
height = newHeight
}
}
actionWindow.setBounds({ x, y, width, height })
}
/**
* Update trigger mode behavior
* Switches between selection-based and alt-key based triggering
@ -1510,6 +1554,18 @@ export class SelectionService {
}
})
// [Windows only] Electron bug workaround - can be removed once fixed
// See: https://github.com/electron/electron/issues/48554
ipcMain.handle(
IpcChannel.Selection_ActionWindowResize,
(event, deltaX: number, deltaY: number, direction: string) => {
const actionWindow = BrowserWindow.fromWebContents(event.sender)
if (actionWindow) {
selectionService?.resizeActionWindow(actionWindow, deltaX, deltaY, direction)
}
}
)
this.isIpcHandlerRegistered = true
}

View File

@ -456,7 +456,10 @@ const api = {
ipcRenderer.invoke(IpcChannel.Selection_ProcessAction, actionItem, isFullScreen),
closeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowClose),
minimizeActionWindow: () => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowMinimize),
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned)
pinActionWindow: (isPinned: boolean) => ipcRenderer.invoke(IpcChannel.Selection_ActionWindowPin, isPinned),
// [Windows only] Electron bug workaround - can be removed once https://github.com/electron/electron/issues/48554 is fixed
resizeActionWindow: (deltaX: number, deltaY: number, direction: string) =>
ipcRenderer.invoke(IpcChannel.Selection_ActionWindowResize, deltaX, deltaY, direction)
},
agentTools: {
respondToPermission: (payload: {

View File

@ -1,4 +1,4 @@
import { isMac } from '@renderer/config/constant'
import { isMac, isWin } from '@renderer/config/constant'
import { useSelectionAssistant } from '@renderer/hooks/useSelectionAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
@ -8,11 +8,14 @@ import { IpcChannel } from '@shared/IpcChannel'
import { Button, Slider, Tooltip } from 'antd'
import { Droplet, Minus, Pin, X } from 'lucide-react'
import { DynamicIcon } from 'lucide-react/dynamic'
import type { FC } from 'react'
import type { FC, MouseEvent as ReactMouseEvent } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
// [Windows only] Electron bug workaround type - can be removed once https://github.com/electron/electron/issues/48554 is fixed
type ResizeDirection = 'n' | 's' | 'e' | 'w' | 'ne' | 'nw' | 'se' | 'sw'
import ActionGeneral from './components/ActionGeneral'
import ActionTranslate from './components/ActionTranslate'
@ -185,11 +188,62 @@ const SelectionActionApp: FC = () => {
}
}
/**
* [Windows only] Manual window resize handler
*
* ELECTRON BUG WORKAROUND:
* In Electron, when using `frame: false` + `transparent: true`, the native window
* resize functionality is broken on Windows. This is a known Electron bug.
* See: https://github.com/electron/electron/issues/48554
*
* This custom resize implementation can be removed once the Electron bug is fixed.
*/
const handleResizeStart = useCallback((e: ReactMouseEvent, direction: ResizeDirection) => {
e.preventDefault()
e.stopPropagation()
let lastX = e.screenX
let lastY = e.screenY
const handleMouseMove = (moveEvent: MouseEvent) => {
const deltaX = moveEvent.screenX - lastX
const deltaY = moveEvent.screenY - lastY
if (deltaX !== 0 || deltaY !== 0) {
window.api.selection.resizeActionWindow(deltaX, deltaY, direction)
lastX = moveEvent.screenX
lastY = moveEvent.screenY
}
}
const handleMouseUp = () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
}, [])
//we don't need to render the component if action is not set
if (!action) return null
return (
<WindowFrame $opacity={opacity / 100}>
{/* [Windows only] Custom resize handles - Electron bug workaround, can be removed once fixed */}
{isWin && (
<>
<ResizeHandle $direction="n" onMouseDown={(e) => handleResizeStart(e, 'n')} />
<ResizeHandle $direction="s" onMouseDown={(e) => handleResizeStart(e, 's')} />
<ResizeHandle $direction="e" onMouseDown={(e) => handleResizeStart(e, 'e')} />
<ResizeHandle $direction="w" onMouseDown={(e) => handleResizeStart(e, 'w')} />
<ResizeHandle $direction="ne" onMouseDown={(e) => handleResizeStart(e, 'ne')} />
<ResizeHandle $direction="nw" onMouseDown={(e) => handleResizeStart(e, 'nw')} />
<ResizeHandle $direction="se" onMouseDown={(e) => handleResizeStart(e, 'se')} />
<ResizeHandle $direction="sw" onMouseDown={(e) => handleResizeStart(e, 'sw')} />
</>
)}
<TitleBar $isWindowFocus={isWindowFocus} style={isMac ? { paddingLeft: '70px' } : {}}>
{action.icon && (
<TitleBarIcon>
@ -431,4 +485,90 @@ const OpacitySlider = styled.div`
}
`
/**
* [Windows only] Custom resize handle styled component
*
* ELECTRON BUG WORKAROUND:
* This component can be removed once https://github.com/electron/electron/issues/48554 is fixed.
*/
const ResizeHandle = styled.div<{ $direction: ResizeDirection }>`
position: absolute;
-webkit-app-region: no-drag;
z-index: 10;
${({ $direction }) => {
const edgeSize = '6px'
const cornerSize = '12px'
switch ($direction) {
case 'n':
return `
top: 0;
left: ${cornerSize};
right: ${cornerSize};
height: ${edgeSize};
cursor: ns-resize;
`
case 's':
return `
bottom: 0;
left: ${cornerSize};
right: ${cornerSize};
height: ${edgeSize};
cursor: ns-resize;
`
case 'e':
return `
right: 0;
top: ${cornerSize};
bottom: ${cornerSize};
width: ${edgeSize};
cursor: ew-resize;
`
case 'w':
return `
left: 0;
top: ${cornerSize};
bottom: ${cornerSize};
width: ${edgeSize};
cursor: ew-resize;
`
case 'ne':
return `
top: 0;
right: 0;
width: ${cornerSize};
height: ${cornerSize};
cursor: nesw-resize;
`
case 'nw':
return `
top: 0;
left: 0;
width: ${cornerSize};
height: ${cornerSize};
cursor: nwse-resize;
`
case 'se':
return `
bottom: 0;
right: 0;
width: ${cornerSize};
height: ${cornerSize};
cursor: nwse-resize;
`
case 'sw':
return `
bottom: 0;
left: 0;
width: ${cornerSize};
height: ${cornerSize};
cursor: nesw-resize;
`
default:
return ''
}
}}
`
export default SelectionActionApp