Skip to content

Commit 7af71dc

Browse files
committed
add mouse ignore toggle button
1 parent 273fe01 commit 7af71dc

File tree

10 files changed

+204
-17
lines changed

10 files changed

+204
-17
lines changed

package-lock.json

Lines changed: 40 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@
4444
"react": "^18.3.1",
4545
"react-dom": "^18.3.1",
4646
"react-icons": "^5.4.0",
47-
"rxjs": "^7.8.1"
47+
"rxjs": "^7.8.1",
48+
"zustand": "^5.0.3"
4849
},
4950
"devDependencies": {
5051
"@electron-toolkit/eslint-config-prettier": "^2.0.0",

src/main/menu-manager.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ export class MenuManager {
3737
return [
3838
{
3939
label: 'Window Mode',
40-
type: 'radio',
40+
type: 'radio' as const,
4141
checked: this.currentMode === 'window',
4242
click: () => {
4343
this.setMode('window');
4444
},
4545
},
4646
{
4747
label: 'Pet Mode',
48-
type: 'radio',
48+
type: 'radio' as const,
4949
checked: this.currentMode === 'pet',
5050
click: () => {
5151
this.setMode('pet');
@@ -60,7 +60,22 @@ export class MenuManager {
6060

6161
const contextMenu = Menu.buildFromTemplate([
6262
...this.getModeMenuItems(),
63-
{ type: 'separator' },
63+
{ type: 'separator' as const },
64+
// Only show toggle mouse ignore in pet mode
65+
...(this.currentMode === 'pet'
66+
? [
67+
{
68+
label: 'Toggle Mouse Passthrough',
69+
click: () => {
70+
const windows = BrowserWindow.getAllWindows();
71+
windows.forEach((window) => {
72+
window.webContents.send('toggle-force-ignore-mouse');
73+
});
74+
},
75+
},
76+
{ type: 'separator' as const },
77+
]
78+
: []),
6479
{
6580
label: 'Show',
6681
click: () => {
@@ -105,7 +120,18 @@ export class MenuManager {
105120
event.sender.send('interrupt');
106121
},
107122
},
108-
{ type: 'separator' },
123+
{ type: 'separator' as const },
124+
// Only show in pet mode
125+
...(this.currentMode === 'pet'
126+
? [
127+
{
128+
label: 'Toggle Mouse Passthrough',
129+
click: () => {
130+
event.sender.send('toggle-force-ignore-mouse');
131+
},
132+
},
133+
]
134+
: []),
109135
{
110136
label: 'Toggle Scrolling to Resize',
111137
click: () => {
@@ -123,9 +149,9 @@ export class MenuManager {
123149
},
124150
]
125151
: []),
126-
{ type: 'separator' },
152+
{ type: 'separator' as const },
127153
...this.getModeMenuItems(),
128-
{ type: 'separator' },
154+
{ type: 'separator' as const },
129155
{
130156
label: 'Switch Character',
131157
visible: this.currentMode === 'pet',
@@ -136,7 +162,7 @@ export class MenuManager {
136162
},
137163
})),
138164
},
139-
{ type: 'separator' },
165+
{ type: 'separator' as const },
140166
{
141167
label: 'Hide',
142168
click: () => {

src/main/window-manager.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export class WindowManager {
2020

2121
private currentMode: 'window' | 'pet' = 'window';
2222

23+
// Track if mouse events are forcibly ignored
24+
private forceIgnoreMouse = false;
25+
2326
constructor() {
2427
ipcMain.on('renderer-ready-for-mode-change', (_event, newMode) => {
2528
if (newMode === 'pet') {
@@ -43,6 +46,11 @@ export class WindowManager {
4346
window.setFullScreen(false);
4447
}
4548
});
49+
50+
// Handle toggle force ignore mouse events from renderer
51+
ipcMain.on('toggle-force-ignore-mouse', () => {
52+
this.toggleForceIgnoreMouse();
53+
});
4654
}
4755

4856
createWindow(options: Electron.BrowserWindowConstructorOptions): BrowserWindow {
@@ -261,6 +269,9 @@ export class WindowManager {
261269
updateComponentHover(componentId: string, isHovering: boolean): void {
262270
if (this.currentMode === 'window') return;
263271

272+
// If force ignore is enabled, don't change the mouse ignore state
273+
if (this.forceIgnoreMouse) return;
274+
264275
if (isHovering) {
265276
this.hoveringComponents.add(componentId);
266277
} else {
@@ -279,4 +290,34 @@ export class WindowManager {
279290
}
280291
}
281292
}
293+
294+
// Toggle force ignore mouse events
295+
toggleForceIgnoreMouse(): void {
296+
this.forceIgnoreMouse = !this.forceIgnoreMouse;
297+
298+
// Apply the new setting immediately
299+
if (this.forceIgnoreMouse) {
300+
if (isMac) {
301+
this.window?.setIgnoreMouseEvents(true);
302+
} else {
303+
this.window?.setIgnoreMouseEvents(true, { forward: true });
304+
}
305+
} else {
306+
// Reapply normal behavior based on hovering components
307+
const shouldIgnore = this.hoveringComponents.size === 0;
308+
if (isMac) {
309+
this.window?.setIgnoreMouseEvents(shouldIgnore);
310+
} else {
311+
this.window?.setIgnoreMouseEvents(shouldIgnore, { forward: true });
312+
}
313+
}
314+
315+
// Notify renderer about the change
316+
this.window?.webContents.send('force-ignore-mouse-changed', this.forceIgnoreMouse);
317+
}
318+
319+
// Get current force ignore state
320+
isForceIgnoreMouse(): boolean {
321+
return this.forceIgnoreMouse;
322+
}
282323
}

src/preload/index.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ declare global {
55
electron: ElectronAPI
66
api: {
77
setIgnoreMouseEvents: (ignore: boolean) => void
8+
toggleForceIgnoreMouse: () => void
9+
onForceIgnoreMouseChanged: (callback: (isForced: boolean) => void) => void
810
onModeChanged: (callback: (mode: 'pet' | 'window') => void) => void
911
showContextMenu: (x: number, y: number) => void
1012
onMicToggle: (callback: () => void) => void

src/preload/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ const api = {
77
setIgnoreMouseEvents: (ignore: boolean) => {
88
ipcRenderer.send('set-ignore-mouse-events', ignore);
99
},
10+
toggleForceIgnoreMouse: () => {
11+
ipcRenderer.send('toggle-force-ignore-mouse');
12+
},
13+
onForceIgnoreMouseChanged: (callback: (isForced: boolean) => void) => {
14+
const handler = (_event: any, isForced: boolean) => callback(isForced);
15+
ipcRenderer.on('force-ignore-mouse-changed', handler);
16+
return () => ipcRenderer.removeListener('force-ignore-mouse-changed', handler);
17+
},
1018
showContextMenu: () => {
1119
console.log('Preload showContextMenu');
1220
ipcRenderer.send('show-context-menu');

src/renderer/src/components/canvas/live2d.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import { useLive2DModel } from "@/hooks/canvas/use-live2d-model";
66
import { useLive2DResize } from "@/hooks/canvas/use-live2d-resize";
77
import { useInterrupt } from "@/hooks/utils/use-interrupt";
88
import { useAudioTask } from "@/hooks/utils/use-audio-task";
9+
import { useForceIgnoreMouse } from "@/hooks/utils/use-force-ignore-mouse";
910

1011
interface Live2DProps {
1112
isPet: boolean;
1213
}
1314

1415
export const Live2D = memo(({ isPet }: Live2DProps): JSX.Element => {
1516
const { modelInfo, isLoading } = useLive2DConfig();
17+
const { forceIgnoreMouse } = useForceIgnoreMouse();
1618

1719
// Register IPC handlers here as Live2D is a persistent component in the pet mode
1820
useIpcHandlers({ isPet });
@@ -54,7 +56,7 @@ export const Live2D = memo(({ isPet }: Live2DProps): JSX.Element => {
5456
style={{
5557
width: isPet ? "100vw" : "100%",
5658
height: isPet ? "100vh" : "100%",
57-
pointerEvents: "auto",
59+
pointerEvents: isPet && forceIgnoreMouse ? "none" : "auto",
5860
overflow: "hidden",
5961
opacity: isLoading ? 0 : 1,
6062
transition: "opacity 0.3s ease-in-out",
@@ -66,7 +68,7 @@ export const Live2D = memo(({ isPet }: Live2DProps): JSX.Element => {
6668
style={{
6769
width: "100%",
6870
height: "100%",
69-
pointerEvents: "auto",
71+
pointerEvents: isPet && forceIgnoreMouse ? "none" : "auto",
7072
display: "block",
7173
}}
7274
/>

src/renderer/src/hooks/canvas/use-live2d-model.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { setModelSize, resetModelPosition } from "./use-live2d-resize";
1818
import { audioTaskQueue } from "@/utils/task-queue";
1919
import { AiStateEnum, useAiState } from "@/context/ai-state-context";
2020
import { toaster } from "@/components/ui/toaster";
21+
import { useForceIgnoreMouse } from "../utils/use-force-ignore-mouse";
2122

2223
interface UseLive2DModelProps {
2324
isPet: boolean; // Whether the model is in pet mode
@@ -38,6 +39,7 @@ export const useLive2DModel = ({
3839
const loadingRef = useRef(false);
3940
const { setAiState, aiState } = useAiState();
4041
const [isModelReady, setIsModelReady] = useState(false);
42+
const { forceIgnoreMouse } = useForceIgnoreMouse();
4143

4244
// Cleanup function for Live2D model
4345
const cleanupModel = useCallback(() => {
@@ -196,6 +198,7 @@ export const useLive2DModel = ({
196198
(model: Live2DModel) => {
197199
if (!model) return;
198200

201+
// Clear all previous listeners
199202
model.removeAllListeners("pointerenter");
200203
model.removeAllListeners("pointerleave");
201204
model.removeAllListeners("rightdown");
@@ -204,6 +207,17 @@ export const useLive2DModel = ({
204207
model.removeAllListeners("pointerup");
205208
model.removeAllListeners("pointerupoutside");
206209

210+
// If force ignore mouse is enabled, disable interaction
211+
if (forceIgnoreMouse && isPet) {
212+
model.interactive = false;
213+
model.cursor = "default";
214+
return;
215+
}
216+
217+
// Enable interactions
218+
model.interactive = true;
219+
model.cursor = "pointer";
220+
207221
let dragging = false;
208222
let pointerX = 0;
209223
let pointerY = 0;
@@ -265,7 +279,7 @@ export const useLive2DModel = ({
265279
dragging = false;
266280
});
267281
},
268-
[isPet],
282+
[isPet, forceIgnoreMouse],
269283
);
270284

271285
const handleTapMotion = useCallback(
@@ -331,7 +345,7 @@ export const useLive2DModel = ({
331345
if (modelRef.current && isModelReady) {
332346
setupModelInteractions(modelRef.current);
333347
}
334-
}, [isModelReady, setupModelInteractions]); // Dependency of setupModelInteractions includes isPet already
348+
}, [isModelReady, setupModelInteractions, forceIgnoreMouse]);
335349

336350
return {
337351
canvasRef,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { create } from "zustand";
2+
3+
// Define the state store interface
4+
interface ForceIgnoreMouseState {
5+
// Whether mouse events are forcibly ignored
6+
forceIgnoreMouse: boolean;
7+
// Set the force ignore mouse state
8+
setForceIgnoreMouse: (forceIgnore: boolean) => void;
9+
}
10+
11+
// Create a global store for force ignore mouse state
12+
const useForceIgnoreMouseStore = create<ForceIgnoreMouseState>((set) => ({
13+
forceIgnoreMouse: false,
14+
setForceIgnoreMouse: (forceIgnore) => set({ forceIgnoreMouse: forceIgnore }),
15+
}));
16+
17+
/**
18+
* Hook to access and manage force ignore mouse state
19+
* This is used to enable/disable mouse interaction with the model in pet mode
20+
*/
21+
export function useForceIgnoreMouse() {
22+
const { forceIgnoreMouse, setForceIgnoreMouse } = useForceIgnoreMouseStore();
23+
24+
return {
25+
forceIgnoreMouse,
26+
setForceIgnoreMouse,
27+
};
28+
}

0 commit comments

Comments
 (0)