From 6db32174bd5ece2c2b405d9b02f2ef835291fcd6 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:44:07 -0600 Subject: [PATCH 01/24] fix(zoom): improve error handling and fallback for zoom backend initialization --- src-tauri/src/lib.rs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a6ef484..64b6aef 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,28 +17,33 @@ struct ZoomConfig { static ZOOM_BACKEND_STATE: OnceLock> = OnceLock::new(); +const DEFAULT_ZOOM_BACKEND: &str = "dxgi"; + +fn init_zoom_backend_state() -> RwLock { + let json_str = include_str!("../../config/zoom.json"); + let backend = serde_json::from_str::(json_str) + .map(|config| config.backend) + .unwrap_or_else(|e| { + eprintln!("Failed to parse zoom.json, falling back to default backend: {}", e); + DEFAULT_ZOOM_BACKEND.to_string() + }); + RwLock::new(backend) +} + pub fn get_zoom_backend() -> String { - let state = ZOOM_BACKEND_STATE.get_or_init(|| { - let json_str = include_str!("../../config/zoom.json"); - let config: ZoomConfig = serde_json::from_str(json_str).expect("Failed to parse zoom.json"); - RwLock::new(config.backend) - }); + let state = ZOOM_BACKEND_STATE.get_or_init(init_zoom_backend_state); match state.read() { Ok(guard) => guard.clone(), Err(e) => { eprintln!("Failed to read zoom backend state: {}", e); - "dxgi".to_string() + DEFAULT_ZOOM_BACKEND.to_string() } } } pub fn set_zoom_backend_internal(new_backend: String) { - let state = ZOOM_BACKEND_STATE.get_or_init(|| { - let json_str = include_str!("../../config/zoom.json"); - let config: ZoomConfig = serde_json::from_str(json_str).expect("Failed to parse zoom.json"); - RwLock::new(config.backend) - }); + let state = ZOOM_BACKEND_STATE.get_or_init(init_zoom_backend_state); if let Ok(mut guard) = state.write() { *guard = new_backend; From f72531853c386f6df50b9f92d22814924ca74665 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:44:28 -0600 Subject: [PATCH 02/24] fix(dxgi): implement UnmapGuard for safe D3D11 resource unmapping --- src-tauri/src/dxgi_capture.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/dxgi_capture.rs b/src-tauri/src/dxgi_capture.rs index c77d99a..f4dd3b8 100644 --- a/src-tauri/src/dxgi_capture.rs +++ b/src-tauri/src/dxgi_capture.rs @@ -9,6 +9,22 @@ use windows::Win32::Graphics::Dxgi::*; const DXGI_ERROR_WAIT_TIMEOUT: i32 = 0x887A0027u32 as i32; const DXGI_ERROR_ACCESS_LOST: i32 = 0x887A0026u32 as i32; +/** + * RAII guard that unmaps a mapped D3D11 subresource on drop, including on panic/unwind. + */ +struct UnmapGuard<'a> { + context: &'a ID3D11DeviceContext, + resource: &'a ID3D11Texture2D, +} + +impl Drop for UnmapGuard<'_> { + fn drop(&mut self) { + unsafe { + self.context.Unmap(self.resource, 0); + } + } +} + pub struct DxgiDuplicator { device: ID3D11Device, context: ID3D11DeviceContext, @@ -270,6 +286,10 @@ impl DxgiDuplicator { self.context .Map(staging, 0, D3D11_MAP_READ, 0, Some(&mut mapped)) .map_err(|e| format!("Map staging: {e}"))?; + let _unmap_guard = UnmapGuard { + context: &self.context, + resource: staging, + }; let row_pitch = mapped.RowPitch as usize; let pixel_count = (region_w * region_h * 4) as usize; let mut rgba = Vec::with_capacity(pixel_count); @@ -297,7 +317,7 @@ impl DxgiDuplicator { } } - self.context.Unmap(staging, 0); + drop(_unmap_guard); Ok((rgba, region_w, region_h)) } From 279d110d8c15cd5362a0b8fff6c518574fc57637 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:49:18 -0600 Subject: [PATCH 03/24] fix(window): implement mode locking for safe window rebuild and visibility handling --- src-tauri/src/types.rs | 22 ++++++++++++++- src-tauri/src/window.rs | 60 ++++++++++++++++++++++------------------- 2 files changed, 53 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index b06339f..929e5b9 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; use std::sync::atomic::AtomicBool; -use std::sync::{Arc, RwLock}; +use std::sync::{Arc, Mutex, RwLock}; use tauri::async_runtime::JoinHandle; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -46,15 +46,35 @@ pub struct MonitorContext { pub struct WindowRegistry { pub mode_windows: RwLock>>, pub current_snapshot: RwLock, + mode_locks: HashMap>, } impl WindowRegistry { pub fn new() -> Self { + let mut mode_locks = HashMap::new(); + for mode in [Mode::Overlay, Mode::Spotlight, Mode::Highlight, Mode::Zoom] { + mode_locks.insert(mode, Mutex::new(())); + } + Self { mode_windows: RwLock::new(HashMap::new()), current_snapshot: RwLock::new(String::new()), + mode_locks, } } + + /// Serializes the destroy→build→show sequence for a given [`Mode`] so + /// concurrent callers (monitor-change loop vs. visibility requests) + /// cannot interleave window mutations for the same mode. + pub fn with_mode_lock(&self, mode: Mode, f: impl FnOnce() -> T) -> T { + let _guard = self + .mode_locks + .get(&mode) + .expect("mode_locks initialized for every Mode variant") + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + f() + } } #[derive(serde::Serialize, Clone)] diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs index 0984d9a..da610c5 100644 --- a/src-tauri/src/window.rs +++ b/src-tauri/src/window.rs @@ -312,12 +312,14 @@ pub fn rebuild_mode_windows( monitors: &[MonitorContext], ) -> Result<(), String> { let monitors = monitors.to_vec(); - if let Err(err) = destroy_mode_windows(app, mode) { - eprintln!("[ERROR] Failed to destroy mode windows {:?}: {}", mode, err); - } - if let Err(err) = build_mode_windows_inner(app, mode, &monitors, true) { - eprintln!("[ERROR] Failed to rebuild mode windows {:?}: {}", mode, err); - } + window_registry().with_mode_lock(mode, || { + if let Err(err) = destroy_mode_windows(app, mode) { + eprintln!("[ERROR] Failed to destroy mode windows {:?}: {}", mode, err); + } + if let Err(err) = build_mode_windows_inner(app, mode, &monitors, true) { + eprintln!("[ERROR] Failed to rebuild mode windows {:?}: {}", mode, err); + } + }); Ok(()) } @@ -443,34 +445,36 @@ pub fn handle_mode_visibility(app: &AppHandle, mode: Mode, visible: bool) -> Res } } - let mut reused_existing = false; - if mode_windows_ready(app, mode) { - let should_show = mode != Mode::Zoom; - if let Err(err) = show_mode_windows_inner(app, mode, should_show) { - eprintln!( - "[WARN] Failed to show existing windows for {:?}: {}", - mode, err - ); - } else { - reused_existing = true; + window_registry().with_mode_lock(mode, || { + let mut reused_existing = false; + if mode_windows_ready(app, mode) { + let should_show = mode != Mode::Zoom; + if let Err(err) = show_mode_windows_inner(app, mode, should_show) { + eprintln!( + "[WARN] Failed to show existing windows for {:?}: {}", + mode, err + ); + } else { + reused_existing = true; + } } - } - if !reused_existing { - match detect_monitors(app) { - Ok(monitors) => { - if let Err(err) = destroy_mode_windows(app, mode) { - eprintln!("[ERROR] Failed to destroy windows for {:?}: {}", mode, err); + if !reused_existing { + match detect_monitors(app) { + Ok(monitors) => { + if let Err(err) = destroy_mode_windows(app, mode) { + eprintln!("[ERROR] Failed to destroy windows for {:?}: {}", mode, err); + } + if let Err(err) = build_mode_windows_inner(app, mode, &monitors, true) { + eprintln!("[ERROR] Failed to build windows for {:?}: {}", mode, err); + } } - if let Err(err) = build_mode_windows_inner(app, mode, &monitors, true) { - eprintln!("[ERROR] Failed to build windows for {:?}: {}", mode, err); + Err(err) => { + eprintln!("[ERROR] Failed to detect monitors: {}", err); } } - Err(err) => { - eprintln!("[ERROR] Failed to detect monitors: {}", err); - } } - } + }); } else if mode != Mode::Zoom { hide_mode_windows_inner(app, mode); } From eaad77629efecc4b5b2fa558408b3d766a62194f Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:53:42 -0600 Subject: [PATCH 04/24] fix(zoom): convert stop_zoom_stream and freeze_zoom to async for improved handling --- src-tauri/src/commands.rs | 4 ++-- src-tauri/src/zoom.rs | 18 ++++++++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 2e5d8d8..6816f49 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -165,8 +165,8 @@ pub fn set_zoom_capture_excluded(excluded: bool) -> Result<(), String> { } #[tauri::command] -pub fn freeze_zoom() -> Result<(), String> { - crate::zoom::stop_zoom_stream()?; +pub async fn freeze_zoom() -> Result<(), String> { + crate::zoom::stop_zoom_stream().await?; #[cfg(target_os = "windows")] { crate::window::set_zoom_capture_exclusion(false); diff --git a/src-tauri/src/zoom.rs b/src-tauri/src/zoom.rs index 1ac19e1..ca37250 100644 --- a/src-tauri/src/zoom.rs +++ b/src-tauri/src/zoom.rs @@ -351,11 +351,25 @@ pub fn set_zoom_stream_config(size: u32, zoom_level: f32) -> Result<(), String> } #[tauri::command] -pub fn stop_zoom_stream() -> Result<(), String> { +pub async fn stop_zoom_stream() -> Result<(), String> { let handle_store = zoom_stream_handle(); if let Some(existing) = handle_store.swap(None) { existing.stop.store(true, Ordering::Relaxed); - existing.task.abort(); + let ZoomStreamHandle { mut task, .. } = match Arc::try_unwrap(existing) { + Ok(handle) => handle, + Err(shared) => { + shared.task.abort(); + return Ok(()); + } + }; + if tokio::time::timeout(Duration::from_secs(3), &mut task) + .await + .is_err() + { + // Loop task did not wind down in time (e.g. stuck DXGI capture + // holding CACHED_DUPLICATOR); abort as a last resort. + task.abort(); + } } Ok(()) } From a244e16e95b49b860ec410c7076465f3f9d406f1 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:55:52 -0600 Subject: [PATCH 05/24] fix(magnifier): add error handling for SetWindowRgn in set_circle function --- src-tauri/src/magnifier.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/magnifier.rs b/src-tauri/src/magnifier.rs index d09e1ef..0983a26 100644 --- a/src-tauri/src/magnifier.rs +++ b/src-tauri/src/magnifier.rs @@ -309,7 +309,9 @@ unsafe fn apply_transform(mag: HWND, zoom: f32) { unsafe fn set_circle(hwnd: HWND, size: u32) { let rgn = CreateEllipticRgn(0, 0, size as i32, size as i32); - SetWindowRgn(hwnd, Some(rgn), true); + if SetWindowRgn(hwnd, Some(rgn), true) == 0 { + let _ = DeleteObject(rgn.into()); + } } unsafe fn set_square(hwnd: HWND) { From 1f7e709e93ed18f26714b1151d62dc68ded2524b Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 16:58:16 -0600 Subject: [PATCH 06/24] fix(canvas): clear autoErase timers and maps on cleanup --- src/composables/useCanvasDrawing.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/composables/useCanvasDrawing.ts b/src/composables/useCanvasDrawing.ts index f772412..76f3763 100644 --- a/src/composables/useCanvasDrawing.ts +++ b/src/composables/useCanvasDrawing.ts @@ -725,6 +725,9 @@ export function useCanvasDrawing(options: { cancelAnimationFrame(autoEraseRaf); autoEraseRaf = null; } + autoEraseTimers.forEach((timer) => clearTimeout(timer)); + autoEraseTimers.clear(); + autoEraseFadeMap.clear(); }); watch(options.clearNonce, () => { From 1d235e357385343eb8aa28aa297f6f35d5eb3c20 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:00:40 -0600 Subject: [PATCH 07/24] fix(monitor): use scaleFactor from monitorContext for coordinate transformations --- src/composables/useMonitorContext.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/composables/useMonitorContext.ts b/src/composables/useMonitorContext.ts index 8543a18..41c1e59 100644 --- a/src/composables/useMonitorContext.ts +++ b/src/composables/useMonitorContext.ts @@ -83,8 +83,8 @@ export function useMonitorContext() { return { x: globalX, y: globalY }; } - const { virtualX, virtualY } = monitorContext.value; - const scale = window.devicePixelRatio || 1.0; + const { virtualX, virtualY, scaleFactor } = monitorContext.value; + const scale = scaleFactor || 1.0; return { x: (globalX - virtualX) / scale, @@ -97,8 +97,8 @@ export function useMonitorContext() { return { x: localX, y: localY }; } - const { virtualX, virtualY } = monitorContext.value; - const scale = window.devicePixelRatio || 1.0; + const { virtualX, virtualY, scaleFactor } = monitorContext.value; + const scale = scaleFactor || 1.0; return { x: localX * scale + virtualX, From 2ee163bc17a18985c7de1112e4e09a8c6a37b11f Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:06:40 -0600 Subject: [PATCH 08/24] fix(zoom): clamp zoom region size to MAX_ZOOM_REGION in capture_zoom_region_raw_sync --- src-tauri/src/zoom.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/zoom.rs b/src-tauri/src/zoom.rs index ca37250..717af27 100644 --- a/src-tauri/src/zoom.rs +++ b/src-tauri/src/zoom.rs @@ -12,6 +12,8 @@ use tokio::time::sleep; use windows::Win32::Foundation::POINT; use windows::Win32::UI::WindowsAndMessaging::GetCursorPos; +const MAX_ZOOM_REGION: i32 = 8192; + static ZOOM_STREAM: OnceLock> = OnceLock::new(); pub fn zoom_stream_handle() -> &'static ArcSwapOption { @@ -83,7 +85,9 @@ fn capture_zoom_region_raw_sync( let origin_x = dup.origin_x; let origin_y = dup.origin_y; let level = zoom_level.max(1.0); - let region = ((size as f32) / level).round().max(1.0) as i32; + let region = ((size as f32) / level) + .round() + .clamp(1.0, MAX_ZOOM_REGION as f32) as i32; let half = region / 2; let local_x = cursor_x - origin_x - half; let local_y = cursor_y - origin_y - half; From 0f85f67a1dc5a942d174ae55b3497df4b54066bc Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:12:47 -0600 Subject: [PATCH 09/24] fix(zoom): clear zoom window handle on mode destruction and validate window existence --- src-tauri/src/window.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/window.rs b/src-tauri/src/window.rs index da610c5..0a58d4e 100644 --- a/src-tauri/src/window.rs +++ b/src-tauri/src/window.rs @@ -19,6 +19,12 @@ pub fn destroy_mode_windows(app: &AppHandle, mode: Mode) -> Result<(), String> { } mode_windows.remove(&mode); + + #[cfg(target_os = "windows")] + if mode == Mode::Zoom { + clear_zoom_hwnd(); + } + Ok(()) } @@ -513,7 +519,7 @@ pub fn apply_zoom_capture_exclusion(window: &tauri::WebviewWindow) { pub fn set_zoom_capture_exclusion(excluded: bool) { use windows::Win32::Foundation::HWND; use windows::Win32::UI::WindowsAndMessaging::{ - SetWindowDisplayAffinity, WINDOW_DISPLAY_AFFINITY, + IsWindow, SetWindowDisplayAffinity, WINDOW_DISPLAY_AFFINITY, }; let ptr = ZOOM_HWND.load(Ordering::Relaxed); @@ -523,6 +529,9 @@ pub fn set_zoom_capture_exclusion(excluded: bool) { let hwnd = HWND(ptr); unsafe { + if !IsWindow(Some(hwnd)).as_bool() { + return; + } if excluded { let _ = SetWindowDisplayAffinity(hwnd, WINDOW_DISPLAY_AFFINITY(0x11)); } else { @@ -530,3 +539,8 @@ pub fn set_zoom_capture_exclusion(excluded: bool) { } } } + +#[cfg(target_os = "windows")] +fn clear_zoom_hwnd() { + ZOOM_HWND.store(std::ptr::null_mut(), Ordering::Relaxed); +} From 1ed0d62dc556314ca369d20bb936b03a50d7ab56 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:13:07 -0600 Subject: [PATCH 10/24] fix(gdi): implement GdiDcGuard for resource management in GDI screenshot capture --- src-tauri/src/zoom.rs | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/zoom.rs b/src-tauri/src/zoom.rs index 717af27..bf25464 100644 --- a/src-tauri/src/zoom.rs +++ b/src-tauri/src/zoom.rs @@ -109,14 +109,35 @@ fn capture_zoom_region_raw_sync( }) } +#[cfg(target_os = "windows")] +struct GdiDcGuard { + hdc_screen: windows::Win32::Graphics::Gdi::HDC, + hdc_mem: windows::Win32::Graphics::Gdi::HDC, + hbm: windows::Win32::Graphics::Gdi::HBITMAP, + hbm_old: windows::Win32::Graphics::Gdi::HGDIOBJ, +} + +#[cfg(target_os = "windows")] +impl Drop for GdiDcGuard { + fn drop(&mut self) { + use windows::Win32::Graphics::Gdi::{DeleteDC, DeleteObject, ReleaseDC, SelectObject}; + unsafe { + SelectObject(self.hdc_mem, self.hbm_old); + let _ = DeleteObject(self.hbm.into()); + let _ = DeleteDC(self.hdc_mem); + let _ = ReleaseDC(None, self.hdc_screen); + } + } +} + #[cfg(target_os = "windows")] fn capture_gdi_screenshot(cursor_x: i32, cursor_y: i32) -> Result<(Vec, u32, u32), String> { use std::mem; use windows::Win32::Foundation::POINT; use windows::Win32::Graphics::Gdi::{ - BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, - GetDIBits, GetMonitorInfoW, MonitorFromPoint, SelectObject, BITMAPINFO, BITMAPINFOHEADER, - BI_RGB, DIB_RGB_COLORS, MONITORINFO, MONITOR_DEFAULTTONEAREST, SRCCOPY, + BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, GetDC, GetDIBits, GetMonitorInfoW, + MonitorFromPoint, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB, DIB_RGB_COLORS, + MONITORINFO, MONITOR_DEFAULTTONEAREST, SRCCOPY, }; unsafe { @@ -138,9 +159,15 @@ fn capture_gdi_screenshot(cursor_x: i32, cursor_y: i32) -> Result<(Vec, u32, let hdc_screen = GetDC(None); let hdc_mem = CreateCompatibleDC(Some(hdc_screen)); let hbm = CreateCompatibleBitmap(hdc_screen, width as i32, height as i32); - let hbm_old = SelectObject(hdc_mem, hbm.into()); + let _dc_guard = GdiDcGuard { + hdc_screen, + hdc_mem, + hbm, + hbm_old, + }; + BitBlt( hdc_mem, 0, @@ -154,8 +181,6 @@ fn capture_gdi_screenshot(cursor_x: i32, cursor_y: i32) -> Result<(Vec, u32, ) .map_err(|e| format!("BitBlt failed: {e}"))?; - SelectObject(hdc_mem, hbm_old); - let mut bmi: BITMAPINFO = mem::zeroed(); bmi.bmiHeader.biSize = mem::size_of::() as u32; bmi.bmiHeader.biWidth = width as i32; @@ -176,10 +201,6 @@ fn capture_gdi_screenshot(cursor_x: i32, cursor_y: i32) -> Result<(Vec, u32, DIB_RGB_COLORS, ); - let _ = DeleteObject(hbm.into()); - let _ = DeleteDC(hdc_mem); - let _ = windows::Win32::Graphics::Gdi::ReleaseDC(None, hdc_screen); - if res == 0 { return Err("GetDIBits failed".to_string()); } From a28121c2a9076011e6d462eb0452220b3b4bbc6e Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:15:32 -0600 Subject: [PATCH 11/24] fix(monitor): manage fallback timer for monitor context retrieval --- src/composables/useMonitorContext.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/composables/useMonitorContext.ts b/src/composables/useMonitorContext.ts index 41c1e59..b963423 100644 --- a/src/composables/useMonitorContext.ts +++ b/src/composables/useMonitorContext.ts @@ -16,6 +16,7 @@ export function useMonitorContext() { const isReady = ref(false); let unlisten: (() => void) | null = null; + let fallbackTimer: ReturnType | null = null; onMounted(async () => { unlisten = await listen("monitor-context", (event) => { @@ -34,7 +35,8 @@ export function useMonitorContext() { monitorContext.value = payload; isReady.value = true; } catch { - setTimeout(() => { + fallbackTimer = setTimeout(() => { + fallbackTimer = null; if (!monitorContext.value) { console.warn( "Monitor context not received from backend, using defaults", @@ -76,6 +78,10 @@ export function useMonitorContext() { if (unlisten) { unlisten(); } + if (fallbackTimer !== null) { + clearTimeout(fallbackTimer); + fallbackTimer = null; + } }); function toLocalCoordinates(globalX: number, globalY: number) { From c0e1e1f86b1cced3535ce7894fe7bf93606160d2 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:20:07 -0600 Subject: [PATCH 12/24] fix(dependencies): add devtools feature to tauri and update CSP settings --- src-tauri/Cargo.toml | 2 +- src-tauri/src/lib.rs | 5 +++++ src-tauri/tauri.conf.json | 5 ++--- src-tauri/tauri.microsoftstore.conf.json | 5 ++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 48ccea8..09f400d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,7 +15,7 @@ crate-type = ["staticlib", "cdylib", "rlib"] tauri-build = { version = "2", features = [] } [dependencies] -tauri = { version = "2", features = ["tray-icon"] } +tauri = { version = "2", features = ["tray-icon", "devtools"] } tauri-plugin-opener = "2" tauri-plugin-global-shortcut = "2" tauri-plugin-store = "2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 64b6aef..c84055e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -78,6 +78,11 @@ pub fn run() { )) .plugin(tauri_plugin_opener::init()) .setup(|app| { + #[cfg(debug_assertions)] + if let Some(window) = app.get_webview_window("main") { + window.open_devtools(); + } + #[cfg(target_os = "windows")] cursor::start_mouse_hook(); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index dd4aab5..9cd3050 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,12 +16,11 @@ "maximized": true, "visible": true, "center": true, - "focus": true, - "devtools": true + "focus": true } ], "security": { - "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost blob: data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval';" + "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost blob: data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';" } }, "bundle": { diff --git a/src-tauri/tauri.microsoftstore.conf.json b/src-tauri/tauri.microsoftstore.conf.json index 08be1fd..248d0ea 100644 --- a/src-tauri/tauri.microsoftstore.conf.json +++ b/src-tauri/tauri.microsoftstore.conf.json @@ -16,12 +16,11 @@ "maximized": true, "visible": true, "center": true, - "focus": true, - "devtools": true + "focus": true } ], "security": { - "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost blob: data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval';" + "csp": "default-src 'self'; img-src 'self' asset: https://asset.localhost blob: data:; font-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self';" } }, "bundle": { From 693cf0607f6900f779d193176f93358a16428896 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:23:38 -0600 Subject: [PATCH 13/24] feat(capabilities): update default capabilities and add mode-windows configuration --- src-tauri/capabilities/default.json | 2 +- src-tauri/capabilities/mode-windows.json | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 src-tauri/capabilities/mode-windows.json diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index b096887..7d19346 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -2,7 +2,7 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main", "overlay", "whiteboard", "cursor-highlight", "spotlight", "zoom"], + "windows": ["main"], "permissions": [ "core:default", "opener:default", diff --git a/src-tauri/capabilities/mode-windows.json b/src-tauri/capabilities/mode-windows.json new file mode 100644 index 0000000..2386f8d --- /dev/null +++ b/src-tauri/capabilities/mode-windows.json @@ -0,0 +1,11 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "mode-windows", + "description": "Minimal capability for overlay mode render windows (no global shortcuts, no autostart)", + "windows": ["overlay", "whiteboard", "cursor-highlight", "spotlight", "zoom"], + "permissions": [ + "core:default", + "opener:default", + "store:default" + ] +} From 463eb79d9cd3dfe9de53c5a1470acdf8019bb650 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:26:02 -0600 Subject: [PATCH 14/24] fix(persistence): implement queuePersist function for overlay and settings stores --- src/stores/overlay.ts | 10 +++++++++- src/stores/settings.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/stores/overlay.ts b/src/stores/overlay.ts index 59d986f..8bbe472 100644 --- a/src/stores/overlay.ts +++ b/src/stores/overlay.ts @@ -54,6 +54,7 @@ export const useOverlayStore = defineStore("overlay", () => { const isHydrating = ref(false); let storeRef: Awaited> | null = null; let persistTimer: number | null = null; + let persistChain: Promise = Promise.resolve(); function snapshotOverlay() { return { @@ -78,6 +79,13 @@ export const useOverlayStore = defineStore("overlay", () => { await store.save(); } + function queuePersist() { + persistChain = persistChain.then(persist).catch((err) => { + console.error("Failed to persist overlay:", err); + }); + return persistChain; + } + function schedulePersist() { if (isHydrating.value) return; if (persistTimer) { @@ -85,7 +93,7 @@ export const useOverlayStore = defineStore("overlay", () => { } persistTimer = window.setTimeout(() => { persistTimer = null; - persist(); + queuePersist(); }, 500); } diff --git a/src/stores/settings.ts b/src/stores/settings.ts index ccb52f5..8e5a12b 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -274,6 +274,7 @@ export const useSettingsStore = defineStore("settings", () => { const isHydrating = ref(false); let storeRef: Awaited> | null = null; let persistTimer: number | null = null; + let persistChain: Promise = Promise.resolve(); function snapshotSettings(): Settings { return { @@ -385,6 +386,13 @@ export const useSettingsStore = defineStore("settings", () => { await store.save(); } + function queuePersist() { + persistChain = persistChain.then(persist).catch((err) => { + console.error("Failed to persist settings:", err); + }); + return persistChain; + } + function schedulePersist() { if (isHydrating.value) return; if (persistTimer) { @@ -392,7 +400,7 @@ export const useSettingsStore = defineStore("settings", () => { } persistTimer = window.setTimeout(() => { persistTimer = null; - persist(); + queuePersist(); }, 500); } From ba6f535fa2ee68a3b0eabad07984316aba4c13cd Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:28:42 -0600 Subject: [PATCH 15/24] fix(cursor): ensure message loop runs correctly in start_mouse_hook function --- src-tauri/src/cursor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/cursor.rs b/src-tauri/src/cursor.rs index 798faf0..49f9a3b 100644 --- a/src-tauri/src/cursor.rs +++ b/src-tauri/src/cursor.rs @@ -49,9 +49,9 @@ pub fn start_mouse_hook() { ); let hook = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), None, 0); - let mut msg = std::mem::zeroed(); - while GetMessageW(&mut msg, None, 0, 0).into() {} if let Ok(hook) = hook { + let mut msg = std::mem::zeroed(); + while GetMessageW(&mut msg, None, 0, 0).into() {} if !hook.0.is_null() { let _ = UnhookWindowsHookEx(hook); } From c3ba11a113c78eb78c9ef596949b4be33d3b44c8 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:38:52 -0600 Subject: [PATCH 16/24] refactor(zoom): remove capture_viewport function and streamline duplicator usage --- src-tauri/src/dxgi_capture.rs | 4 -- src-tauri/src/lib.rs | 1 - src-tauri/src/zoom.rs | 101 ++++++++++++---------------------- 3 files changed, 36 insertions(+), 70 deletions(-) diff --git a/src-tauri/src/dxgi_capture.rs b/src-tauri/src/dxgi_capture.rs index f4dd3b8..cd497ad 100644 --- a/src-tauri/src/dxgi_capture.rs +++ b/src-tauri/src/dxgi_capture.rs @@ -322,8 +322,4 @@ impl DxgiDuplicator { Ok((rgba, region_w, region_h)) } } - - pub fn capture_full_frame(&mut self) -> Result<(Vec, u32, u32), String> { - self.capture_region(0, 0, self.width, self.height) - } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c84055e..7d3cf54 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -291,7 +291,6 @@ pub fn run() { zoom::stop_zoom_stream, commands::freeze_zoom, commands::unfreeze_zoom, - zoom::capture_viewport_without_zoom, commands::mag_zoom_show, commands::mag_zoom_hide, commands::mag_zoom_set_config, diff --git a/src-tauri/src/zoom.rs b/src-tauri/src/zoom.rs index bf25464..3c26a7e 100644 --- a/src-tauri/src/zoom.rs +++ b/src-tauri/src/zoom.rs @@ -20,47 +20,14 @@ pub fn zoom_stream_handle() -> &'static ArcSwapOption { ZOOM_STREAM.get_or_init(ArcSwapOption::empty) } -async fn capture_viewport() -> Result { - tokio::task::spawn_blocking(|| { - let mut duplicator = - DxgiDuplicator::new_for_point(0, 0).map_err(|e| format!("DxgiDuplicator: {e}"))?; - - let (rgba, width, height) = duplicator - .capture_full_frame() - .map_err(|e| format!("capture_full_frame: {e}"))?; - - let mut png_data = Vec::new(); - { - let mut encoder = png::Encoder::new(std::io::Cursor::new(&mut png_data), width, height); - encoder.set_color(png::ColorType::Rgba); - encoder.set_depth(png::BitDepth::Eight); - let mut writer = encoder - .write_header() - .map_err(|e| format!("PNG header: {e}"))?; - writer - .write_image_data(&rgba) - .map_err(|e| format!("PNG write: {e}"))?; - } - - use base64::Engine as _; - let base64_string = base64::engine::general_purpose::STANDARD.encode(&png_data); - let data_url = format!("data:image/png;base64,{}", base64_string); - Ok(data_url) - }) - .await - .map_err(|e| format!("spawn_blocking: {e}"))? -} - use std::sync::Mutex; static CACHED_DUPLICATOR: Mutex> = Mutex::new(None); -fn capture_zoom_region_raw_sync( +fn with_cached_duplicator_for_point( cursor_x: i32, cursor_y: i32, - size: u32, - zoom_level: f32, - _filter: ZoomFilter, -) -> Result { + f: impl FnOnce(&mut DxgiDuplicator) -> Result, +) -> Result { let mut cache = CACHED_DUPLICATOR .lock() .map_err(|_| "Mutex locked".to_string())?; @@ -81,31 +48,41 @@ fn capture_zoom_region_raw_sync( } let dup = cache.as_mut().unwrap(); + f(dup).inspect_err(|e| { + if e.contains("ACCESS_LOST") { + *cache = None; + } + }) +} - let origin_x = dup.origin_x; - let origin_y = dup.origin_y; - let level = zoom_level.max(1.0); - let region = ((size as f32) / level) - .round() - .clamp(1.0, MAX_ZOOM_REGION as f32) as i32; - let half = region / 2; - let local_x = cursor_x - origin_x - half; - let local_y = cursor_y - origin_y - half; - - let (rgba, width, height) = dup - .capture_region(local_x, local_y, region as u32, region as u32) - .inspect_err(|e| { - if e.contains("ACCESS_LOST") { - *cache = None; - } - })?; +fn capture_zoom_region_raw_sync( + cursor_x: i32, + cursor_y: i32, + size: u32, + zoom_level: f32, + _filter: ZoomFilter, +) -> Result { + with_cached_duplicator_for_point(cursor_x, cursor_y, |dup| { + let origin_x = dup.origin_x; + let origin_y = dup.origin_y; + let level = zoom_level.max(1.0); + let region = ((size as f32) / level) + .round() + .clamp(1.0, MAX_ZOOM_REGION as f32) as i32; + let half = region / 2; + let local_x = cursor_x - origin_x - half; + let local_y = cursor_y - origin_y - half; + + let (rgba, width, height) = + dup.capture_region(local_x, local_y, region as u32, region as u32)?; - use base64::Engine as _; - let data = base64::engine::general_purpose::STANDARD.encode(&rgba); - Ok(ZoomRegionRawPayload { - data, - width, - height, + use base64::Engine as _; + let data = base64::engine::general_purpose::STANDARD.encode(&rgba); + Ok(ZoomRegionRawPayload { + data, + width, + height, + }) }) } @@ -398,9 +375,3 @@ pub async fn stop_zoom_stream() -> Result<(), String> { } Ok(()) } - -#[tauri::command] -pub async fn capture_viewport_without_zoom(app: tauri::AppHandle) -> Result { - let _ = app; - capture_viewport().await -} From 39486f08276b43dee5f62d01abc15d302299baef Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:41:18 -0600 Subject: [PATCH 17/24] fix(zoom): ensure sourceBuffer is set before updating canvas with decoded data --- src/components/overlay/ZoomShell.vue | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/overlay/ZoomShell.vue b/src/components/overlay/ZoomShell.vue index 86a1514..5aa40d0 100644 --- a/src/components/overlay/ZoomShell.vue +++ b/src/components/overlay/ZoomShell.vue @@ -360,11 +360,13 @@ onMounted(async () => { ); sourceBuffer = sourceImageData.data; } - if (sourceImageData) { + if (sourceImageData && sourceBuffer) { const decoded = decodeBase64(event.payload.data); - sourceBuffer?.set(decoded); - sourceCtx.putImageData(sourceImageData, 0, 0); - screenshot.value = sourceCanvas; + if (decoded.length === sourceBuffer.length) { + sourceBuffer.set(decoded); + sourceCtx.putImageData(sourceImageData, 0, 0); + screenshot.value = sourceCanvas; + } } position.value = { x: event.payload.cursor_x, y: event.payload.cursor_y }; updateCursorVelocity(event.payload.cursor_x, event.payload.cursor_y); From de9ecb74798ee8047cfe0071147796b8a63cf0bc Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 17:44:15 -0600 Subject: [PATCH 18/24] docs(zoom): add documentation for ZOOM_BACKEND_STATE initialization --- src-tauri/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 7d3cf54..07c3150 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,6 +15,9 @@ struct ZoomConfig { backend: String, } +/// In-memory zoom backend state. Seeded once from the compiled-in +/// `config/zoom.json` factory default, then overwritten on every app start +/// by `hydrate()` (frontend) via `set_zoom_backend_cmd` static ZOOM_BACKEND_STATE: OnceLock> = OnceLock::new(); const DEFAULT_ZOOM_BACKEND: &str = "dxgi"; From 8d9d1590151d84685d902bb493fd252cb9f07901 Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sat, 18 Jul 2026 22:03:30 -0600 Subject: [PATCH 19/24] fix(overlay): rename handleHideDock to handleCloseOverlay and update functionality --- src/components/overlay/OverlayShell.vue | 9 +++++---- src/components/shared/CanvasStage.vue | 6 +++++- src/locales/en.json | 2 +- src/locales/es.json | 2 +- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/components/overlay/OverlayShell.vue b/src/components/overlay/OverlayShell.vue index af2f730..60eadff 100644 --- a/src/components/overlay/OverlayShell.vue +++ b/src/components/overlay/OverlayShell.vue @@ -211,8 +211,8 @@ function handleClear() { emit("overlay-clear"); } -function handleHideDock() { - dockHidden.value = true; +function handleCloseOverlay() { + invoke("set_overlay_visible", { visible: false }); } function handleToggleDock() { @@ -392,9 +392,10 @@ onMounted(async () => { }); keydownListener = (event: KeyboardEvent) => { if (event.key !== "Escape") return; + if (canvasStageRef.value?.isTextEditing) return; event.preventDefault(); event.stopPropagation(); - handleClear(); + handleCloseOverlay(); }; localShortcutListener = (event) => handleLocalShortcut(event); globalThis.addEventListener("keydown", keydownListener); @@ -508,7 +509,7 @@ onBeforeUnmount(() => { @redo="handleRedo" @drag-handle="startDockDrag" @open-config="handleOpenConfig" - @close-dock="handleHideDock" + @close-dock="handleCloseOverlay" /> diff --git a/src/components/shared/CanvasStage.vue b/src/components/shared/CanvasStage.vue index 105e558..c57f4a7 100644 --- a/src/components/shared/CanvasStage.vue +++ b/src/components/shared/CanvasStage.vue @@ -1,5 +1,5 @@ + + + + diff --git a/src/locales/en.json b/src/locales/en.json index 2a20cf1..8e3de62 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -121,6 +121,24 @@ } } }, + "onboarding": { + "welcome": { + "subtitle": "Annotate, highlight, and focus anything on your screen." + }, + "hotkeys": { + "title": "Global shortcuts & tray", + "description": "Vynta lives in the system tray. Each mode is triggered by its global shortcut from any app — customize them in the Shortcuts tab." + }, + "nav": { + "back": "Back", + "next": "Next", + "start": "Get started", + "skip": "Skip" + }, + "stepLabel": "Step {current} of {total}", + "reopen": "View intro", + "reopenDescription": "Replay the welcome guide." + }, "hotkeys": { "title": "Keyboard Shortcuts", "placeholder": "Press a combination", diff --git a/src/locales/es.json b/src/locales/es.json index e366bbb..cc7743c 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -121,6 +121,24 @@ } } }, + "onboarding": { + "welcome": { + "subtitle": "Anota, resalta y enfoca cualquier cosa en tu pantalla." + }, + "hotkeys": { + "title": "Atajos globales y bandeja", + "description": "Vynta vive en la bandeja del sistema. Cada modo se activa con su atajo global desde cualquier app — personalízalos en la pestaña Atajos." + }, + "nav": { + "back": "Atrás", + "next": "Siguiente", + "start": "Empezar", + "skip": "Saltar" + }, + "stepLabel": "Paso {current} de {total}", + "reopen": "Ver introducción", + "reopenDescription": "Vuelve a ver la guía de bienvenida." + }, "hotkeys": { "title": "Atajos de teclado", "placeholder": "Presiona una combinación", diff --git a/src/stores/settings.ts b/src/stores/settings.ts index 8e5a12b..fe7ef1f 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -42,6 +42,7 @@ type Settings = { overlayDockScreenSize: { width: number; height: number }; whiteboardDockPosition: { x: number; y: number }; whiteboardGridEnabled: boolean; + onboardingCompleted: boolean; }; export const useSettingsStore = defineStore("settings", () => { @@ -102,6 +103,7 @@ export const useSettingsStore = defineStore("settings", () => { const startWithWindows = ref(false); const restorePreferencesOnLaunch = ref(true); const previewEnabled = ref(true); + const onboardingCompleted = ref(false); // Docks const overlayDockPosition = ref({ x: 0, y: 0 }); @@ -184,6 +186,10 @@ export const useSettingsStore = defineStore("settings", () => { previewEnabled.value = enabled; } + function setOnboardingCompleted(enabled: boolean) { + onboardingCompleted.value = enabled; + } + function setOverlayDockPosition(pos: { x: number; y: number }) { overlayDockPosition.value = { ...pos }; } @@ -312,6 +318,7 @@ export const useSettingsStore = defineStore("settings", () => { overlayDockScreenSize: overlayDockScreenSize.value, whiteboardDockPosition: whiteboardDockPosition.value, whiteboardGridEnabled: whiteboardGridEnabled.value, + onboardingCompleted: onboardingCompleted.value, }; } @@ -377,6 +384,8 @@ export const useSettingsStore = defineStore("settings", () => { whiteboardDockPosition.value = settings.whiteboardDockPosition; if (typeof settings.whiteboardGridEnabled === "boolean") whiteboardGridEnabled.value = settings.whiteboardGridEnabled; + if (typeof settings.onboardingCompleted === "boolean") + onboardingCompleted.value = settings.onboardingCompleted; } async function persist() { @@ -424,6 +433,8 @@ export const useSettingsStore = defineStore("settings", () => { whiteboardDockPosition.value = stored.whiteboardDockPosition; if (typeof stored.whiteboardGridEnabled === "boolean") whiteboardGridEnabled.value = stored.whiteboardGridEnabled; + if (typeof stored.onboardingCompleted === "boolean") + onboardingCompleted.value = stored.onboardingCompleted; if (typeof stored.restorePreferencesOnLaunch === "boolean") { restorePreferencesOnLaunch.value = stored.restorePreferencesOnLaunch; } @@ -491,6 +502,7 @@ export const useSettingsStore = defineStore("settings", () => { overlayDockScreenSize.value = { width: 0, height: 0 }; whiteboardDockPosition.value = { x: 0, y: 0 }; whiteboardGridEnabled.value = true; + onboardingCompleted.value = false; } async function resetSettings() { @@ -554,6 +566,7 @@ export const useSettingsStore = defineStore("settings", () => { overlayDockScreenSize, whiteboardDockPosition, whiteboardGridEnabled, + onboardingCompleted, setStrokeColor, setDefaultStrokeColor, @@ -594,5 +607,6 @@ export const useSettingsStore = defineStore("settings", () => { setOverlayDockScreenSize, setWhiteboardDockPosition, setWhiteboardGridEnabled, + setOnboardingCompleted, }; }); diff --git a/src/utils/format-accelerator.ts b/src/utils/format-accelerator.ts new file mode 100644 index 0000000..47d3709 --- /dev/null +++ b/src/utils/format-accelerator.ts @@ -0,0 +1,29 @@ +const displayTokenMap: Record = { + commandorcontrol: "Ctrl", + ctrl: "Ctrl", + control: "Ctrl", + shift: "Shift", + alt: "Alt", + option: "Alt", + super: "Win", + meta: "Win", + win: "Win", + windows: "Win", +}; + +/** + * Formats a shortcut accelerator string into a human-readable label. + * + * @param {string} accelerator Accelerator string (e.g. "Ctrl+1"). + * @returns {string} Display label (e.g. "Ctrl + 1"), or "Sin asignar" when empty. + */ +export function formatAccelerator(accelerator: string): string { + if (!accelerator) return "Sin asignar"; + return accelerator + .split("+") + .map((token) => { + const key = token.trim().toLowerCase(); + return displayTokenMap[key] ?? token; + }) + .join(" + "); +} From cf356c403ce3af88b29d6e6856313b92ab01efec Mon Sep 17 00:00:00 2001 From: daiv05 Date: Sun, 19 Jul 2026 16:31:09 -0600 Subject: [PATCH 22/24] feat(cursor): add border width and fill opacity settings for cursor highlight --- src/components/app/AppShell.vue | 14 +++++- src/components/app/panels/HomeModes.vue | 46 +++++++++++++++++-- .../overlay/CursorHighlightShell.vue | 37 ++++++++++++--- src/locales/en.json | 4 +- src/locales/es.json | 4 +- src/stores/settings.ts | 24 ++++++++++ src/types/settings.ts | 2 + 7 files changed, 118 insertions(+), 13 deletions(-) diff --git a/src/components/app/AppShell.vue b/src/components/app/AppShell.vue index cc0c1e0..fd366d9 100644 --- a/src/components/app/AppShell.vue +++ b/src/components/app/AppShell.vue @@ -42,6 +42,8 @@ const { cursorHighlightColor, cursorHighlightSize, cursorHighlightShape, + cursorHighlightBorderWidth, + cursorHighlightFillOpacity, shortcutMap, modeShortcutsEnabled, spotlightBackdrop, @@ -276,12 +278,20 @@ watch( ); watch( - [cursorHighlightColor, cursorHighlightSize, cursorHighlightShape], + [ + cursorHighlightColor, + cursorHighlightSize, + cursorHighlightShape, + cursorHighlightBorderWidth, + cursorHighlightFillOpacity, + ], () => { emit("cursor-highlight-settings", { cursorHighlightColor: cursorHighlightColor.value, cursorHighlightSize: cursorHighlightSize.value, cursorHighlightShape: cursorHighlightShape.value, + cursorHighlightBorderWidth: cursorHighlightBorderWidth.value, + cursorHighlightFillOpacity: cursorHighlightFillOpacity.value, }); }, { immediate: true }, @@ -368,6 +378,8 @@ onMounted(async () => { cursorHighlightColor: cursorHighlightColor.value, cursorHighlightSize: cursorHighlightSize.value, cursorHighlightShape: cursorHighlightShape.value, + cursorHighlightBorderWidth: cursorHighlightBorderWidth.value, + cursorHighlightFillOpacity: cursorHighlightFillOpacity.value, }); }, ); diff --git a/src/components/app/panels/HomeModes.vue b/src/components/app/panels/HomeModes.vue index 0cdd7ed..87277cb 100644 --- a/src/components/app/panels/HomeModes.vue +++ b/src/components/app/panels/HomeModes.vue @@ -18,6 +18,8 @@ const { cursorHighlightColor, cursorHighlightSize, cursorHighlightShape, + cursorHighlightBorderWidth, + cursorHighlightFillOpacity, spotlightBackdrop, spotlightRadius, spotlightOpacity, @@ -80,13 +82,19 @@ const cursorPreviewStyle = computed(() => { const scaledSize = 24 + ((realSize - 24) / (140 - 24)) * (70 - 24); const size = `${Math.round(scaledSize)}px`; + const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(color); + const r = rgb ? Number.parseInt(rgb[1], 16) : 255; + const g = rgb ? Number.parseInt(rgb[2], 16) : 255; + const b = rgb ? Number.parseInt(rgb[3], 16) : 255; + const fill = `rgba(${r}, ${g}, ${b}, ${cursorHighlightFillOpacity.value})`; + return { width: size, height: size, + borderWidth: `${cursorHighlightBorderWidth.value}px`, borderColor: color, boxShadow: `0 0 30px ${color}66`, - background: - "radial-gradient(circle at center, rgba(255, 255, 255, 0.1), transparent 70%)", + background: `radial-gradient(circle at center, ${fill}, transparent 70%)`, borderRadius: cursorHighlightShape.value === "circle" ? "999px" @@ -286,6 +294,38 @@ const zoomPreviewStyle = computed(() => { /> {{ cursorHighlightSize }}px +
+ + + {{ cursorHighlightBorderWidth }}px +
+
+ + + {{ Math.round(cursorHighlightFillOpacity * 100) }}% +