Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 52 additions & 26 deletions src-tauri/src/core/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use rdev::{Event, EventType, listen};
use serde::Serialize;
use serde_json::{Value, json};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use tauri::{AppHandle, Emitter, Runtime, command};

#[derive(Debug, Clone, Serialize)]
Expand Down Expand Up @@ -29,35 +31,59 @@ pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Res

IS_LISTENING.store(true, Ordering::SeqCst);

let callback = move |event: Event| {
let device_event = match event.event_type {
EventType::ButtonPress(button) => DeviceEvent {
kind: DeviceEventKind::MousePress,
value: json!(format!("{:?}", button)),
},
EventType::ButtonRelease(button) => DeviceEvent {
kind: DeviceEventKind::MouseRelease,
value: json!(format!("{:?}", button)),
},
EventType::MouseMove { x, y } => DeviceEvent {
kind: DeviceEventKind::MouseMove,
value: json!({ "x": x, "y": y }),
},
EventType::KeyPress(key) => DeviceEvent {
kind: DeviceEventKind::KeyboardPress,
value: json!(format!("{:?}", key)),
},
EventType::KeyRelease(key) => DeviceEvent {
kind: DeviceEventKind::KeyboardRelease,
value: json!(format!("{:?}", key)),
},
_ => return,
// 在新线程中运行监听事件,避免阻塞主线程
std::thread::spawn(move || {
let last_mouse_move_time = Arc::new(Mutex::new(Instant::now()));

let throttle_duration = Duration::from_millis(30);

let callback = move |event: Event| {
let device_event = match event.event_type {
EventType::ButtonPress(button) => DeviceEvent {
kind: DeviceEventKind::MousePress,
value: json!(format!("{:?}", button)),
},
EventType::ButtonRelease(button) => DeviceEvent {
kind: DeviceEventKind::MouseRelease,
value: json!(format!("{:?}", button)),
},
EventType::MouseMove { x, y } => {
// --- 节流 ---
let mut last_time = last_mouse_move_time.lock().unwrap();
if last_time.elapsed() < throttle_duration {
// 如果距离上次发送时间太短,直接忽略此次事件
return;
}
// 更新最后发送时间
*last_time = Instant::now();
// --- 鼠标移动节流逻辑结束 ---

DeviceEvent {
kind: DeviceEventKind::MouseMove,
value: json!({ "x": x, "y": y }),
}
}
EventType::KeyPress(key) => DeviceEvent {
kind: DeviceEventKind::KeyboardPress,
value: json!(format!("{:?}", key)),
},
EventType::KeyRelease(key) => DeviceEvent {
kind: DeviceEventKind::KeyboardRelease,
value: json!(format!("{:?}", key)),
},
_ => return,
};

let _ = app_handle.emit("device-changed", device_event);
};

let _ = app_handle.emit("device-changed", device_event);
};
// 在 thread::spawn 中 开始监听
if let Err(error) = listen(callback) {
eprintln!("Error: {:?}", error);

listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?;
IS_LISTENING.store(false, Ordering::SeqCst);
}
});

Ok(())
}
38 changes: 37 additions & 1 deletion src/composables/useDevice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { PhysicalPosition } from '@tauri-apps/api/dpi'

import { invoke } from '@tauri-apps/api/core'
import { PhysicalPosition as PhysicalPositionImpl } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition } from '@tauri-apps/api/window'

Expand All @@ -8,6 +11,7 @@ import { useModel } from './useModel'
import { useTauriListen } from './useTauriListen'

