Skip to content

Commit db2fcee

Browse files
authored
feat: add hover state handling and opacity adjustments for floating button (#1396)
* feat: add hover state handling and opacity adjustments for floating button * chore: update
1 parent 85a7490 commit db2fcee

9 files changed

Lines changed: 340 additions & 13 deletions

File tree

src/main/events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ export const FLOATING_BUTTON_EVENTS = {
230230
VISIBILITY_CHANGED: 'floating-button:visibility-changed', // 悬浮按钮显示状态改变
231231
POSITION_CHANGED: 'floating-button:position-changed', // 悬浮按钮位置改变
232232
ENABLED_CHANGED: 'floating-button:enabled-changed', // 悬浮按钮启用状态改变
233+
HOVER_STATE_CHANGED: 'floating-button:hover-state-changed',
233234
SNAPSHOT_REQUEST: 'floating-button:snapshot-request',
234235
SNAPSHOT_UPDATED: 'floating-button:snapshot-updated',
235236
LANGUAGE_REQUEST: 'floating-button:language-request',

src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
} from './layout'
1111
import windowStateManager from 'electron-window-state'
1212

13-
const FLOATING_WIDGET_WINDOW_OPACITY = 1
14-
1513
export class FloatingButtonWindow {
1614
private window: BrowserWindow | null = null
1715
private config: FloatingButtonConfig
@@ -83,7 +81,7 @@ export class FloatingButtonWindow {
8381
this.windowState.manage(this.window)
8482
this.window.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
8583
this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
86-
this.window.setOpacity(FLOATING_WIDGET_WINDOW_OPACITY)
84+
this.window.setOpacity(1)
8785
this.setBounds(initialBounds)
8886

8987
if (isDev) {
@@ -135,7 +133,7 @@ export class FloatingButtonWindow {
135133
return
136134
}
137135

138-
this.window.setOpacity(FLOATING_WIDGET_WINDOW_OPACITY)
136+
this.window.setOpacity(1)
139137

140138
if (config.alwaysOnTop !== undefined) {
141139
this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
@@ -176,6 +174,14 @@ export class FloatingButtonWindow {
176174
this.state.bounds = { ...bounds }
177175
}
178176

177+
public setOpacity(opacity: number): void {
178+
if (!this.window || this.window.isDestroyed()) {
179+
return
180+
}
181+
182+
this.window.setOpacity(opacity)
183+
}
184+
179185
public getDockSide(): FloatingWidgetDockSide {
180186
return this.dockSide
181187
}

src/main/presenter/floatingButtonPresenter/index.ts

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { FloatingButtonWindow } from './FloatingButtonWindow'
22
import { FloatingButtonConfig, FloatingButtonState, DEFAULT_FLOATING_BUTTON_CONFIG } from './types'
33
import {
44
buildFloatingWidgetSnapshot,
5+
getPeekedCollapsedBounds,
56
getWidgetSizeForSnapshot,
67
repositionWidgetForResize,
78
snapWidgetBoundsToEdge,
@@ -23,6 +24,9 @@ const EMPTY_SNAPSHOT: FloatingWidgetSnapshot = {
2324

2425
const WIDGET_LAYOUT_ANIMATION_DURATION_MS = 360
2526
const WIDGET_LAYOUT_ANIMATION_INTERVAL_MS = 16
27+
const COLLAPSE_REVEAL_LOCK_MS = WIDGET_LAYOUT_ANIMATION_DURATION_MS + 120
28+
const COLLAPSED_WIDGET_INACTIVE_OPACITY = 0.5
29+
const ACTIVE_WIDGET_OPACITY = 1
2630

2731
type DragRuntimeState = {
2832
startX: number
@@ -39,7 +43,10 @@ export class FloatingButtonPresenter {
3943
private configPresenter: IConfigPresenter
4044
private snapshot: FloatingWidgetSnapshot = { ...EMPTY_SNAPSHOT }
4145
private layoutAnimationTimer: ReturnType<typeof setInterval> | null = null
46+
private collapseRevealTimer: ReturnType<typeof setTimeout> | null = null
4247
private isDragging = false
48+
private isHovered = false
49+
private collapseRevealLock = false
4350
private pendingLayoutSync = false
4451

4552
constructor(configPresenter: IConfigPresenter) {
@@ -80,6 +87,8 @@ export class FloatingButtonPresenter {
8087
this.config.enabled = false
8188
this.snapshot = { ...EMPTY_SNAPSHOT }
8289
this.isDragging = false
90+
this.isHovered = false
91+
this.clearCollapseRevealLock()
8392
this.pendingLayoutSync = false
8493
this.stopLayoutAnimation()
8594

@@ -88,6 +97,7 @@ export class FloatingButtonPresenter {
8897
ipcMain.removeHandler(FLOATING_BUTTON_EVENTS.THEME_REQUEST)
8998
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
9099
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)
100+
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED)
91101
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED)
92102
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.SET_EXPANDED)
93103
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.OPEN_SESSION)
@@ -111,10 +121,10 @@ export class FloatingButtonPresenter {
111121
this.config.enabled = true
112122

113123
if (this.floatingWindow) {
114-
this.floatingWindow.show()
115124
await this.refreshWidgetState()
116125
this.refreshLanguage()
117126
await this.refreshTheme()
127+
this.floatingWindow.show()
118128
return
119129
}
120130

@@ -190,10 +200,10 @@ export class FloatingButtonPresenter {
190200
await this.floatingWindow.create()
191201
}
192202

193-
this.floatingWindow.show()
194203
await this.refreshWidgetState()
195204
this.refreshLanguage()
196205
await this.refreshTheme()
206+
this.floatingWindow.show()
197207
}
198208

199209
private registerIpcHandlers(): void {
@@ -202,6 +212,7 @@ export class FloatingButtonPresenter {
202212
ipcMain.removeHandler(FLOATING_BUTTON_EVENTS.THEME_REQUEST)
203213
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.CLICKED)
204214
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.RIGHT_CLICKED)
215+
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED)
205216
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED)
206217
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.SET_EXPANDED)
207218
ipcMain.removeAllListeners(FLOATING_BUTTON_EVENTS.OPEN_SESSION)
@@ -234,6 +245,10 @@ export class FloatingButtonPresenter {
234245
this.showContextMenu()
235246
})
236247

248+
ipcMain.on(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED, (_event, hovering: boolean) => {
249+
this.setHovering(Boolean(hovering))
250+
})
251+
237252
ipcMain.on(FLOATING_BUTTON_EVENTS.TOGGLE_EXPANDED, () => {
238253
this.toggleExpanded()
239254
})
@@ -257,9 +272,11 @@ export class FloatingButtonPresenter {
257272
}
258273

259274
this.stopLayoutAnimation()
275+
this.clearCollapseRevealLock()
276+
this.isDragging = true
260277
const stableBounds = this.getSnapshotBounds(bounds)
261278
this.floatingWindow.setBounds(stableBounds)
262-
this.isDragging = true
279+
this.floatingWindow.setOpacity(this.resolveWindowOpacity())
263280

264281
dragState = {
265282
startX: x,
@@ -313,7 +330,12 @@ export class FloatingButtonPresenter {
313330
this.floatingWindow.setBounds(snapped)
314331
this.isDragging = false
315332
dragState = null
333+
this.floatingWindow.setOpacity(this.resolveWindowOpacity())
334+
const hadPendingLayoutSync = this.pendingLayoutSync
316335
this.flushPendingLayoutSync()
336+
if (!hadPendingLayoutSync) {
337+
this.applyWindowLayout()
338+
}
317339
})
318340
}
319341

@@ -322,10 +344,20 @@ export class FloatingButtonPresenter {
322344
return
323345
}
324346

347+
const wasExpanded = this.snapshot.expanded
348+
if (expanded) {
349+
this.clearCollapseRevealLock()
350+
}
351+
325352
this.snapshot = {
326353
...this.snapshot,
327354
expanded
328355
}
356+
357+
if (wasExpanded && !expanded) {
358+
this.engageCollapseRevealLock()
359+
}
360+
329361
this.applyWindowLayout(true)
330362
this.pushSnapshotToRenderer()
331363
}
@@ -334,6 +366,20 @@ export class FloatingButtonPresenter {
334366
this.setExpanded(!this.snapshot.expanded)
335367
}
336368

369+
private setHovering(hovering: boolean): void {
370+
if (this.isHovered === hovering) {
371+
return
372+
}
373+
374+
this.isHovered = hovering
375+
376+
if (!this.snapshot.expanded && this.collapseRevealLock) {
377+
return
378+
}
379+
380+
this.applyWindowLayout(true)
381+
}
382+
337383
private applyWindowLayout(animate = false): void {
338384
if (!this.floatingWindow?.exists()) {
339385
return
@@ -350,6 +396,7 @@ export class FloatingButtonPresenter {
350396
}
351397

352398
const nextBounds = this.getSnapshotBounds(bounds)
399+
this.floatingWindow.setOpacity(this.resolveWindowOpacity())
353400

354401
if (!animate || this.areBoundsEqual(bounds, nextBounds)) {
355402
this.stopLayoutAnimation()
@@ -411,6 +458,29 @@ export class FloatingButtonPresenter {
411458
}
412459
}
413460

461+
private clearCollapseRevealLock(): void {
462+
this.collapseRevealLock = false
463+
464+
if (this.collapseRevealTimer) {
465+
clearTimeout(this.collapseRevealTimer)
466+
this.collapseRevealTimer = null
467+
}
468+
}
469+
470+
private engageCollapseRevealLock(): void {
471+
this.collapseRevealLock = true
472+
473+
if (this.collapseRevealTimer) {
474+
clearTimeout(this.collapseRevealTimer)
475+
}
476+
477+
this.collapseRevealTimer = setTimeout(() => {
478+
this.collapseRevealTimer = null
479+
this.collapseRevealLock = false
480+
this.applyWindowLayout(true)
481+
}, COLLAPSE_REVEAL_LOCK_MS)
482+
}
483+
414484
private flushPendingLayoutSync(): void {
415485
if (!this.pendingLayoutSync) {
416486
return
@@ -426,12 +496,32 @@ export class FloatingButtonPresenter {
426496
}
427497

428498
const currentDisplay = screen.getDisplayMatching(bounds)
429-
return repositionWidgetForResize(
499+
const resizedBounds = repositionWidgetForResize(
430500
bounds,
431501
getWidgetSizeForSnapshot(this.snapshot),
432502
currentDisplay.workArea,
433503
this.floatingWindow.getDockSide()
434504
)
505+
506+
if (!this.snapshot.expanded && !this.shouldRevealCollapsedWidget()) {
507+
return getPeekedCollapsedBounds(
508+
resizedBounds,
509+
currentDisplay.workArea,
510+
this.floatingWindow.getDockSide()
511+
)
512+
}
513+
514+
return resizedBounds
515+
}
516+
517+
private shouldRevealCollapsedWidget(): boolean {
518+
return this.snapshot.expanded || this.isHovered || this.isDragging || this.collapseRevealLock
519+
}
520+
521+
private resolveWindowOpacity(): number {
522+
return this.shouldRevealCollapsedWidget()
523+
? ACTIVE_WIDGET_OPACITY
524+
: COLLAPSED_WIDGET_INACTIVE_OPACITY
435525
}
436526

437527
private easeInOutCubic(progress: number): number {

src/main/presenter/floatingButtonPresenter/layout.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ export interface WidgetRect {
1515
}
1616

1717
export const FLOATING_WIDGET_LAYOUT = {
18-
collapsedIdle: { width: 64, height: 64 },
19-
collapsedBusy: { width: 64, height: 64 },
18+
collapsedIdle: { width: 50, height: 50 },
19+
collapsedBusy: { width: 50, height: 50 },
2020
expandedWidth: 388,
2121
expandedMinHeight: 168,
2222
expandedMaxHeight: 392,
@@ -147,6 +147,25 @@ export function repositionWidgetForResize(
147147
}
148148
}
149149

150+
export function getPeekedCollapsedBounds(
151+
bounds: WidgetRect,
152+
workArea: WidgetRect,
153+
dockSide: FloatingWidgetDockSide
154+
): WidgetRect {
155+
const hiddenWidth = Math.round(bounds.width / 2)
156+
const x =
157+
dockSide === 'left'
158+
? workArea.x - hiddenWidth
159+
: workArea.x + workArea.width - bounds.width + hiddenWidth
160+
161+
return {
162+
x: Math.round(x),
163+
y: clampWidgetY(bounds.y, bounds.height, workArea),
164+
width: bounds.width,
165+
height: bounds.height
166+
}
167+
}
168+
150169
export function snapWidgetBoundsToEdge(
151170
bounds: WidgetRect,
152171
workArea: WidgetRect

src/preload/floating-preload.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { FloatingWidgetSnapshot } from '@shared/types/floating-widget'
55
const FLOATING_BUTTON_EVENTS = {
66
CLICKED: 'floating-button:clicked',
77
RIGHT_CLICKED: 'floating-button:right-clicked',
8+
HOVER_STATE_CHANGED: 'floating-button:hover-state-changed',
89
SNAPSHOT_REQUEST: 'floating-button:snapshot-request',
910
SNAPSHOT_UPDATED: 'floating-button:snapshot-updated',
1011
LANGUAGE_REQUEST: 'floating-button:language-request',
@@ -58,6 +59,10 @@ const floatingButtonAPI = {
5859
ipcRenderer.send(FLOATING_BUTTON_EVENTS.SET_EXPANDED, expanded)
5960
},
6061

62+
setHovering: (hovering: boolean) => {
63+
ipcRenderer.send(FLOATING_BUTTON_EVENTS.HOVER_STATE_CHANGED, hovering)
64+
},
65+
6166
openSession: (sessionId: string) => {
6267
ipcRenderer.send(FLOATING_BUTTON_EVENTS.OPEN_SESSION, sessionId)
6368
},

0 commit comments

Comments
 (0)