Skip to content

feat: add hover state handling and opacity adjustments for floating button#1396

Merged
zerob13 merged 2 commits intodevfrom
floating-window
Mar 26, 2026
Merged

feat: add hover state handling and opacity adjustments for floating button#1396
zerob13 merged 2 commits intodevfrom
floating-window

Conversation

@zhangmo8
Copy link
Copy Markdown
Collaborator

@zhangmo8 zhangmo8 commented Mar 26, 2026

close #1393

20260326_144013.mp4

Summary by CodeRabbit

  • New Features

    • Floating button responds to hover with visual opacity feedback and can be externally signaled to reveal.
    • Widget reveals on hover, dims when collapsed/inactive, and now “peeks” partially off‑screen when idle.
    • Improved close-motion and transition timing for smoother collapse/expand animations.
    • Refined collapsed sizing for a sleeker appearance.
  • Tests

    • Added comprehensive tests covering hover reveal, peek positioning, opacity, and animation timing.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Introduces hover-driven "peek and reveal" behavior: adds hover IPC/event, exposes renderer API to set hover, reduces collapsed widget size to 50×50, computes peeked off-screen bounds, adds opacity control (0.5 idle, 1 revealed), and implements a collapse reveal-lock and closing-motion state.

Changes

Cohort / File(s) Summary
Events
src/main/events.ts
Added HOVER_STATE_CHANGED (floating-button:hover-state-changed) to FLOATING_BUTTON_EVENTS.
Window API
src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts
Removed internal opacity constant; added public setOpacity(opacity: number): void and replaced internal uses with explicit opacity calls.
Presenter Logic
src/main/presenter/floatingButtonPresenter/index.ts
Added hover-state listener (IPC), reveal-lock timer for collapse transitions, opacity resolution (0.5 vs 1), peeked bounds usage, and adjusted drag/layout application timing.
Layout
src/main/presenter/floatingButtonPresenter/layout.ts
Changed collapsed sizes from 64×64 → 50×50 and added getPeekedCollapsedBounds(...) to compute half-offscreen peek positioning.
Preload / IPC Bridge
src/preload/floating-preload.ts
Added floatingButtonAPI.setHovering(hovering: boolean) to dispatch hover-state-changed IPC events.
Renderer types
src/renderer/floating/env.d.ts
Added window.floatingButtonAPI.setHovering: (hovering: boolean) => void signature.
Vue component
src/renderer/floating/FloatingButton.vue
Added closing-motion state, hover tracking and handlers, wired hover API calls, template/data-motion exposure, and CSS for closing-phase styling.
Tests
test/main/presenter/floatingButtonPresenter/index.test.ts, test/main/presenter/floatingButtonPresenter/layout.test.ts
Updated mocks to include opacity and setOpacity; added/adjusted tests for opacity, peeked bounds, and hover-driven reveal/close timing behaviors.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Renderer as FloatingButton.vue
    participant Preload as floating-preload
    participant Presenter as FloatingButtonPresenter
    participant Window as FloatingButtonWindow

    User->>Renderer: mouse enter
    Renderer->>Preload: floatingButtonAPI.setHovering(true)
    Preload->>Presenter: HOVER_STATE_CHANGED(true)
    Presenter->>Presenter: update hover state, resolve opacity/bounds
    Presenter->>Window: setOpacity(1)
    Presenter->>Presenter: apply revealed layout

    User->>Renderer: mouse leave
    Renderer->>Preload: floatingButtonAPI.setHovering(false)
    Preload->>Presenter: HOVER_STATE_CHANGED(false)
    Presenter->>Presenter: update hover state, resolve opacity/bounds (peek)
    Presenter->>Window: setOpacity(0.5)
    Presenter->>Presenter: apply peeked layout
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • yyhhyyyyyy
  • zerob13

Poem

🐇 I peek at edges, a fifty-pixel sigh,
I dim to hush, then brighten when you’re nigh.
A hover wakes my glow, a gentle little hop,
I hide and then return — the perfect countertop! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: adding hover state handling and opacity adjustments for the floating button widget.
Linked Issues check ✅ Passed The PR implements the requested features: reduced floating button size (50x50 vs 64x64), edge-hiding via opacity/peek positioning, and hover-driven reveal mechanics.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the linked issue requirements: size reduction, opacity management, hover handling, and layout adjustments for edge-anchoring.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch floating-window

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
test/main/presenter/floatingButtonPresenter/index.test.ts (1)

