replace device_query with tauri API so no need for accessibility perm

on darwin
This commit is contained in:
2026-09-06 01:42:36 +08:00
parent 6e1d89f517
commit ce0fb10073
16 changed files with 209 additions and 655 deletions
-2
View File
@@ -29,7 +29,6 @@ image = { version = "0.25", default-features = false, features = ["gif", "jpeg",
lazy_static = "1.5.0"
ed25519-dalek = "3.0.0"
getrandom = "0.4.3"
device_query = "4.0.1"
tokio = { version = "1.53.1", features = ["macros", "sync", "time"] }
tokio-tungstenite = { version = "0.28", features = ["rustls-tls-native-roots"] }
futures-util = "0.3"
@@ -60,7 +59,6 @@ windows = { version = "0.58", features = [
] }
[target.'cfg(target_os = "macos")'.dependencies]
macos-accessibility-client = "0.0.1"
objc2 = "0.6.3"
objc2-app-kit = "0.3.2"
objc2-foundation = "0.3.2"
+5 -17
View File
@@ -9,7 +9,6 @@ struct ApplicationState {
pub async fn init(handle: &AppHandle) -> Result<(), String> {
handle.manage(ApplicationState::default());
let accessibility_permission_granted = crate::macos::init(handle).await?;
let settings = crate::settings::get(&handle.state())
.await
.map_err(crate::db::command_error)?;
@@ -17,17 +16,14 @@ pub async fn init(handle: &AppHandle) -> Result<(), String> {
crate::ui::splashscreen::close(handle).await?;
if settings.onboarding_done {
start(handle).await;
if cfg!(target_os = "macos") && !accessibility_permission_granted {
crate::ui::onboarding::show_accessibility_page(handle).await?;
}
start(handle);
} else {
crate::ui::onboarding::show_initial(handle).await?;
}
Ok(())
}
pub async fn start(handle: &AppHandle) {
pub fn start(handle: &AppHandle) {
let state = handle.state::<ApplicationState>();
if state
.started
@@ -37,8 +33,10 @@ pub async fn start(handle: &AppHandle) {
return;
}
if let Err(error) = crate::cursor::start_tracking(handle) {
eprintln!("failed to initialize cursor tracking: {error}");
}
crate::ui::init(handle);
reconcile_cursor(handle).await;
}
pub fn is_started(handle: &AppHandle) -> bool {
@@ -47,13 +45,3 @@ pub fn is_started(handle: &AppHandle) -> bool {
.started
.load(Ordering::Acquire)
}
pub async fn reconcile_cursor(handle: &AppHandle) {
if !is_started(handle) || !crate::macos::accessibility_permission_granted(handle) {
return;
}
if let Err(error) = crate::cursor::start_tracking(handle).await {
eprintln!("failed to initialize cursor tracking: {error}");
}
}
+151 -202
View File
@@ -1,5 +1,3 @@
use device_query::{DeviceEvents, DeviceEventsHandler, DeviceQuery, DeviceState};
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use specta::Type;
use std::collections::HashMap;
@@ -8,7 +6,10 @@ use std::sync::RwLock;
use std::time::Duration;
use tauri::{AppHandle, Manager};
use tauri_specta::Event;
use tokio::sync::{mpsc, oneshot, watch};
use tokio::sync::watch;
pub(crate) const SYSTEM_CURSOR_POLL_INTERVAL: Duration = Duration::from_millis(250);
const CURSOR_BROADCAST_INTERVAL: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Type)]
#[serde(rename_all = "camelCase")]
@@ -71,121 +72,87 @@ impl CursorState {
}
}
fn current_position(handle: &AppHandle) -> Result<CursorPositions, String> {
let monitor = handle
.primary_monitor()
.map_err(|error| error.to_string())?
.ok_or("Primary monitor is unavailable")?;
let position = DeviceState::checked_new()
.ok_or_else(|| "System cursor access is unavailable".to_owned())?
.get_mouse()
.coords;
fn read_system_cursor_position(handle: &AppHandle) -> Result<CursorPosition, String> {
handle
.cursor_position()
.map(|position| CursorPosition {
x: position.x,
y: position.y,
})
.map_err(|error| error.to_string())
}
#[cfg(target_os = "windows")]
let raw = CursorPosition {
x: position.0 as f64,
y: position.1 as f64,
};
#[cfg(not(target_os = "windows"))]
let raw = CursorPosition {
x: position.0 as f64 * monitor.scale_factor(),
y: position.1 as f64 * monitor.scale_factor(),
};
Ok(CursorPositions {
mapped: transform_cursor_pos(&raw, true, &monitor),
fn positions_from_raw(raw: CursorPosition, monitor: &tauri::Monitor) -> CursorPositions {
CursorPositions {
mapped: transform_cursor_pos(&raw, true, monitor),
raw,
})
}
}
// Was private, but for some reason LSP
// complains even when there's no external references.
// Possibly because of `lazy_static!`.
// Just leave it public I guess.
struct CursorTask {
stop_tx: watch::Sender<bool>,
task: tauri::async_runtime::JoinHandle<()>,
}
enum CursorTracker {
Starting,
Running(CursorTask),
#[derive(Default)]
pub(crate) struct CursorPositionProvider {
latest: RwLock<Option<CursorPosition>>,
tracker: Mutex<Option<CursorTask>>,
}
lazy_static! {
static ref CURSOR_TRACKER: Mutex<Option<CursorTracker>> = Mutex::new(None);
impl CursorPositionProvider {
pub(crate) fn latest(&self) -> Result<Option<CursorPosition>, String> {
self.latest
.read()
.map(|position| position.clone())
.map_err(|error| error.to_string())
}
fn update(&self, position: CursorPosition) -> Result<(), String> {
let mut latest = self.latest.write().map_err(|error| error.to_string())?;
if latest.as_ref() != Some(&position) {
*latest = Some(position);
}
Ok(())
}
}
pub fn init(app: &AppHandle) {
app.manage(CursorState::default());
app.manage(CursorPositionProvider::default());
}
/// Starts cursor tracking after Accessibility permission is available.
pub async fn start_tracking(app: &AppHandle) -> Result<(), String> {
if !crate::macos::accessibility_permission_granted(app) {
return Err("macOS Accessibility permission has not been granted".to_owned());
}
/// Starts the shared cursor provider and change-based cursor broadcasting.
pub fn start_tracking(app: &AppHandle) -> Result<(), String> {
let primary_monitor = app
.primary_monitor()
.map_err(|error| error.to_string())?
.ok_or_else(|| "Failed to resolve primary monitor".to_owned())?;
{
let mut tracker = CURSOR_TRACKER
.lock()
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
if tracker.is_some() {
return Ok(());
}
*tracker = Some(CursorTracker::Starting);
let provider = app.state::<CursorPositionProvider>();
let mut tracker = provider
.tracker
.lock()
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
if tracker.is_some() {
return Ok(());
}
println!("Initializing cursor tracking...");
match current_position(app) {
Ok(positions) => update_cursor_position(app, positions),
Err(error) => eprintln!("Failed to resolve current cursor position: {error}"),
}
let initial_position = read_system_cursor_position(app)
.map_err(|error| format!("Failed to resolve current cursor position: {error}"))?;
provider.update(initial_position.clone())?;
update_cursor_position(app, positions_from_raw(initial_position, &primary_monitor));
let (stop_tx, stop_rx) = watch::channel(false);
let (ready_tx, ready_rx) = oneshot::channel();
let handle = app.clone();
let task = tauri::async_runtime::spawn(async move {
if let Err(e) = init_cursor_tracking_i(stop_rx, primary_monitor, handle, ready_tx).await {
println!("Failed to initialize cursor tracking: {}", e);
if let Err(error) = track_cursor(stop_rx, primary_monitor, handle).await {
eprintln!("Cursor tracking stopped with an error: {error}");
}
});
let startup = ready_rx
.await
.unwrap_or_else(|_| Err("Cursor tracking task stopped during initialization".to_owned()));
if let Err(error) = startup {
if let Ok(mut tracker) = CURSOR_TRACKER.lock()
&& matches!(*tracker, Some(CursorTracker::Starting))
{
*tracker = None;
}
let _ = task.await;
return Err(error);
}
let installed = {
let mut tracker = CURSOR_TRACKER
.lock()
.map_err(|error| format!("Failed to lock cursor tracker state: {error}"))?;
if matches!(*tracker, Some(CursorTracker::Starting)) {
*tracker = Some(CursorTracker::Running(CursorTask { stop_tx, task }));
true
} else {
false
}
};
if !installed {
return Err("Cursor tracking startup was cancelled".to_owned());
}
*tracker = Some(CursorTask { stop_tx, task });
println!("EVENT: Cursor Tracker Enabled");
Ok(())
@@ -265,6 +232,86 @@ fn transform_coords(pos: &CursorPosition, to_normalized: bool, w: f64, h: f64) -
}
}
/// Stop cursor tracking and unregister all listeners.
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
pub async fn stop_cursor_tracking(app: &AppHandle) {
println!("stop_cursor_tracking called");
let tracker = match app.state::<CursorPositionProvider>().tracker.lock() {
Ok(mut tracker) => tracker.take(),
Err(e) => {
println!("Failed to lock cursor tracker state: {}", e);
return;
}
};
let tracker = match tracker {
Some(tracker) => tracker,
None => {
println!("Cursor tracking is not running");
return;
}
};
if let Err(e) = tracker.stop_tx.send(true) {
println!("Failed to signal cursor tracking stop: {}", e);
}
if let Err(e) = tracker.task.await {
println!("Cursor tracking task join failed: {}", e);
}
println!("EVENT: Cursor Tracker Disabled");
}
async fn track_cursor(
mut stop_rx: watch::Receiver<bool>,
monitor: tauri::Monitor,
handle: AppHandle,
) -> Result<(), String> {
let provider = handle.state::<CursorPositionProvider>();
let mut last_broadcast_position = provider.latest()?;
let mut poll_interval = tokio::time::interval(SYSTEM_CURSOR_POLL_INTERVAL);
let mut broadcast_interval = tokio::time::interval(CURSOR_BROADCAST_INTERVAL);
poll_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
broadcast_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
// The initial position was already sampled and broadcast by start_tracking.
poll_interval.tick().await;
broadcast_interval.tick().await;
loop {
tokio::select! {
_ = poll_interval.tick() => {
match read_system_cursor_position(&handle) {
Ok(position) => provider.update(position)?,
Err(error) => eprintln!("Failed to read system cursor position: {error}"),
}
}
_ = broadcast_interval.tick() => {
let latest = provider.latest()?;
if latest != last_broadcast_position {
if let Some(position) = latest.as_ref() {
update_cursor_position(
&handle,
positions_from_raw(position.clone(), &monitor),
);
}
last_broadcast_position = latest;
}
}
changed = stop_rx.changed() => {
if changed.is_err() || *stop_rx.borrow() {
println!("Stopping cursor tracking");
break;
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@@ -318,124 +365,26 @@ mod tests {
assert_eq!(two_x.x, one_x.x);
assert_eq!(two_x.y, one_x.y);
}
}
/// Stop cursor tracking and unregister all listeners.
#[allow(dead_code)] // TODO: get rid of this when app teardown sequence is introduced.
pub async fn stop_cursor_tracking() {
println!("stop_cursor_tracking called");
#[test]
fn cursor_provider_keeps_the_latest_system_position() {
let provider = CursorPositionProvider::default();
assert_eq!(provider.latest().unwrap(), None);
let tracker = match CURSOR_TRACKER.lock() {
Ok(mut tracker) => tracker.take(),
Err(e) => {
println!("Failed to lock cursor tracker state: {}", e);
return;
}
};
provider
.update(CursorPosition { x: 120.0, y: 80.0 })
.unwrap();
assert_eq!(
provider.latest().unwrap(),
Some(CursorPosition { x: 120.0, y: 80.0 })
);
let tracker = match tracker {
Some(CursorTracker::Running(tracker)) => tracker,
Some(CursorTracker::Starting) => {
println!("Cursor tracking startup cancelled");
return;
}
None => {
println!("Cursor tracking is not running");
return;
}
};
if let Err(e) = tracker.stop_tx.send(true) {
println!("Failed to signal cursor tracking stop: {}", e);
provider
.update(CursorPosition { x: 240.0, y: 160.0 })
.unwrap();
assert_eq!(
provider.latest().unwrap(),
Some(CursorPosition { x: 240.0, y: 160.0 })
);
}
if let Err(e) = tracker.task.await {
println!("Cursor tracking task join failed: {}", e);
}
println!("EVENT: Cursor Tracker Disabled");
}
async fn init_cursor_tracking_i(
mut stop_rx: watch::Receiver<bool>,
monitor: tauri::Monitor,
handle: AppHandle,
ready_tx: oneshot::Sender<Result<(), String>>,
) -> Result<(), String> {
// Create a channel to decouple event generation (producer) from processing (consumer).
// Capacity 100 is plenty for 500ms polling (2Hz).
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
let permission_handle = handle.clone();
// Spawn the consumer task
// This task handles WebSocket reporting and local position projection updates.
// It runs independently of the device event loop.
tauri::async_runtime::spawn(async move {
println!("Cursor event consumer started");
while let Some(positions) = rx.recv().await {
update_cursor_position(&handle, positions);
}
println!("Cursor event consumer stopped (channel closed)");
});
let device_state = match DeviceEventsHandler::new(Duration::from_millis(500)) {
Some(device_state) => device_state,
None => {
let error = "Failed to create device event handler (already running?)".to_owned();
let _ = ready_tx.send(Err(error.clone()));
return Err(error);
}
};
println!("Device event handler created successfully");
println!("Setting up mouse move handler for event broadcasting...");
#[cfg(not(target_os = "windows"))]
let scale_factor = monitor.scale_factor();
// The producer closure moves `tx` into it.
// device_query runs this closure on its own thread.
let _guard = device_state.on_mouse_move(move |position: &(i32, i32)| {
if !crate::macos::accessibility_permission_granted(&permission_handle) {
return;
}
#[cfg(target_os = "windows")]
let raw = CursorPosition {
x: position.0 as f64,
y: position.1 as f64,
};
#[cfg(not(target_os = "windows"))]
let raw = CursorPosition {
x: position.0 as f64 * scale_factor,
y: position.1 as f64 * scale_factor,
};
let mapped = transform_cursor_pos(&raw, true, &monitor);
let positions = CursorPositions { raw, mapped };
// Send to consumer channel (non-blocking)
if let Err(e) = tx.try_send(positions) {
println!("Failed to send cursor position to channel: {:?}", e);
}
});
ready_tx
.send(Ok(()))
.map_err(|_| "Cursor tracking startup was cancelled".to_owned())?;
println!("Mouse move handler registered - now broadcasting cursor events to all windows");
// Keep the handler alive while tracking is enabled.
// This loop is necessary to keep `_guard` and `device_state` in scope.
while stop_rx.changed().await.is_ok() {
if *stop_rx.borrow() {
println!("Stopping cursor tracking event handler");
break;
}
}
Ok(())
}
-4
View File
@@ -6,7 +6,6 @@ mod images;
mod interactions;
mod keypair;
mod live_data;
mod macos;
mod network;
mod onboarding;
mod profile;
@@ -114,9 +113,7 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
puppet::list_puppet_states,
scene_configuration::get_scene_configuration,
scene_configuration::update_scene_configuration,
onboarding::get_onboarding_status,
onboarding::complete_onboarding,
macos::request_accessibility_permission,
settings::autostart::get_autostart_enabled,
settings::autostart::set_autostart_enabled,
settings::get_locale_settings,
@@ -136,7 +133,6 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
interactions::FriendInteractionReceived,
puppet::PuppetStatesChanged,
scene_configuration::SceneConfigurationChanged,
onboarding::OnboardingStatus,
settings::LocaleChanged,
])
}
-118
View File
@@ -1,118 +0,0 @@
use std::{
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tauri::{AppHandle, Manager};
pub struct AccessibilityPermissionState(AtomicBool);
impl AccessibilityPermissionState {
fn new(granted: bool) -> Self {
Self(AtomicBool::new(granted))
}
fn replace(&self, granted: bool) -> bool {
self.0.swap(granted, Ordering::AcqRel) != granted
}
fn granted(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
#[cfg(target_os = "macos")]
fn system_accessibility_permission_granted() -> bool {
macos_accessibility_client::accessibility::application_is_trusted()
}
#[cfg(not(target_os = "macos"))]
fn system_accessibility_permission_granted() -> bool {
true
}
#[cfg(target_os = "macos")]
fn request_system_accessibility_permission() -> bool {
macos_accessibility_client::accessibility::application_is_trusted_with_prompt()
}
#[cfg(not(target_os = "macos"))]
fn request_system_accessibility_permission() -> bool {
true
}
pub fn accessibility_permission_granted(handle: &AppHandle) -> bool {
if !cfg!(target_os = "macos") {
return true;
}
handle.state::<AccessibilityPermissionState>().granted()
}
async fn apply_permission_state(handle: &AppHandle, granted: bool) -> Result<bool, String> {
if !handle
.state::<AccessibilityPermissionState>()
.replace(granted)
{
return Ok(false);
}
let database = handle.state();
let status = crate::onboarding::emit_status(handle, &database).await?;
if status.onboarding_done {
if granted {
crate::application::reconcile_cursor(handle).await;
} else {
crate::ui::onboarding::show_accessibility_page(handle).await?;
}
}
Ok(true)
}
pub async fn init(handle: &AppHandle) -> Result<bool, String> {
let granted = system_accessibility_permission_granted();
handle.manage(AccessibilityPermissionState::new(granted));
#[cfg(target_os = "macos")]
{
let handle = handle.clone();
tauri::async_runtime::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(1));
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
interval.tick().await;
loop {
interval.tick().await;
let granted = system_accessibility_permission_granted();
if let Err(error) = apply_permission_state(&handle, granted).await {
eprintln!("failed to update macOS Accessibility permission: {error}");
}
}
});
}
Ok(granted)
}
#[tauri::command]
#[specta::specta]
pub async fn request_accessibility_permission(handle: AppHandle) -> Result<bool, String> {
let granted = tauri::async_runtime::spawn_blocking(request_system_accessibility_permission)
.await
.map_err(|error| error.to_string())?;
apply_permission_state(&handle, granted).await?;
Ok(granted)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn permission_state_reports_only_actual_transitions() {
let state = AccessibilityPermissionState::new(false);
assert!(!state.replace(false));
assert!(state.replace(true));
assert!(!state.replace(true));
assert!(state.replace(false));
}
}
+2 -56
View File
@@ -1,56 +1,6 @@
use serde::{Deserialize, Serialize};
use specta::Type;
use tauri::{AppHandle, State};
use tauri_specta::Event;
use crate::db::{self, AppDatabase};
use crate::settings::AppSettings;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Type, Event)]
#[serde(rename_all = "camelCase")]
pub struct OnboardingStatus {
pub onboarding_done: bool,
pub macos_accessibility_permission_granted: bool,
pub requires_accessibility_permission: bool,
}
impl OnboardingStatus {
fn new(handle: &AppHandle, settings: AppSettings) -> Self {
Self {
onboarding_done: settings.onboarding_done,
macos_accessibility_permission_granted: crate::macos::accessibility_permission_granted(
handle,
),
requires_accessibility_permission: cfg!(target_os = "macos"),
}
}
}
pub(crate) async fn emit_status(
handle: &AppHandle,
database: &AppDatabase,
) -> Result<OnboardingStatus, String> {
let status = OnboardingStatus::new(
handle,
crate::settings::get(database)
.await
.map_err(db::command_error)?,
);
status.clone().emit(handle).map_err(db::command_error)?;
Ok(status)
}
#[tauri::command]
#[specta::specta]
pub async fn get_onboarding_status(
handle: AppHandle,
database: State<'_, AppDatabase>,
) -> Result<OnboardingStatus, String> {
let settings = crate::settings::get(&database)
.await
.map_err(db::command_error)?;
Ok(OnboardingStatus::new(&handle, settings))
}
#[tauri::command]
#[specta::specta]
@@ -58,15 +8,10 @@ pub async fn complete_onboarding(
handle: AppHandle,
database: State<'_, AppDatabase>,
) -> Result<(), String> {
if cfg!(target_os = "macos") && !crate::macos::accessibility_permission_granted(&handle) {
return Err("Accessibility access must be granted before setup can finish.".to_owned());
}
crate::settings::set_onboarding_done(&database, true)
.await
.map_err(db::command_error)?;
emit_status(&handle, &database).await?;
crate::application::start(&handle).await;
crate::application::start(&handle);
Ok(())
}
@@ -75,6 +20,7 @@ mod tests {
use sqlx::sqlite::SqlitePoolOptions;
use super::*;
use crate::settings::AppSettings;
async fn database() -> AppDatabase {
let pool = SqlitePoolOptions::new()
+18 -33
View File
@@ -2,37 +2,30 @@ use tauri::{AppHandle, Manager, WebviewUrl};
const WINDOW_LABEL: &str = "onboarding";
async fn show(handle: &AppHandle, accessibility_only: bool) -> Result<(), String> {
pub async fn show_initial(handle: &AppHandle) -> Result<(), String> {
if let Some(window) = handle.get_webview_window(WINDOW_LABEL) {
if accessibility_only {
window
.eval("window.location.href = '/onboarding?step=accessibility'")
.map_err(|error| error.to_string())?;
}
window.unminimize().map_err(|error| error.to_string())?;
window.show().map_err(|error| error.to_string())?;
return window.set_focus().map_err(|error| error.to_string());
}
let page = if accessibility_only {
"/onboarding?step=accessibility"
} else {
"/onboarding"
};
let window =
tauri::WebviewWindowBuilder::new(handle, WINDOW_LABEL, WebviewUrl::App(page.into()))
.title(crate::settings::text(
handle,
crate::settings::NativeText::OnboardingTitle,
))
.inner_size(680.0, 520.0)
.min_inner_size(680.0, 520.0)
.transparent(true)
.resizable(false)
.maximizable(false)
.center()
.build()
.map_err(|error| error.to_string())?;
let window = tauri::WebviewWindowBuilder::new(
handle,
WINDOW_LABEL,
WebviewUrl::App("/onboarding".into()),
)
.title(crate::settings::text(
handle,
crate::settings::NativeText::OnboardingTitle,
))
.inner_size(680.0, 520.0)
.min_inner_size(680.0, 520.0)
.transparent(true)
.resizable(false)
.maximizable(false)
.center()
.build()
.map_err(|error| error.to_string())?;
let handle = handle.clone();
window.on_window_event(move |event| {
@@ -44,11 +37,3 @@ async fn show(handle: &AppHandle, accessibility_only: bool) -> Result<(), String
});
Ok(())
}
pub async fn show_initial(handle: &AppHandle) -> Result<(), String> {
show(handle, false).await
}
pub async fn show_accessibility_page(handle: &AppHandle) -> Result<(), String> {
show(handle, true).await
}
+12 -5
View File
@@ -1,4 +1,4 @@
use std::{sync::RwLock, time::Duration};
use std::sync::RwLock;
use serde::Deserialize;
use specta::Type;
@@ -82,15 +82,22 @@ pub const WINDOW_LABEL: &str = "scene";
fn track_scene_hitboxes(app_handle: AppHandle, window: tauri::WebviewWindow) {
tauri::async_runtime::spawn(async move {
let mut ignores_cursor = true;
let mut interval = tokio::time::interval(Duration::from_millis(16));
let mut interval = tokio::time::interval(crate::cursor::SYSTEM_CURSOR_POLL_INTERVAL);
interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
interval.tick().await;
let cursor = match app_handle.cursor_position() {
Ok(cursor) => cursor,
let cursor = match app_handle
.state::<crate::cursor::CursorPositionProvider>()
.latest()
{
Ok(Some(cursor)) => cursor,
Ok(None) => continue,
Err(error) => {
eprintln!("Failed to read cursor position for scene hit-testing: {error}");
eprintln!(
"Failed to read shared cursor position for scene hit-testing: {error}"
);
continue;
}
};