Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ export const FLOATING_BUTTON_EVENTS = {
VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变
POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变
ENABLED_CHANGED: 'floating-button:enabled-changed', // 悬浮按钮启用状态改变
HOVER_STATE_CHANGED: 'floating-button:hover-state-changed',
SNAPSHOT_REQUEST: 'floating-button:snapshot-request',
SNAPSHOT_UPDATED: 'floating-button:snapshot-updated',
LANGUAGE_REQUEST: 'floating-button:language-request',
Expand Down
14 changes: 10 additions & 4 deletions src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {
} from './layout'
import windowStateManager from 'electron-window-state'

const FLOATING_WIDGET_WINDOW_OPACITY = 1

export class FloatingButtonWindow {
private window: BrowserWindow | null = null
private config: FloatingButtonConfig
Expand Down Expand Up @@ -83,7 +81,7 @@ export class FloatingButtonWindow {
this.windowState.manage(this.window)
this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
this.window.setOpacity(FLOATING_WIDGET_WINDOW_OPACITY)
this.window.setOpacity(1)
this.setBounds(initialBounds)

if (isDev) {
Expand Down Expand Up @@ -135,7 +133,7 @@ export class FloatingButtonWindow {
return
}

this.window.setOpacity(FLOATING_WIDGET_WINDOW_OPACITY)
this.window.setOpacity(1)

if (config.alwaysOnTop !== undefined) {
this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
Expand Down Expand Up @@ -176,6 +174,14 @@ export class FloatingButtonWindow {
this.state.bounds = { ...bounds }
}

public setOpacity(opacity: number): void {
if (!this.window || this.window.isDestroyed()) {
return
}

this.window.setOpacity(opacity)
}

public getDockSide(): FloatingWidgetDockSide {
return this.dockSide
}
Expand Down
98 changes: 94 additions & 4 deletions src/main/presenter/floatingButtonPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { FloatingButtonWindow } from './FloatingButtonWindow'
import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types'
import {
buildFloatingWidgetSnapshot,
getPeekedCollapsedBounds,
getWidgetSizeForSnapshot,
repositionWidgetForResize,
snapWidgetBoundsToEdge,
Expand All @@ -23,6 +24,9 @@ const EMPTY_SNAPSHOT: FloatingWidgetSnapshot = {

const WIDGET_LAYOUT_ANIMATION_DURATION_MS = 360
const WIDGET_LAYOUT_ANIMATION_INTERVAL_MS = 16
const COLLAPSE_REVEAL_LOCK_MS = WIDGET_LAYOUT_ANIMATION_DURATION_MS + 120
const COLLAPSED_WIDGET_INACTIVE_OPACITY = 0.5
const ACTIVE_WIDGET_OPACITY = 1

type DragRuntimeState = {
startX: number
Expand All @@ -39,7 +43,10 @@ export class FloatingButtonPresenter {
private configPresenter: IConfigPresenter
private snapshot: FloatingWidgetSnapshot = { ...EMPTY_SNAPSHOT }
private layoutAnimationTimer: ReturnType<typeof setInterval> | null = null
private collapseRevealTimer: ReturnType<typeof setTimeout> | null = null
private isDragging = false
private isHovered = false
private collapseRevealLock = false
private pendingLayoutSync = false

constructor(configPresenter: IConfigPresenter) {
Expand Down Expand Up @@ -80,6 +87,8 @@ export class FloatingButtonPresenter {
this.config.enabled = false
this.snapshot = { ...EMPTY_SNAPSHOT }
this.isDragging = false
this.isHovered = false
this.clearCollapseRevealLock()
this.pendingLayoutSync = false
this.stopLayoutAnimation()

Expand All @@ -88,6 +97,7 @@ export class FloatingButtonPresenter {
ipcMain.removeHandler(FLOATING_BUTTON_EVENTS.THEME_REQUEST)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.SET_EXPANDED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.OPEN_SESSION)
Expand All @@ -111,10 +121,10 @@ export class FloatingButtonPresenter {
this.config.enabled = true

if (this.floatingWindow) {
this.floatingWindow.show()
await this.refreshWidgetState()
this.refreshLanguage()
await this.refreshTheme()
this.floatingWindow.show()
return
}

Expand Down Expand Up @@ -190,10 +200,10 @@ export class FloatingButtonPresenter {
await this.floatingWindow.create()
}

this.floatingWindow.show()
await this.refreshWidgetState()
this.refreshLanguage()
await this.refreshTheme()
this.floatingWindow.show()
}

private registerIpcHandlers(): void {
Expand All @@ -202,6 +212,7 @@ export class FloatingButtonPresenter {
ipcMain.removeHandler(FLOATING_BUTTON_EVENTS.THEME_REQUEST)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.SET_EXPANDED)
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.OPEN_SESSION)
Expand Down Expand Up @@ -234,6 +245,10 @@ export class FloatingButtonPresenter {
this.showContextMenu()
})

ipcMain.on(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED, (_event, hovering: boolean) => {
this.setHovering(Boolean(hovering))
})

ipcMain.on(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED, () => {
this.toggleExpanded()
})
Expand All @@ -257,9 +272,11 @@ export class FloatingButtonPresenter {
}

this.stopLayoutAnimation()
this.clearCollapseRevealLock()
this.isDragging = true
const stableBounds = this.getSnapshotBounds(bounds)
this.floatingWindow.setBounds(stableBounds)
this.isDragging = true
this.floatingWindow.setOpacity(this.resolveWindowOpacity())

dragState = {
startX: x,
Expand Down Expand Up @@ -313,7 +330,12 @@ export class FloatingButtonPresenter {
this.floatingWindow.setBounds(snapped)
this.isDragging = false
dragState = null
this.floatingWindow.setOpacity(this.resolveWindowOpacity())
const hadPendingLayoutSync = this.pendingLayoutSync
this.flushPendingLayoutSync()
if (!hadPendingLayoutSync) {
this.applyWindowLayout()
}
})
}

Expand All @@ -322,10 +344,20 @@ export class FloatingButtonPresenter {
return
}

const wasExpanded = this.snapshot.expanded
if (expanded) {
this.clearCollapseRevealLock()
}

this.snapshot = {
...this.snapshot,
expanded
}

if (wasExpanded && !expanded) {
this.engageCollapseRevealLock()
}

this.applyWindowLayout(true)
this.pushSnapshotToRenderer()
}
Expand All @@ -334,6 +366,20 @@ export class FloatingButtonPresenter {
this.setExpanded(!this.snapshot.expanded)
}

private setHovering(hovering: boolean): void {
if (this.isHovered === hovering) {
return
}

this.isHovered = hovering

if (!this.snapshot.expanded && this.collapseRevealLock) {
return
}

this.applyWindowLayout(true)
}

private applyWindowLayout(animate = false): void {
if (!this.floatingWindow?.exists()) {
return
Expand All @@ -350,6 +396,7 @@ export class FloatingButtonPresenter {
}

const nextBounds = this.getSnapshotBounds(bounds)
this.floatingWindow.setOpacity(this.resolveWindowOpacity())

if (!animate || this.areBoundsEqual(bounds, nextBounds)) {
this.stopLayoutAnimation()
Expand Down Expand Up @@ -411,6 +458,29 @@ export class FloatingButtonPresenter {
}
}

private clearCollapseRevealLock(): void {
this.collapseRevealLock = false

if (this.collapseRevealTimer) {
clearTimeout(this.collapseRevealTimer)
this.collapseRevealTimer = null
}
}

private engageCollapseRevealLock(): void {
this.collapseRevealLock = true

if (this.collapseRevealTimer) {
clearTimeout(this.collapseRevealTimer)
}

this.collapseRevealTimer = setTimeout(() => {
this.collapseRevealTimer = null
this.collapseRevealLock = false
this.applyWindowLayout(true)
}, COLLAPSE_REVEAL_LOCK_MS)
}

private flushPendingLayoutSync(): void {
if (!this.pendingLayoutSync) {
return
Expand All @@ -426,12 +496,32 @@ export class FloatingButtonPresenter {
}

const currentDisplay = screen.getDisplayMatching(bounds)
return repositionWidgetForResize(
const resizedBounds = repositionWidgetForResize(
bounds,
getWidgetSizeForSnapshot(this.snapshot),
currentDisplay.workArea,
this.floatingWindow.getDockSide()
)

if (!this.snapshot.expanded && !this.shouldRevealCollapsedWidget()) {
return getPeekedCollapsedBounds(
resizedBounds,
currentDisplay.workArea,
this.floatingWindow.getDockSide()
)
}

return resizedBounds
}

private shouldRevealCollapsedWidget(): boolean {
return this.snapshot.expanded || this.isHovered || this.isDragging || this.collapseRevealLock
}

private resolveWindowOpacity(): number {
return this.shouldRevealCollapsedWidget()
? ACTIVE_WIDGET_OPACITY
: COLLAPSED_WIDGET_INACTIVE_OPACITY
}

private easeInOutCubic(progress: number): number {
Expand Down
23 changes: 21 additions & 2 deletions src/main/presenter/floatingButtonPresenter/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export interface WidgetRect {
}

export const FLOATING_WIDGET_LAYOUT = {
collapsedIdle: { width: 64, height: 64 },
collapsedBusy: { width: 64, height: 64 },
collapsedIdle: { width: 50, height: 50 },
collapsedBusy: { width: 50, height: 50 },
expandedWidth: 388,
expandedMinHeight: 168,
expandedMaxHeight: 392,
Expand Down Expand Up @@ -147,6 +147,25 @@ export function repositionWidgetForResize(
}
}

export function getPeekedCollapsedBounds(
bounds: WidgetRect,
workArea: WidgetRect,
dockSide: FloatingWidgetDockSide
): WidgetRect {
const hiddenWidth = Math.round(bounds.width / 2)
const x =
dockSide === 'left'
? workArea.x - hiddenWidth
: workArea.x + workArea.width - bounds.width + hiddenWidth

return {
x: Math.round(x),
y: clampWidgetY(bounds.y, bounds.height, workArea),
width: bounds.width,
height: bounds.height
}
}

export function snapWidgetBoundsToEdge(
bounds: WidgetRect,
workArea: WidgetRect
Expand Down
5 changes: 5 additions & 0 deletions src/preload/floating-preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { FloatingWidgetSnapshot } from '@shared/types/floating-widget'
const FLOATING_BUTTON_EVENTS = {
CLICKED: 'floating-button:clicked',
RIGHT_CLICKED: 'floating-button:right-clicked',
HOVER_STATE_CHANGED: 'floating-button:hover-state-changed',
SNAPSHOT_REQUEST: 'floating-button:snapshot-request',
SNAPSHOT_UPDATED: 'floating-button:snapshot-updated',
LANGUAGE_REQUEST: 'floating-button:language-request',
Expand Down Expand Up @@ -58,6 +59,10 @@ const floatingButtonAPI = {
ipcRenderer.send(FLOATING_BUTTON_EVENTS.SET_EXPANDED, expanded)
},

setHovering: (hovering: boolean) => {
ipcRenderer.send(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED, hovering)
},

openSession: (sessionId: string) => {
ipcRenderer.send(FLOATING_BUTTON_EVENTS.OPEN_SESSION, sessionId)
},
Expand Down
Loading
Loading