Skip to content

Commit d3499cb

Browse files
authored
fix(web-ui): poll cursor position for companion pet hover (#628)
Use Tauri cursorPosition with window outer position and scale factor to keep pet hitbox hover state accurate when the window is moved or DPI changes.
1 parent 25fb9d4 commit d3499cb

1 file changed

Lines changed: 94 additions & 1 deletion

File tree

src/web-ui/src/app/components/AgentCompanionDesktopPet/AgentCompanionDesktopPet.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { listen } from '@tauri-apps/api/event';
4-
import { getCurrentWindow } from '@tauri-apps/api/window';
4+
import { cursorPosition, getCurrentWindow } from '@tauri-apps/api/window';
55
import { aiExperienceConfigService, type AgentCompanionPetSelection, type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService';
66
import { ChatInputPixelPet, type ChatInputPixelPetMood } from '@/flow_chat/components/ChatInputPixelPet';
77
import type { ChatInputPetMood } from '@/flow_chat/utils/chatInputPetMood';
@@ -21,6 +21,7 @@ const BUBBLE_GAP = 6;
2121
const BUBBLE_MIN_WIDTH = 132;
2222
const BUBBLE_MAX_WIDTH = 252;
2323
const WINDOW_EDGE_BUFFER = 4;
24+
const POINTER_HOVER_POLL_INTERVAL_MS = 120;
2425

2526
export const AgentCompanionDesktopPet: React.FC = () => {
2627
const { t } = useTranslation('flow-chat');
@@ -144,6 +145,98 @@ export const AgentCompanionDesktopPet: React.FC = () => {
144145
});
145146
}, [activePetSize.height, activePetSize.width, tasks]);
146147

148+
useEffect(() => {
149+
const tauriWindow = getCurrentWindow();
150+
let disposed = false;
151+
let windowPosition: { x: number; y: number } | null = null;
152+
let scaleFactor = 1;
153+
let removeWindowMovedListener: (() => void) | null = null;
154+
let removeScaleChangedListener: (() => void) | null = null;
155+
156+
void tauriWindow.outerPosition()
157+
.then(position => {
158+
windowPosition = position;
159+
})
160+
.catch(error => {
161+
log.warn('Failed to read Agent companion window position', error);
162+
});
163+
164+
void tauriWindow.scaleFactor()
165+
.then(nextScaleFactor => {
166+
scaleFactor = nextScaleFactor;
167+
})
168+
.catch(error => {
169+
log.warn('Failed to read Agent companion window scale factor', error);
170+
});
171+
172+
void tauriWindow.onMoved(event => {
173+
windowPosition = event.payload;
174+
}).then(unlisten => {
175+
if (disposed) {
176+
unlisten();
177+
} else {
178+
removeWindowMovedListener = unlisten;
179+
}
180+
}).catch(error => {
181+
log.warn('Failed to listen for Agent companion window moves', error);
182+
});
183+
184+
void tauriWindow.onScaleChanged(event => {
185+
scaleFactor = event.payload.scaleFactor;
186+
}).then(unlisten => {
187+
if (disposed) {
188+
unlisten();
189+
} else {
190+
removeScaleChangedListener = unlisten;
191+
}
192+
}).catch(error => {
193+
log.warn('Failed to listen for Agent companion scale changes', error);
194+
});
195+
196+
const pollPointerHover = async () => {
197+
try {
198+
if (!windowPosition) {
199+
windowPosition = await tauriWindow.outerPosition();
200+
}
201+
202+
const pointer = await cursorPosition();
203+
if (disposed) {
204+
return;
205+
}
206+
207+
const hitbox = dockRef.current?.querySelector<HTMLElement>('.bitfun-agent-companion-window__pet-hitbox');
208+
if (!hitbox) {
209+
setIsHoveringPet(false);
210+
return;
211+
}
212+
213+
const hitboxRect = hitbox.getBoundingClientRect();
214+
const pointerX = (pointer.x - windowPosition.x) / scaleFactor;
215+
const pointerY = (pointer.y - windowPosition.y) / scaleFactor;
216+
const isPointerInsideHitbox = pointerX >= hitboxRect.left
217+
&& pointerX <= hitboxRect.right
218+
&& pointerY >= hitboxRect.top
219+
&& pointerY <= hitboxRect.bottom;
220+
221+
setIsHoveringPet(isPointerInsideHitbox);
222+
} catch (error) {
223+
log.warn('Failed to poll Agent companion pointer hover state', error);
224+
}
225+
};
226+
227+
const intervalId = window.setInterval(() => {
228+
void pollPointerHover();
229+
}, POINTER_HOVER_POLL_INTERVAL_MS);
230+
void pollPointerHover();
231+
232+
return () => {
233+
disposed = true;
234+
window.clearInterval(intervalId);
235+
removeWindowMovedListener?.();
236+
removeScaleChangedListener?.();
237+
};
238+
}, []);
239+
147240
const startDrag = (event: React.PointerEvent<HTMLDivElement>) => {
148241
if (event.button !== 0) {
149242
return;

0 commit comments

Comments
 (0)