diff --git a/packages/shared/IpcChannel.ts b/packages/shared/IpcChannel.ts index 167721a7f0..1c61745a60 100644 --- a/packages/shared/IpcChannel.ts +++ b/packages/shared/IpcChannel.ts @@ -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', diff --git a/src/main/services/SelectionService.ts b/src/main/services/SelectionService.ts index a096dfcfd7..695026003b 100644 --- a/src/main/services/SelectionService.ts +++ b/src/main/services/SelectionService.ts @@ -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 } diff --git a/src/preload/index.ts b/src/preload/index.ts index 25b1064d49..a357f59f00 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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: { diff --git a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx index ab8e94a723..3ecda63a24 100644 --- a/src/renderer/src/windows/selection/action/SelectionActionApp.tsx +++ b/src/renderer/src/windows/selection/action/SelectionActionApp.tsx @@ -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 ( + {/* [Windows only] Custom resize handles - Electron bug workaround, can be removed once fixed */} + {isWin && ( + <> + handleResizeStart(e, 'n')} /> + handleResizeStart(e, 's')} /> + handleResizeStart(e, 'e')} /> + handleResizeStart(e, 'w')} /> + handleResizeStart(e, 'ne')} /> + handleResizeStart(e, 'nw')} /> + handleResizeStart(e, 'se')} /> + handleResizeStart(e, 'sw')} /> + + )} + {action.icon && ( @@ -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