feat(rust): commands module with click-through toggle helpers
This commit is contained in:
1
src-tauri/src/commands/mod.rs
Normal file
1
src-tauri/src/commands/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod overlay;
|
||||
117
src-tauri/src/commands/overlay.rs
Normal file
117
src-tauri/src/commands/overlay.rs
Normal file
@@ -0,0 +1,117 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Emitter, Manager, Runtime};
|
||||
|
||||
pub const OVERLAY_LABEL: &str = "overlay";
|
||||
pub const EVT_CLICK_THROUGH_TOGGLED: &str = "click-through-toggled";
|
||||
pub const EVT_CLICK_THROUGH_NO_OVERLAY: &str = "click-through-no-overlay";
|
||||
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct ClickThroughToggledPayload {
|
||||
#[serde(rename = "clickThrough")]
|
||||
pub click_through: bool,
|
||||
}
|
||||
|
||||
/// Applies a specific click-through state to the overlay (sets ignore_cursor_events,
|
||||
/// performs the opacity pulse, emits the toggled event). Returns the applied state.
|
||||
///
|
||||
/// Tauri 2's WebviewWindow doesn't expose getters for `ignore_cursor_events` or
|
||||
/// `opacity`, so callers must compute the new state themselves: the JS-side
|
||||
/// `toggle_click_through` command receives `current` from the front end and
|
||||
/// passes `!current`; the global shortcut handler maintains its own cache via
|
||||
/// `AppState.click_through`.
|
||||
pub async fn apply_state<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
new_state: bool,
|
||||
) -> Result<Option<bool>, String> {
|
||||
let Some(overlay) = app.get_webview_window(OVERLAY_LABEL) else {
|
||||
let _ = app.emit(EVT_CLICK_THROUGH_NO_OVERLAY, ());
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
overlay
|
||||
.set_ignore_cursor_events(new_state)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// Opacity dip half of the visual pulse. The JS click-through-toggled
|
||||
// handler is responsible for restoring opacity to the user's actual
|
||||
// target — Rust deliberately does NOT call set_opacity again, so there
|
||||
// is no possibility of the global-shortcut path "snapping back" to a
|
||||
// wrong value (we don't have a current-opacity getter).
|
||||
let _ = overlay.set_opacity(0.4);
|
||||
tokio::time::sleep(Duration::from_millis(180)).await;
|
||||
|
||||
app.emit(
|
||||
EVT_CLICK_THROUGH_TOGGLED,
|
||||
ClickThroughToggledPayload {
|
||||
click_through: new_state,
|
||||
},
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Some(new_state))
|
||||
}
|
||||
|
||||
/// Tauri command — invoked from JS. The JS side passes the *current* state so
|
||||
/// we can compute the inverse without needing a getter on the Tauri side.
|
||||
#[tauri::command]
|
||||
pub async fn toggle_click_through<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
current: bool,
|
||||
) -> Result<Option<bool>, String> {
|
||||
apply_state(&app, !current).await
|
||||
}
|
||||
|
||||
/// Stateless setter — used by the global shortcut handler, which doesn't have
|
||||
/// access to the JS-side current value. Reads the *intended* state from a
|
||||
/// shared `Mutex<bool>` kept in app state.
|
||||
pub async fn toggle_via_global_shortcut<R: Runtime>(
|
||||
app: &AppHandle<R>,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<crate::AppState>();
|
||||
let new_value = {
|
||||
let mut guard = state.click_through.lock().expect("poisoned");
|
||||
*guard = !*guard;
|
||||
*guard
|
||||
};
|
||||
apply_state(app, new_value).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sync the Rust-side click-through cache with what the JS side just applied.
|
||||
/// Called on overlay open/close and whenever the JS toggle changes user-side.
|
||||
#[tauri::command]
|
||||
pub fn sync_click_through_cache(
|
||||
app: AppHandle,
|
||||
value: bool,
|
||||
) -> Result<(), String> {
|
||||
let state = app.state::<crate::AppState>();
|
||||
let mut guard = state.click_through.lock().map_err(|_| "poisoned".to_string())?;
|
||||
*guard = value;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pull the hotkey-registration outcome that was decided during setup.
|
||||
/// Pull-style avoids the listener-not-yet-registered race that an event-based
|
||||
/// approach would have.
|
||||
#[tauri::command]
|
||||
pub fn get_hotkey_status(app: AppHandle) -> Result<crate::HotkeyStatus, String> {
|
||||
let state = app.state::<crate::AppState>();
|
||||
let guard = state.hotkey_status.lock().map_err(|_| "poisoned".to_string())?;
|
||||
Ok(guard.clone())
|
||||
}
|
||||
|
||||
/// Set window opacity. Tauri 2's JS WebviewWindow does NOT expose setOpacity
|
||||
/// (only the Rust side does), so the JS layer routes through this command.
|
||||
#[tauri::command]
|
||||
pub fn set_window_opacity<R: Runtime>(
|
||||
app: AppHandle<R>,
|
||||
label: String,
|
||||
opacity: f32,
|
||||
) -> Result<(), String> {
|
||||
let Some(window) = app.get_webview_window(&label) else {
|
||||
return Err(format!("window not found: {label}"));
|
||||
};
|
||||
window.set_opacity(opacity).map_err(|e| e.to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user