diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 754579c..547bab0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -67,6 +67,7 @@ fn specta_builder() -> tauri_specta::Builder { profile::update_profile, keypair::get_public_key, network::list_statuses, + ui::scene::update_scene_hitboxes, ]) .events(tauri_specta::collect_events![ friends::FriendsChanged, diff --git a/src-tauri/src/ui/mod.rs b/src-tauri/src/ui/mod.rs index 85d252b..8517446 100644 --- a/src-tauri/src/ui/mod.rs +++ b/src-tauri/src/ui/mod.rs @@ -2,7 +2,7 @@ use tauri::AppHandle; #[cfg(debug_assertions)] mod debug; -mod scene; +pub mod scene; pub fn init(app_handle: &AppHandle) { #[cfg(debug_assertions)] diff --git a/src-tauri/src/ui/scene/mod.rs b/src-tauri/src/ui/scene/mod.rs index 11d1342..7c5ce52 100644 --- a/src-tauri/src/ui/scene/mod.rs +++ b/src-tauri/src/ui/scene/mod.rs @@ -1,4 +1,43 @@ -use tauri::{AppHandle, Manager, WebviewUrl}; +use std::{sync::RwLock, time::Duration}; + +use serde::Deserialize; +use specta::Type; +use tauri::{AppHandle, Manager, State, WebviewUrl}; + +#[derive(Debug, Clone, Deserialize, Type)] +#[serde(rename_all = "camelCase")] +pub struct SceneHitbox { + x: f64, + y: f64, + width: f64, + height: f64, +} + +impl SceneHitbox { + fn contains(&self, x: f64, y: f64) -> bool { + x >= self.x && x <= self.x + self.width && y >= self.y && y <= self.y + self.height + } +} + +#[derive(Default)] +pub struct SceneHitboxes(RwLock>); + +impl SceneHitboxes { + fn contains(&self, x: f64, y: f64) -> Result { + let hitboxes = self.0.read().map_err(|error| error.to_string())?; + Ok(hitboxes.iter().any(|hitbox| hitbox.contains(x, y))) + } +} + +#[tauri::command] +#[specta::specta] +pub fn update_scene_hitboxes( + hitboxes: Vec, + state: State<'_, SceneHitboxes>, +) -> Result<(), String> { + *state.0.write().map_err(|error| error.to_string())? = hitboxes; + Ok(()) +} pub fn apply_macos_decoration_window_policy(app_handle: &AppHandle, window_label: String) { #[cfg(target_os = "macos")] @@ -40,6 +79,57 @@ pub fn apply_macos_decoration_window_policy(app_handle: &AppHandle, window_label 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)); + + loop { + interval.tick().await; + + let cursor = match app_handle.cursor_position() { + Ok(cursor) => cursor, + Err(error) => { + eprintln!("Failed to read cursor position for scene hit-testing: {error}"); + continue; + } + }; + let window_position = match window.outer_position() { + Ok(position) => position, + Err(error) => { + eprintln!("Failed to read scene position for hit-testing: {error}"); + continue; + } + }; + let scale_factor = match window.scale_factor() { + Ok(scale_factor) => scale_factor, + Err(error) => { + eprintln!("Failed to read scene scale factor for hit-testing: {error}"); + continue; + } + }; + let x = (cursor.x - window_position.x as f64) / scale_factor; + let y = (cursor.y - window_position.y as f64) / scale_factor; + let should_ignore = match app_handle.state::().contains(x, y) { + Ok(contains_cursor) => !contains_cursor, + Err(error) => { + eprintln!("Failed to read scene hitboxes: {error}"); + continue; + } + }; + + if should_ignore != ignores_cursor { + match window.set_ignore_cursor_events(should_ignore) { + Ok(()) => ignores_cursor = should_ignore, + Err(error) => { + eprintln!("Failed to update scene cursor event policy: {error}") + } + } + } + } + }); +} + pub fn overlay_fullscreen( app_handle: &AppHandle, window: &tauri::WebviewWindow, @@ -80,6 +170,7 @@ pub fn open_window(app_handle: &AppHandle) -> Result Result { }, async listStatuses() : Promise { return await TAURI_INVOKE("list_statuses"); +}, +async updateSceneHitboxes(hitboxes: SceneHitbox[]) : Promise { + return await TAURI_INVOKE("update_scene_hitboxes", { hitboxes }); } } @@ -90,6 +93,7 @@ export type ProfileChanged = { profile: User } export type Remote = { id: string; address: string; name: string | null; port: number | null } export type RemoteInput = { address: string; name: string | null; port: number | null } export type RemotesChanged = { remotes: Remote[] } +export type SceneHitbox = { x: number; y: number; width: number; height: number } /** * Public user identity exchanged with peers and remote servers. `id` is the * user's Ed25519 public key and is not persisted as local profile metadata. diff --git a/src/routes/scene/+page.svelte b/src/routes/scene/+page.svelte index 8dc2fd3..59b4c34 100644 --- a/src/routes/scene/+page.svelte +++ b/src/routes/scene/+page.svelte @@ -1,5 +1,12 @@ -
-
-

WYD

+ + +
+
diff --git a/src/routes/scene/hitboxes.ts b/src/routes/scene/hitboxes.ts new file mode 100644 index 0000000..d9311dc --- /dev/null +++ b/src/routes/scene/hitboxes.ts @@ -0,0 +1,41 @@ +import { commands } from "$lib/bindings"; + +const HITBOX_SELECTOR = ".scene-hitbox"; + +export function startHitboxSync(): () => void { + let resizeObserver: ResizeObserver; + + const syncHitboxes = () => { + const elements = Array.from(document.querySelectorAll(HITBOX_SELECTOR)); + elements.forEach((element) => resizeObserver.observe(element)); + + const hitboxes = elements.map((element) => { + const { x, y, width, height } = element.getBoundingClientRect(); + return { x, y, width, height }; + }); + + commands + .updateSceneHitboxes(hitboxes) + .catch((error) => console.error("Failed to update scene hitboxes", error)); + }; + + resizeObserver = new ResizeObserver(syncHitboxes); + const mutationObserver = new MutationObserver(syncHitboxes); + mutationObserver.observe(document.body, { + attributes: true, + attributeFilter: ["class", "style"], + childList: true, + subtree: true, + }); + window.addEventListener("resize", syncHitboxes); + syncHitboxes(); + + return () => { + resizeObserver.disconnect(); + mutationObserver.disconnect(); + window.removeEventListener("resize", syncHitboxes); + commands + .updateSceneHitboxes([]) + .catch((error) => console.error("Failed to clear scene hitboxes", error)); + }; +}