hitbox detection UI thing idk
This commit is contained in:
@@ -67,6 +67,7 @@ fn specta_builder() -> tauri_specta::Builder<tauri::Wry> {
|
|||||||
profile::update_profile,
|
profile::update_profile,
|
||||||
keypair::get_public_key,
|
keypair::get_public_key,
|
||||||
network::list_statuses,
|
network::list_statuses,
|
||||||
|
ui::scene::update_scene_hitboxes,
|
||||||
])
|
])
|
||||||
.events(tauri_specta::collect_events![
|
.events(tauri_specta::collect_events![
|
||||||
friends::FriendsChanged,
|
friends::FriendsChanged,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use tauri::AppHandle;
|
|||||||
|
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
mod debug;
|
mod debug;
|
||||||
mod scene;
|
pub mod scene;
|
||||||
|
|
||||||
pub fn init(app_handle: &AppHandle) {
|
pub fn init(app_handle: &AppHandle) {
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
|
|||||||
@@ -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<Vec<SceneHitbox>>);
|
||||||
|
|
||||||
|
impl SceneHitboxes {
|
||||||
|
fn contains(&self, x: f64, y: f64) -> Result<bool, String> {
|
||||||
|
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<SceneHitbox>,
|
||||||
|
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) {
|
pub fn apply_macos_decoration_window_policy(app_handle: &AppHandle, window_label: String) {
|
||||||
#[cfg(target_os = "macos")]
|
#[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";
|
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::<SceneHitboxes>().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(
|
pub fn overlay_fullscreen(
|
||||||
app_handle: &AppHandle,
|
app_handle: &AppHandle,
|
||||||
window: &tauri::WebviewWindow,
|
window: &tauri::WebviewWindow,
|
||||||
@@ -80,6 +170,7 @@ pub fn open_window(app_handle: &AppHandle) -> Result<tauri::WebviewWindow, Strin
|
|||||||
.always_on_top(true)
|
.always_on_top(true)
|
||||||
.skip_taskbar(true)
|
.skip_taskbar(true)
|
||||||
.shadow(false)
|
.shadow(false)
|
||||||
|
.accept_first_mouse(true)
|
||||||
.visible(true)
|
.visible(true)
|
||||||
.focused(false);
|
.focused(false);
|
||||||
|
|
||||||
@@ -92,10 +183,32 @@ pub fn open_window(app_handle: &AppHandle) -> Result<tauri::WebviewWindow, Strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn init(app_handle: &AppHandle) {
|
pub fn init(app_handle: &AppHandle) {
|
||||||
|
app_handle.manage(SceneHitboxes::default());
|
||||||
let window = open_window(app_handle).expect("Scene window should be opened successfully");
|
let window = open_window(app_handle).expect("Scene window should be opened successfully");
|
||||||
overlay_fullscreen(app_handle, &window).expect("Scene window should be fullscreened");
|
overlay_fullscreen(app_handle, &window).expect("Scene window should be fullscreened");
|
||||||
apply_macos_decoration_window_policy(app_handle, WINDOW_LABEL.to_string());
|
apply_macos_decoration_window_policy(app_handle, WINDOW_LABEL.to_string());
|
||||||
window
|
window
|
||||||
.set_ignore_cursor_events(true)
|
.set_ignore_cursor_events(true)
|
||||||
.expect("Scene window needs to ignore cursor events");
|
.expect("Scene window needs to ignore cursor events");
|
||||||
|
track_scene_hitboxes(app_handle.clone(), window);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scene_hitbox_includes_edges_and_excludes_outside_points() {
|
||||||
|
let hitbox = SceneHitbox {
|
||||||
|
x: 10.0,
|
||||||
|
y: 20.0,
|
||||||
|
width: 30.0,
|
||||||
|
height: 40.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(hitbox.contains(10.0, 20.0));
|
||||||
|
assert!(hitbox.contains(40.0, 60.0));
|
||||||
|
assert!(!hitbox.contains(9.9, 20.0));
|
||||||
|
assert!(!hitbox.contains(40.1, 60.0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ async getPublicKey() : Promise<string> {
|
|||||||
},
|
},
|
||||||
async listStatuses() : Promise<ConnectionStatus[]> {
|
async listStatuses() : Promise<ConnectionStatus[]> {
|
||||||
return await TAURI_INVOKE("list_statuses");
|
return await TAURI_INVOKE("list_statuses");
|
||||||
|
},
|
||||||
|
async updateSceneHitboxes(hitboxes: SceneHitbox[]) : Promise<null> {
|
||||||
|
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 Remote = { id: string; address: string; name: string | null; port: number | null }
|
||||||
export type RemoteInput = { 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 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
|
* 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.
|
* user's Ed25519 public key and is not persisted as local profile metadata.
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
<div>
|
<script lang="ts">
|
||||||
<div class="mt-12 mx-auto px-3 py-1 rounded w-min bg-white text-black">
|
import { onMount } from "svelte";
|
||||||
<p>WYD</p>
|
import { startHitboxSync } from "./hitboxes";
|
||||||
|
|
||||||
|
onMount(startHitboxSync);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="size-full flex flex-col justify-end">
|
||||||
|
<div role="banner" class="scene-hitbox w-full h-min bg-white text-black">
|
||||||
|
<p class="w-max">WYD</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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));
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user