import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general'
import { useModelStore } from '@/stores/model'
import { inBetween } from '@/utils/is'
import { isWindows } from '@/utils/platform'
Expand Down Expand Up @@ -38,8 +42,13 @@ export function useDevice() {
const modelStore = useModelStore()
const releaseTimers = new Map<string, NodeJS.Timeout>()
const catStore = useCatStore()
const generalStore = useGeneralStore()
const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()

let lastMouseMoveTime = 0
let pendingMove = false
let pendingMovePosition: PhysicalPosition | null = null

const startListening = () => {
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
}
Expand All @@ -64,7 +73,22 @@ export function useDevice() {
}

const handleCursorMove = async () => {
const cursorPoint = await cursorPosition()
const now = Date.now()
const throttleMs = generalStore.performance.mouseMoveThrottle

// 前端节流处理
if (throttleMs > 0 && now - lastMouseMoveTime < throttleMs) {
if (generalStore.performance.mouseMoveThrottleOptimize) {
pendingMove = true
}
return
}

lastMouseMoveTime = now

const cursorPoint = pendingMovePosition || await cursorPosition()
pendingMovePosition = null
pendingMove = false

handleMouseMove(cursorPoint)

Expand All @@ -82,6 +106,14 @@ export function useDevice() {
appWindow.setIgnoreCursorEvents(isInWindow)
}
}

// 有挂起的移动事件 延迟到下一次处理
if (pendingMove && throttleMs > 0) {
const remainingTime = throttleMs - (Date.now() - lastMouseMoveTime)
setTimeout(() => {
handleCursorMove()
}, Math.max(0, remainingTime))
}
}

