pet menu persistence

This commit is contained in:
2026-01-07 01:46:46 +08:00
parent 6cadedf0e6
commit 77472d43f8
4 changed files with 89 additions and 10 deletions

View File

@@ -14,7 +14,7 @@ use crate::{
}, },
cursor::start_cursor_tracking, cursor::start_cursor_tracking,
doll_editor::open_doll_editor_window, doll_editor::open_doll_editor_window,
scene::{open_splash_window, set_scene_interactive}, scene::{open_splash_window, set_pet_menu_state, set_scene_interactive},
}, },
state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL}, state::{init_app_data, init_app_data_scoped, AppDataRefreshScope, FDOLL},
}; };
@@ -425,6 +425,7 @@ pub fn run() {
open_client_config_manager, open_client_config_manager,
open_doll_editor_window, open_doll_editor_window,
set_scene_interactive, set_scene_interactive,
set_pet_menu_state,
start_auth_flow, start_auth_flow,
logout_and_restart logout_and_restart
]) ])

View File

@@ -16,6 +16,16 @@ pub static SPLASH_WINDOW_LABEL: &str = "splash";
static SCENE_INTERACTIVE_STATE: OnceCell<Arc<AtomicBool>> = OnceCell::new(); static SCENE_INTERACTIVE_STATE: OnceCell<Arc<AtomicBool>> = OnceCell::new();
static MODIFIER_LISTENER_INIT: OnceCell<()> = OnceCell::new(); static MODIFIER_LISTENER_INIT: OnceCell<()> = OnceCell::new();
// New: Track which pets have open menus
static OPEN_PET_MENUS: OnceCell<Arc<std::sync::Mutex<std::collections::HashSet<String>>>> =
OnceCell::new();
fn get_open_pet_menus() -> Arc<std::sync::Mutex<std::collections::HashSet<String>>> {
OPEN_PET_MENUS
.get_or_init(|| Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())))
.clone()
}
fn scene_interactive_state() -> Arc<AtomicBool> { fn scene_interactive_state() -> Arc<AtomicBool> {
SCENE_INTERACTIVE_STATE SCENE_INTERACTIVE_STATE
.get_or_init(|| Arc::new(AtomicBool::new(false))) .get_or_init(|| Arc::new(AtomicBool::new(false)))
@@ -25,6 +35,14 @@ fn scene_interactive_state() -> Arc<AtomicBool> {
pub fn update_scene_interactive(interactive: bool) { pub fn update_scene_interactive(interactive: bool) {
let app_handle = get_app_handle(); let app_handle = get_app_handle();
// If we are forcing interactive to false (e.g. background click), clear any open menus
// This prevents the loop from immediately re-enabling it if the frontend hasn't updated yet
if !interactive {
if let Ok(mut menus) = get_open_pet_menus().lock() {
menus.clear();
}
}
if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) { if let Some(window) = app_handle.get_window(SCENE_WINDOW_LABEL) {
if let Err(e) = window.set_ignore_cursor_events(!interactive) { if let Err(e) = window.set_ignore_cursor_events(!interactive) {
error!("Failed to toggle scene cursor events: {}", e); error!("Failed to toggle scene cursor events: {}", e);
@@ -43,6 +61,33 @@ pub fn set_scene_interactive(interactive: bool) {
update_scene_interactive(interactive); update_scene_interactive(interactive);
} }
#[tauri::command]
pub fn set_pet_menu_state(id: String, open: bool) {
let menus_arc = get_open_pet_menus();
let should_update = {
if let Ok(mut menus) = menus_arc.lock() {
if open {
menus.insert(id);
} else {
menus.remove(&id);
}
!menus.is_empty()
} else {
false
}
};
// After updating state, re-evaluate interactivity immediately
// We don't have direct access to key state here easily without recalculating everything,
// but the loop will pick it up shortly.
// HOWEVER, if we just closed the last menu and keys aren't held, we might want to ensure it closes fast.
// For now, let the loop handle it to avoid race conditions with key states.
// But if we just OPENED a menu, we definitely want to ensure interactive is TRUE.
if should_update {
update_scene_interactive(true);
}
}
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
#[link(name = "ApplicationServices", kind = "framework")] #[link(name = "ApplicationServices", kind = "framework")]
extern "C" { extern "C" {
@@ -96,11 +141,22 @@ fn start_scene_modifier_listener() {
loop { loop {
let keys = device_state.get_keys(); let keys = device_state.get_keys();
// Check for Alt key (Option on Mac) // Check for Alt key (Option on Mac)
let interactive = (keys.contains(&Keycode::LAlt) || keys.contains(&Keycode::RAlt)) || keys.contains(&Keycode::Command); let keys_interactive = (keys.contains(&Keycode::LAlt) || keys.contains(&Keycode::RAlt)) || keys.contains(&Keycode::Command);
// Check if any pet menus are open
let menus_open = {
if let Ok(menus) = get_open_pet_menus().lock() {
!menus.is_empty()
} else {
false
}
};
let interactive = keys_interactive || menus_open;
if interactive != last_interactive { if interactive != last_interactive {
// State changed // State changed
info!("Key down state chanegd!"); info!("Interactive state changed to: {}", interactive);
let previous = state.swap(interactive, Ordering::SeqCst); let previous = state.swap(interactive, Ordering::SeqCst);
if previous != interactive { if previous != interactive {
update_scene_interactive(interactive); update_scene_interactive(interactive);

View File

@@ -88,6 +88,7 @@
{@const config = getFriendDollConfig(userId)} {@const config = getFriendDollConfig(userId)}
{#if config} {#if config}
<DesktopPet <DesktopPet
id={userId}
targetX={position.mapped.x * innerWidth} targetX={position.mapped.x * innerWidth}
targetY={position.mapped.y * innerHeight} targetY={position.mapped.y * innerHeight}
name={getFriendName(userId)} name={getFriendName(userId)}

View File

@@ -1,10 +1,12 @@
<script lang="ts"> <script lang="ts">
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { invoke } from "@tauri-apps/api/core";
import { usePetState } from "$lib/composables/usePetState"; import { usePetState } from "$lib/composables/usePetState";
import { getSpriteSheetUrl } from "$lib/utils/sprite-utils"; import { getSpriteSheetUrl } from "$lib/utils/sprite-utils";
import PetSprite from "$lib/components/PetSprite.svelte"; import PetSprite from "$lib/components/PetSprite.svelte";
import onekoGif from "../../assets/oneko/oneko.gif"; import onekoGif from "../../assets/oneko/oneko.gif";
export let id = "";
export let targetX = 0; export let targetX = 0;
export let targetY = 0; export let targetY = 0;
export let name = ""; export let name = "";
@@ -21,9 +23,19 @@
let lastFrameTimestamp: number; let lastFrameTimestamp: number;
let spriteSheetUrl = onekoGif; let spriteSheetUrl = onekoGif;
let isPetMenuOpen = false;
// Watch for color changes to regenerate sprite // Watch for color changes to regenerate sprite
$: updateSprite(bodyColor, outlineColor); $: updateSprite(bodyColor, outlineColor);
$: (isInteractive, (isPetMenuOpen = false));
$: {
if (id) {
invoke("set_pet_menu_state", { id, open: isPetMenuOpen });
}
}
async function updateSprite( async function updateSprite(
body: string | undefined, body: string | undefined,
outline: string | undefined, outline: string | undefined,
@@ -65,23 +77,32 @@
}); });
</script> </script>
<button <div
onclick={() => {
console.log("clicked on neko");
}}
class="desktop-pet flex flex-col items-center relative" class="desktop-pet flex flex-col items-center relative"
style=" style="
transform: translate({$position.x - 16}px, {$position.y - 16}px); transform: translate({$position.x - 16}px, {$position.y - 16}px);
z-index: 50; z-index: 50;
" "
> >
<div class="hover:scale-110 active:scale-95 transition-transform"> {#if isPetMenuOpen}
<div
class="absolute -translate-y-44 w-30 h-40 bg-neutral-500 rounded-md"
role="menu"
tabindex="0"
aria-label="Pet Menu"
></div>
{/if}
<button
onclick={() => {
isPetMenuOpen = !isPetMenuOpen;
}}
>
<PetSprite <PetSprite
{spriteSheetUrl} {spriteSheetUrl}
spriteX={$currentSprite.x} spriteX={$currentSprite.x}
spriteY={$currentSprite.y} spriteY={$currentSprite.y}
/> />
</div> </button>
<span <span
class="absolute -bottom-5 width-full text-[10px] bg-black/50 text-white px-1 rounded backdrop-blur-sm mt-1 whitespace-nowrap opacity-0 transition-opacity" class="absolute -bottom-5 width-full text-[10px] bg-black/50 text-white px-1 rounded backdrop-blur-sm mt-1 whitespace-nowrap opacity-0 transition-opacity"
@@ -89,7 +110,7 @@
> >
{name} {name}
</span> </span>
</button> </div>
<style> <style>
.desktop-pet { .desktop-pet {