226-232: ⚠️ Potential issue | 🟠 Major

This drag-move expectation is still using the pre-resize x-offset.

After DRAG_START, the presenter snaps the collapsed widget to the fully revealed x-position before applying the drag delta. With the new 50px width, moving from (100, 100) to (220, 150) lands at 1270, so 1256 is stale.

Proposed fix
     await emitEvent(FLOATING_BUTTON_EVENTS.DRAG_MOVE, { x: 220, y: 150 })
     expect(floatingWindowState.bounds).toMatchObject({
-      x: 1256,
+      x:
+        electronState.workArea.x +
+        electronState.workArea.width -
+        getCollapsedWidgetSize(0).width +
+        (220 - 100),
       y: 230,
       width: getCollapsedWidgetSize(0).width,
       height: getCollapsedWidgetSize(0).height
     })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/main/presenter/floatingButtonPresenter/index.test.ts` around lines 226 -
232, The test expectation for the DRAG_MOVE event uses the old x-offset (1256);
update the expected x to account for the presenter's snap-to-fully-revealed
behavior after DRAG_START plus the drag delta so it becomes 1270. Modify the
assertion in the FLOATING_BUTTON_EVENTS.DRAG_MOVE test to expect
floatingWindowState.bounds.x === 1270 while keeping the width/height checks
using getCollapsedWidgetSize(0).height/width and leave the rest of the assertion
unchanged.
src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts (1)

130-140: ⚠️ Potential issue | 🟡 Minor

Don't overwrite presenter-controlled opacity in updateConfig().

src/main/presenter/floatingButtonPresenter/index.ts now owns the 0.5/1 opacity state. This unconditional reset leaves a collapsed idle widget fully opaque after any config update until some later hover/layout event reapplies the dimmed state.

Proposed fix
   public updateConfig(config: Partial<FloatingButtonConfig>): void {
     this.config = { ...this.config, ...config }
     if (!this.window) {
       return
     }
-
-    this.window.setOpacity(1)
 
     if (config.alwaysOnTop !== undefined) {
       this.window.setAlwaysOnTop(this.config.alwaysOnTop, 'floating')
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts` around
lines 130 - 140, updateConfig in FloatingButtonWindow currently forces the
window to full opacity with this.window.setOpacity(1), which overwrites the
presenter-managed dimmed state; change it so opacity is only updated when an
explicit opacity value is provided in the incoming config (e.g., if
(config.opacity !== undefined) this.window.setOpacity(config.opacity)) and
remove the unconditional setOpacity(1) call so presenter-controlled 0.5/1 state
remains intact.
src/renderer/floating/FloatingButton.vue (1)

1-2: ⚠️ Potential issue | 🟡 Minor

Run Prettier to fix formatting issues.

The pipeline reports Prettier formatting violations. As per coding guidelines, run pnpm run format to fix code style issues before merging.

pnpm run format
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/floating/FloatingButton.vue` around lines 1 - 2, Run the
project's formatter on the changed Vue script block to resolve Prettier
violations: open src/renderer/floating/FloatingButton.vue (the <script setup
lang="ts"> section where computed, onMounted, onUnmounted, ref are imported) and
run the project's format command (pnpm run format) to apply Prettier rules, then
review and stage the resulting formatting changes and update the commit/PR.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/floating/FloatingButton.vue`:
- Around line 132-138: handleMouseLeave skips while dragging so if a drag ends
outside the widget the hover state can remain true; update handleMouseUp to
reconcile hover by checking dragState.value.isDragging (or whether a drag just
finished) and call setHovering(false) (or invoke the same logic as
handleMouseLeave) after clearing dragState so isHovering is reset when the drag
ends outside the component; reference handleMouseUp, handleMouseLeave,
dragState, setHovering and isDragging when making the change.