const handleAutoRelease = (key: string, delay = 100) => {
Expand Down Expand Up @@ -131,6 +163,10 @@ export function useDevice() {
case 'MouseRelease':
return handleMouseChange(value, false)
case 'MouseMove':
// 缓存鼠标位置用于节流优化
if (generalStore.performance.mouseMoveThrottleOptimize) {
pendingMovePosition = new PhysicalPositionImpl(value.x, value.y)
}
return handleCursorMove()
}
})
Expand Down
9 changes: 7 additions & 2 deletions src/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
"updateSettings": "Update Settings",
"autoCheckUpdate": "Auto Check for Updates",
"permissionsSettings": "Permissions Settings",
"inputMonitoringPermission": "Input Monitoring Permission"
"inputMonitoringPermission": "Input Monitoring Permission",
"performanceSettings": "Performance Settings",
"mouseMoveThrottle": "Mouse Move Throttle",
"mouseMoveThrottleOptimize": "Throttle Optimization"
},
"options": {
"auto": "System",
Expand All @@ -64,7 +67,9 @@
"hints": {
"showTaskbarIcon": "When enabled, the window can be captured via OBS Studio.",
"inputMonitoringPermission": "Enable input monitoring to receive keyboard and mouse events from the system.",
"inputMonitoringPermissionGuide": "If the permission is already enabled, select it and click the \"-\" button to remove it, then manually add it again and restart the app."
"inputMonitoringPermissionGuide": "If the permission is already enabled, select it and click the \"-\" button to remove it, then manually add it again and restart the app.",
"mouseMoveThrottle": "Controls the frequency of frontend mouse move event processing in milliseconds. Lower values respond faster but consume more performance. Recommended range 8-33ms (approx. 30-120fps). Set to 0 for no limit.",
"mouseMoveThrottleOptimize": "When enabled, the app will cache and optimize mouse position updates during throttling to reduce processing overhead while maintaining smooth movement."
},
"status": {
"authorized": "Authorized",
Expand Down
9 changes: 7 additions & 2 deletions src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
"updateSettings": "Configurações de atualização",
"autoCheckUpdate": "Verificar atualizações automaticamente",
"permissionsSettings": "Configurações de Permissões",
"inputMonitoringPermission": "Permissão de Monitoramento de Entrada"
"inputMonitoringPermission": "Permissão de Monitoramento de Entrada",
"performanceSettings": "Configurações de Desempenho",
"mouseMoveThrottle": "Limitação de Movimento do Mouse",
"mouseMoveThrottleOptimize": "Otimização de Limitação"
},
"options": {
"auto": "Sistema",
Expand All @@ -64,7 +67,9 @@
"hints": {
"showTaskbarIcon": "Uma vez ativado, você pode capturar a janela via OBS Studio.",
"inputMonitoringPermission": "Ative a permissão de monitoramento de entrada para receber eventos de teclado e mouse do sistema para responder às suas ações.",
"inputMonitoringPermissionGuide": "Se a permissão já estiver ativada, primeiro selecione-a e clique no botão \"-\" para removê-la. Em seguida, adicione-a novamente manualmente e reinicie o aplicativo para garantir que a permissão entre em vigor."
"inputMonitoringPermissionGuide": "Se a permissão já estiver ativada, primeiro selecione-a e clique no botão \"-\" para removê-la. Em seguida, adicione-a novamente manualmente e reinicie o aplicativo para garantir que a permissão entre em vigor.",
"mouseMoveThrottle": "Controla a frequência de processamento de eventos de movimento do mouse no frontend em milissegundos. Valores menores respondem mais rápido, mas consomem mais desempenho. Faixa recomendada 8-33ms (aprox. 30-120fps). Defina como 0 para sem limite.",
"mouseMoveThrottleOptimize": "Quando ativado, o aplicativo armazenará em cache e otimizará as atualizações de posição do mouse durante a limitação, reduzindo a sobrecarga de processamento enquanto mantém um movimento suave."
},
"status": {
"authorized": "Autorizado",
Expand Down
9 changes: 7 additions & 2 deletions src/locales/vi-VN.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
"updateSettings": "Cài đặt cập nhật",
"autoCheckUpdate": "Tự động kiểm tra cập nhật",
"permissionsSettings": "Cài đặt quyền",
"inputMonitoringPermission": "Quyền giám sát đầu vào"
"inputMonitoringPermission": "Quyền giám sát đầu vào",
"performanceSettings": "Cài đặt hiệu suất",
"mouseMoveThrottle": "Giới hạn di chuyển chuột",
"mouseMoveThrottleOptimize": "Tối ưu giới hạn"
},
"options": {
"auto": "Theo hệ thống",
Expand All @@ -64,7 +67,9 @@
"hints": {
"showTaskbarIcon": "Bật để có thể quay cửa sổ qua OBS.",
"inputMonitoringPermission": "Bật quyền giám sát để nhận sự kiện bàn phím và chuột từ hệ thống nhằm phản hồi thao tác của bạn.",
"inputMonitoringPermissionGuide": "Nếu quyền đã được bật, hãy chọn nó và nhấn nút \"-\" để xóa. Sau đó thêm lại thủ công và khởi động lại ứng dụng để đảm bảo quyền được áp dụng."
"inputMonitoringPermissionGuide": "Nếu quyền đã được bật, hãy chọn nó và nhấn nút \"-\" để xóa. Sau đó thêm lại thủ công và khởi động lại ứng dụng để đảm bảo quyền được áp dụng.",
"mouseMoveThrottle": "Kiểm soát tần suất xử lý sự kiện di chuyển chuột ở frontend theo mili giây. Giá trị thấp hơn phản hồi nhanh hơn nhưng tiêu tốn hiệu suất cao hơn. Khuyến nghị 8-33ms (khoảng 30-120fps). Đặt 0 để không giới hạn.",
"mouseMoveThrottleOptimize": "Khi bật, ứng dụng sẽ lưu vào bộ nhớ cache và tối ưu hóa các cập nhật vị trí chuột trong quá trình giới hạn, giảm chi phí xử lý trong khi vẫn giữ được chuyển động mượt mà."
},
"status": {
"authorized": "Đã cấp quyền",
Expand Down
9 changes: 7 additions & 2 deletions src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@
"updateSettings": "更新设置",
"autoCheckUpdate": "自动检查更新",
"permissionsSettings": "权限设置",
"inputMonitoringPermission": "输入监控权限"
"inputMonitoringPermission": "输入监控权限",
"performanceSettings": "性能设置",
"mouseMoveThrottle": "鼠标移动节流",
"mouseMoveThrottleOptimize": "节流优化"
},
"options": {
"auto": "跟随系统",
Expand All @@ -64,7 +67,9 @@
"hints": {
"showTaskbarIcon": "启用后,即可通过 OBS Studio 捕获窗口。",
"inputMonitoringPermission": "开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。",
"inputMonitoringPermissionGuide": "如果权限已开启,请先选中并点击“-”按钮将其删除,然后重新手动添加,最后重启应用以确保权限生效。"
"inputMonitoringPermissionGuide": "如果权限已开启,请先选中并点击\"-\"按钮将其删除,然后重新手动添加,最后重启应用以确保权限生效。",
"mouseMoveThrottle": "控制前端处理鼠标移动事件的频率,单位为毫秒。值越小响应越快但性能消耗越高,推荐范围 8-33ms(约 30-120fps)。设置为 0 表示不限制。",
"mouseMoveThrottleOptimize": "启用后,应用将在节流期间缓存并优化鼠标位置更新,在保持平滑移动的同时减少处理开销。"
},
"status": {
"authorized": "已授权",
Expand Down
38 changes: 36 additions & 2 deletions src/pages/preference/components/general/index.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
import { Select, Switch } from 'ant-design-vue'
import { watch } from 'vue'
import { InputNumber, Select, Switch } from 'ant-design-vue'
import { computed, watch } from 'vue'

import MacosPermissions from './components/macos-permissions/index.vue'
import ThemeMode from './components/theme-mode/index.vue'
Expand All @@ -23,6 +23,14 @@ watch(() => generalStore.app.autostart, async (value) => {
disable()
}
}, { immediate: true })

// 计算推荐的 FPS 值
const recommendedFps = computed(() => {
const throttleMs = generalStore.performance.mouseMoveThrottle
if (throttleMs <= 0) return '∞'
const fps = Math.round(1000 / throttleMs)
return `${fps} FPS`
})
</script>

<template>
Expand Down Expand Up @@ -67,4 +75,30 @@ watch(() => generalStore.app.autostart, async (value) => {
<Switch v-model:checked="generalStore.update.autoCheck" />
</ProListItem>
</ProList>

<ProList :title="$t('pages.preference.general.labels.performanceSettings')">
<ProListItem
:description="$t('pages.preference.general.hints.mouseMoveThrottle')"
:title="$t('pages.preference.general.labels.mouseMoveThrottle')"
>
<InputNumber
v-model:value="generalStore.performance.mouseMoveThrottle"
:max="100"
:min="0"
:step="1"
style="width: 150px"
>
<template #addonAfter>
ms ({{ recommendedFps }})
</template>
</InputNumber>
</ProListItem>

<ProListItem
:description="$t('pages.preference.general.hints.mouseMoveThrottleOptimize')"
:title="$t('pages.preference.general.labels.mouseMoveThrottleOptimize')"
>
<Switch v-model:checked="generalStore.performance.mouseMoveThrottleOptimize" />
</ProListItem>
</ProList>
</template>
10 changes: 10 additions & 0 deletions src/stores/general.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface GeneralStore {
update: {
autoCheck: boolean
}
performance: {
mouseMoveThrottle: number
mouseMoveThrottleOptimize: boolean
}
}

export const useGeneralStore = defineStore('general', () => {
Expand Down Expand Up @@ -58,6 +62,11 @@ export const useGeneralStore = defineStore('general', () => {
autoCheck: false,
})

const performance = reactive<GeneralStore['performance']>({
mouseMoveThrottle: 16,
mouseMoveThrottleOptimize: true,
})

const getLanguage = async () => {
const locale = await getLocale<Language>()

Expand Down Expand Up @@ -89,6 +98,7 @@ export const useGeneralStore = defineStore('general', () => {
app,
appearance,
update,
performance,
init,
}
})
Loading