Skip to content

Commit 074a7c8

Browse files
authored
fix: prevent window from hiding when moved on Windows (#748)
* fix: prevent window from hiding when moved on Windows * docs: update changelog * update
1 parent bc524e1 commit 074a7c8

8 files changed

Lines changed: 82 additions & 101 deletions

File tree

docs/content.en/docs/release-notes/_index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ Information about release notes of Coco Server is provided here.
1717
- feat: voice input support in both search and chat modes #732
1818

1919
### 🐛 Bug fix
20+
2021
- fix(file search): apply filters before from/size parameters #741
2122
- fix(file search): searching by name&content does not search file name #743
23+
- fix: prevent window from hiding when moved on Windows #748
2224

2325
### ✈️ Improvements
2426

@@ -316,4 +318,4 @@ Information about release notes of Coco Server is provided here.
316318

317319
### Bug fix
318320

319-
### Improvements
321+
### Improvements

src-tauri/src/lib.rs

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ pub fn run() {
145145
server::attachment::delete_attachment,
146146
server::transcription::transcription,
147147
server::system_settings::get_system_settings,
148-
simulate_mouse_click,
149148
extension::built_in::application::get_app_list,
150149
extension::built_in::application::get_app_search_path,
151150
extension::built_in::application::get_app_metadata,
@@ -452,52 +451,6 @@ async fn hide_check(app_handle: AppHandle) {
452451
window.hide().unwrap();
453452
}
454453

455-
#[tauri::command]
456-
async fn simulate_mouse_click<R: Runtime>(window: WebviewWindow<R>, is_chat_mode: bool) {
457-
#[cfg(target_os = "windows")]
458-
{
459-
use enigo::{Button, Coordinate, Direction, Enigo, Mouse, Settings};
460-
use std::{thread, time::Duration};
461-
462-
if let Ok(mut enigo) = Enigo::new(&Settings::default()) {
463-
// Save the current mouse position
464-
if let Ok((original_x, original_y)) = enigo.location() {
465-
// Retrieve the window's outer position (top-left corner)
466-
if let Ok(position) = window.outer_position() {
467-
// Retrieve the window's inner size (client area)
468-
if let Ok(size) = window.inner_size() {
469-
// Calculate the center position of the title bar
470-
let x = position.x + (size.width as i32 / 2);
471-
let y = if is_chat_mode {
472-
position.y + size.height as i32 - 50
473-
} else {
474-
position.y + 30
475-
};
476-
477-
// Move the mouse cursor to the calculated position
478-
if enigo.move_mouse(x, y, Coordinate::Abs).is_ok() {
479-
// // Simulate a left mouse click
480-
let _ = enigo.button(Button::Left, Direction::Click);
481-
// let _ = enigo.button(Button::Left, Direction::Release);
482-
483-
thread::sleep(Duration::from_millis(100));
484-
485-
// Move the mouse cursor back to the original position
486-
let _ = enigo.move_mouse(original_x, original_y, Coordinate::Abs);
487-
}
488-
}
489-
}
490-
}
491-
}
492-
}
493-
494-
#[cfg(not(target_os = "windows"))]
495-
{
496-
let _ = window;
497-
let _ = is_chat_mode;
498-
}
499-
}
500-
501454
/// Log format:
502455
///
503456
/// ```text

src/components/Search/InputBox.tsx

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import InputControls from "./InputControls";
1818
import { useExtensionsStore } from "@/stores/extensionsStore";
1919
import AudioRecording from "../AudioRecording";
2020
import { isDefaultServer } from "@/utils";
21+
import { useTauriFocus } from "@/hooks/useTauriFocus";
2122

2223
interface ChatInputProps {
2324
onSend: (message: string) => void;
@@ -99,18 +100,12 @@ export default function ChatInput({
99100
const { setSearchValue, visibleExtensionStore, selectedExtension } =
100101
useSearchStore();
101102

102-
useEffect(() => {
103-
const handleFocus = () => {
103+
useTauriFocus({
104+
onFocus() {
104105
setBlurred(false);
105106
setModifierKeyPressed(false);
106-
};
107-
108-
window.addEventListener("focus", handleFocus);
109-
110-
return () => {
111-
window.removeEventListener("focus", handleFocus);
112-
};
113-
}, []);
107+
},
108+
});
114109

115110
const handleToggleFocus = useCallback(() => {
116111
textareaRef.current?.focus();

src/components/SearchChat/index.tsx

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,6 @@ function SearchChat({
118118
const isWin10 = await platformAdapter.isWindows10();
119119

120120
setIsWin10(isWin10);
121-
122-
const unlisten = platformAdapter.listenEvent("show-coco", () => {
123-
//console.log("show-coco");
124-
125-
platformAdapter.invokeBackend("simulate_mouse_click", {
126-
isChatMode: isChatModeRef.current,
127-
});
128-
});
129-
130-
return () => {
131-
// Cleanup logic if needed
132-
unlisten.then((fn) => fn());
133-
};
134121
});
135122

136123
useEffect(() => {

src/hooks/useTauriFocus.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useRef } from "react";
2+
import { debounce, noop } from "lodash-es";
3+
import { useMount, useUnmount } from "ahooks";
4+
5+
import { useAppStore } from "@/stores/appStore";
6+
import { isMac } from "@/utils/platform";
7+
import platformAdapter from "@/utils/platformAdapter";
8+
9+
interface Props {
10+
onFocus?: () => void;
11+
onBlur?: () => void;
12+
}
13+
14+
export const useTauriFocus = (props: Props) => {
15+
const { onFocus, onBlur } = props;
16+
const { isTauri } = useAppStore();
17+
const unlistenRef = useRef(noop);
18+
19+
useMount(async () => {
20+
if (!isTauri) return;
21+
22+
const appWindow = await platformAdapter.getWebviewWindow();
23+
24+
const wait = isMac ? 0 : 100;
25+
26+
const debounced = debounce(({ payload }) => {
27+
if (payload) {
28+
console.log("Window focused");
29+
30+
onFocus?.();
31+
} else {
32+
console.log("Window blurred");
33+
34+
onBlur?.();
35+
}
36+
}, wait);
37+
38+
unlistenRef.current = await appWindow.onFocusChanged(debounced);
39+
});
40+
41+
useUnmount(() => {
42+
unlistenRef.current();
43+
});
44+
};

src/hooks/useWindowEvents.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,21 @@
1-
import { useEffect } from "react";
2-
31
import { useAppStore } from "@/stores/appStore";
42
import platformAdapter from "@/utils/platformAdapter";
3+
import { useTauriFocus } from "./useTauriFocus";
54

65
export function useWindowEvents() {
76
const isPinned = useAppStore((state) => state.isPinned);
87
const visible = useAppStore((state) => state.visible);
98
const setBlurred = useAppStore((state) => state.setBlurred);
109

11-
useEffect(() => {
12-
const handleBlur = async () => {
13-
console.log("Window blurred");
10+
useTauriFocus({
11+
onBlur() {
1412
if (isPinned || visible) {
1513
return setBlurred(true);
1614
}
1715

18-
await platformAdapter.hideWindow();
19-
console.log("Hide Coco");
20-
};
21-
22-
window.addEventListener("blur", handleBlur);
16+
platformAdapter.hideWindow();
2317

24-
// Clean up event listeners on component unmount
25-
return () => {
26-
window.removeEventListener("blur", handleBlur);
27-
};
28-
}, [isPinned, visible]);
18+
console.log("Hide Coco");
19+
},
20+
});
2921
}

src/utils/tauriAdapter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,14 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
8484
);
8585
return requestScreenRecordingPermission();
8686
},
87-
87+
8888
async checkMicrophonePermission() {
8989
const { checkMicrophonePermission } = await import(
9090
"tauri-plugin-macos-permissions-api"
9191
);
9292
return checkMicrophonePermission();
9393
},
94-
94+
9595
async requestMicrophonePermission() {
9696
const { requestMicrophonePermission } = await import(
9797
"tauri-plugin-macos-permissions-api"

src/utils/wrappers/tauriWrappers.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
import * as commands from '@/commands';
1+
import * as commands from "@/commands";
22

33
// Window operations
44
export const windowWrapper = {
55
async getCurrentWindow() {
66
const { getCurrentWindow } = await import("@tauri-apps/api/window");
77
return getCurrentWindow();
88
},
9-
9+
1010
async getWebviewWindow() {
11-
const { getCurrentWebviewWindow } = await import("@tauri-apps/api/webviewWindow");
11+
const { getCurrentWebviewWindow } = await import(
12+
"@tauri-apps/api/webviewWindow"
13+
);
1214
return getCurrentWebviewWindow();
1315
},
14-
16+
1517
async setSize(width: number, height: number) {
1618
const { LogicalSize } = await import("@tauri-apps/api/dpi");
1719
const window = await this.getWebviewWindow();
@@ -27,7 +29,7 @@ export const eventWrapper = {
2729
const { emit } = await import("@tauri-apps/api/event");
2830
return emit(event, payload);
2931
},
30-
32+
3133
async listen(event: string, callback: Function) {
3234
const { listen } = await import("@tauri-apps/api/event");
3335
return listen(event, (e) => callback(e));
@@ -37,16 +39,22 @@ export const eventWrapper = {
3739
// System functions
3840
export const systemWrapper = {
3941
async checkScreenPermission() {
40-
const { checkScreenRecordingPermission } = await import("tauri-plugin-macos-permissions-api");
42+
const { checkScreenRecordingPermission } = await import(
43+
"tauri-plugin-macos-permissions-api"
44+
);
4145
return checkScreenRecordingPermission();
4246
},
43-
44-
async captureScreen(id: number, type: 'monitor' | 'window') {
45-
if (type === 'monitor') {
46-
const { getMonitorScreenshot } = await import("tauri-plugin-screenshots-api");
47+
48+
async captureScreen(id: number, type: "monitor" | "window") {
49+
if (type === "monitor") {
50+
const { getMonitorScreenshot } = await import(
51+
"tauri-plugin-screenshots-api"
52+
);
4753
return getMonitorScreenshot(id);
4854
} else {
49-
const { getWindowScreenshot } = await import("tauri-plugin-screenshots-api");
55+
const { getWindowScreenshot } = await import(
56+
"tauri-plugin-screenshots-api"
57+
);
5058
return getWindowScreenshot(id);
5159
}
5260
},
@@ -60,5 +68,5 @@ export const commandWrapper = {
6068
return (commands as any)[commandName](...args);
6169
}
6270
throw new Error(`Command ${commandName} not found`);
63-
}
64-
};
71+
},
72+
};

0 commit comments

Comments
 (0)