In `@test/main/presenter/floatingButtonPresenter/layout.test.ts`:
- Around line 85-103: The test assumes a 64px collapsed button but the layout
now uses 50px; update the expectation in the 'hides half of the collapsed widget
outside the work area when idle' test to match the new collapsed size by
changing the asserted peeked x from 832 to 839 (or compute it dynamically using
getCollapsedWidgetSize(0).width) so it aligns with getPeekedCollapsedBounds and
the collapsed size defined in layout.ts.

---

Outside diff comments:
In `@src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts`:
- Around line 130-140: updateConfig in FloatingButtonWindow currently forces the
window to full opacity with this.window.setOpacity(1), which overwrites the
presenter-managed dimmed state; change it so opacity is only updated when an
explicit opacity value is provided in the incoming config (e.g., if
(config.opacity !== undefined) this.window.setOpacity(config.opacity)) and
remove the unconditional setOpacity(1) call so presenter-controlled 0.5/1 state
remains intact.

In `@src/renderer/floating/FloatingButton.vue`:
- Around line 1-2: Run the project's formatter on the changed Vue script block
to resolve Prettier violations: open src/renderer/floating/FloatingButton.vue
(the <script setup lang="ts"> section where computed, onMounted, onUnmounted,
ref are imported) and run the project's format command (pnpm run format) to
apply Prettier rules, then review and stage the resulting formatting changes and
update the commit/PR.

In `@test/main/presenter/floatingButtonPresenter/index.test.ts`:
- Around line 226-232: The test expectation for the DRAG_MOVE event uses the old
x-offset (1256); update the expected x to account for the presenter's
snap-to-fully-revealed behavior after DRAG_START plus the drag delta so it
becomes 1270. Modify the assertion in the FLOATING_BUTTON_EVENTS.DRAG_MOVE test
to expect floatingWindowState.bounds.x === 1270 while keeping the width/height
checks using getCollapsedWidgetSize(0).height/width and leave the rest of the
assertion unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a62c009e-9622-4bbe-8490-3c2cd3771728

📥 Commits

Reviewing files that changed from the base of the PR and between 5781fa0 and 1d93460.

📒 Files selected for processing (9)
  • src/main/events.ts
  • src/main/presenter/floatingButtonPresenter/FloatingButtonWindow.ts
  • src/main/presenter/floatingButtonPresenter/index.ts
  • src/main/presenter/floatingButtonPresenter/layout.ts
  • src/preload/floating-preload.ts
  • src/renderer/floating/FloatingButton.vue
  • src/renderer/floating/env.d.ts
  • test/main/presenter/floatingButtonPresenter/index.test.ts
  • test/main/presenter/floatingButtonPresenter/layout.test.ts

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/renderer/floating/FloatingButton.vue (1)

128-138: ⚠️ Potential issue | 🟡 Minor

Keep hover latched for the full press/drag gesture.

Line 133 only suppresses mouseleave after isDragging flips true. During the 180ms delay / before the 4px threshold, a quick exit still sends setHovering(false) while the button is held, so the main presenter can peek the window back toward the edge mid-gesture (src/preload/floating-preload.ts:62-64, src/main/presenter/floatingButtonPresenter/index.ts:369-381). handleMouseUp also never reconciles hover afterward, so once it drifts it can stay wrong until the next enter/leave.

💡 Possible fix
 const handleMouseLeave = () => {
-  if (dragState.value.isDragging) {
+  if (dragState.value.isMouseDown) {
     return
   }

   setHovering(false)
 }
+
+const reconcileHovering = (event: MouseEvent) => {
+  const hovered = document.elementFromPoint(event.clientX, event.clientY) as HTMLElement | null
+  setHovering(Boolean(hovered?.closest('.widget-stage')))
+}

 const handleMouseUp = (event: MouseEvent) => {
   if (event.button !== 0) {
     return
   }
@@
   if (wasDragging) {
     dragState.value.isDragging = false
     isDragging.value = false
     window.floatingButtonAPI.onDragEnd(event.screenX, event.screenY)
   } else if (!snapshot.value.expanded) {
     toggleExpanded()
   }
+
+  reconcileHovering(event)

   document.removeEventListener('mousemove', handleMouseMove)
   document.removeEventListener('mouseup', handleMouseUp)
 }

Also applies to: 189-202, 266-267

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/floating/FloatingButton.vue` around lines 128 - 138, The
mouseleave handler currently only blocks clearing hover when
dragState.value.isDragging is true, which allows clearing during the initial
press-to-drag delay; update handleMouseLeave to keep hover latched for the
entire press/drag gesture by also checking whatever flag indicates an ongoing
press (e.g., dragState.value.isPressed or dragState.value.isPointerDown) and
return early if that flag is set; additionally, update handleMouseUp to
re-evaluate/reconcile hover state after release (call setHovering(...) based on
pointer position/element under pointer or simply setHovering(false) only when
not pressed/dragging) so hover can't remain out-of-sync—apply the same guarded
logic pattern to the other similar handlers mentioned (lines around 189-202 and
266-267) using the same dragState symbols and setHovering function.
🧹 Nitpick comments (1)
src/renderer/floating/FloatingButton.vue (1)

570-572: These close-motion overrides look effectively dead.

By the time data-motion='closing' renders, snapshot.expanded has already flipped false, so the collapsed side no longer carries .collapsed-layer-hidden. That means these selectors never participate in the close path and can mislead later motion tweaks.

Also applies to: 652-654

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/floating/FloatingButton.vue` around lines 570 - 572, The CSS
rules targeting ".widget-stage[data-motion='closing'] .collapsed-layer-hidden"
are dead because snapshot.expanded is already false by the time
data-motion='closing' is applied, so elements no longer have the
.collapsed-layer-hidden class; remove these closing-specific overrides (the
rules around ".widget-stage[data-motion='closing'] .collapsed-layer-hidden" and
the duplicate block at 652-654) or, if you intended to affect the close
animation, instead ensure the element keeps .collapsed-layer-hidden until after
the closing motion by adjusting the snapshot.expanded toggle logic; reference
the selectors ".widget-stage[data-motion='closing'] .collapsed-layer-hidden",
the ".collapsed-layer-hidden" class and the snapshot.expanded state when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/renderer/floating/FloatingButton.vue`:
- Around line 128-138: The mouseleave handler currently only blocks clearing
hover when dragState.value.isDragging is true, which allows clearing during the
initial press-to-drag delay; update handleMouseLeave to keep hover latched for
the entire press/drag gesture by also checking whatever flag indicates an
ongoing press (e.g., dragState.value.isPressed or dragState.value.isPointerDown)
and return early if that flag is set; additionally, update handleMouseUp to
re-evaluate/reconcile hover state after release (call setHovering(...) based on
pointer position/element under pointer or simply setHovering(false) only when
not pressed/dragging) so hover can't remain out-of-sync—apply the same guarded
logic pattern to the other similar handlers mentioned (lines around 189-202 and
266-267) using the same dragState symbols and setHovering function.

---

Nitpick comments:
In `@src/renderer/floating/FloatingButton.vue`:
- Around line 570-572: The CSS rules targeting
".widget-stage[data-motion='closing'] .collapsed-layer-hidden" are dead because
snapshot.expanded is already false by the time data-motion='closing' is applied,
so elements no longer have the .collapsed-layer-hidden class; remove these
closing-specific overrides (the rules around
".widget-stage[data-motion='closing'] .collapsed-layer-hidden" and the duplicate
block at 652-654) or, if you intended to affect the close animation, instead
ensure the element keeps .collapsed-layer-hidden until after the closing motion
by adjusting the snapshot.expanded toggle logic; reference the selectors
".widget-stage[data-motion='closing'] .collapsed-layer-hidden", the
".collapsed-layer-hidden" class and the snapshot.expanded state when making the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7f222862-ac35-4ff1-a6ea-ddc543fc759e

📥 Commits

Reviewing files that changed from the base of the PR and between 1d93460 and 1f268d8.

📒 Files selected for processing (1)
  • src/renderer/floating/FloatingButton.vue

@zerob13 zerob13 merged commit db2fcee into dev Mar 26, 2026
3 checks passed
@zhangmo8 zhangmo8 deleted the floating-window branch March 26, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【feature】 悬浮按钮再小点更好,靠边隐藏 v 1.0.0 beta 6版本

2 participants