diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index bf30627..8f71a28 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1145,8 +1145,12 @@ dependencies = [ "gif", "image", "keyring", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-foundation 0.3.2", "once_cell", "rand 0.9.2", + "raw-window-handle", "reqwest", "rust_socketio", "serde", @@ -1165,6 +1169,7 @@ dependencies = [ "tracing-subscriber", "ts-rs", "url", + "windows 0.58.0", ] [[package]] @@ -1800,7 +1805,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -4241,7 +4246,7 @@ dependencies = [ "unicode-segmentation", "url", "windows 0.61.3", - "windows-core", + "windows-core 0.61.2", "windows-version", "x11-dl", ] @@ -5489,9 +5494,9 @@ dependencies = [ "webview2-com-macros", "webview2-com-sys", "windows 0.61.3", - "windows-core", - "windows-implement", - "windows-interface", + "windows-core 0.61.2", + "windows-implement 0.60.0", + "windows-interface 0.59.1", ] [[package]] @@ -5513,7 +5518,7 @@ checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" dependencies = [ "thiserror 2.0.17", "windows 0.61.3", - "windows-core", + "windows-core 0.61.2", ] [[package]] @@ -5577,6 +5582,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.61.3" @@ -5584,7 +5599,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ "windows-collections", - "windows-core", + "windows-core 0.61.2", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -5596,7 +5611,20 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core", + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", ] [[package]] @@ -5605,11 +5633,11 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement", - "windows-interface", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", ] [[package]] @@ -5618,11 +5646,22 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", "windows-threading", ] +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "windows-implement" version = "0.60.0" @@ -5634,6 +5673,17 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "windows-interface" version = "0.59.1" @@ -5663,7 +5713,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core", + "windows-core 0.61.2", "windows-link 0.1.3", ] @@ -5674,8 +5724,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ "windows-link 0.1.3", - "windows-result", - "windows-strings", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -5687,6 +5746,16 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-strings" version = "0.4.2" @@ -6082,7 +6151,7 @@ dependencies = [ "webkit2gtk-sys", "webview2-com", "windows 0.61.3", - "windows-core", + "windows-core 0.61.2", "windows-version", "x11-dl", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 2b6dea6..eef3075 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -44,7 +44,19 @@ tracing-appender = "0.2.4" tauri-plugin-dialog = "2.4.2" image = {version = "0.25.9", default-features = false, features = ["gif"] } gif = "0.14.1" +raw-window-handle = "0.6" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" tauri-plugin-positioner = "2" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6.3" +objc2-app-kit = { version = "0.3.2", features = ["NSWindow", "NSWindowScripting"] } +objc2-foundation = "0.3.2" + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_UI_Input_KeyboardAndMouse", +] } diff --git a/src-tauri/src/services/doll_editor.rs b/src-tauri/src/services/doll_editor.rs index 98b4625..854ec23 100644 --- a/src-tauri/src/services/doll_editor.rs +++ b/src-tauri/src/services/doll_editor.rs @@ -1,10 +1,55 @@ -use tauri::{Emitter, Manager}; +#[cfg(target_os = "windows")] +use tauri::WebviewWindow; +use tauri::{Emitter, Listener, Manager}; use tracing::{error, info}; use crate::get_app_handle; static APP_MENU_WINDOW_LABEL: &str = "app_menu"; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::{BOOL, HWND}; +#[cfg(target_os = "windows")] +use windows::Win32::UI::Input::KeyboardAndMouse::EnableWindow; // Correct location for EnableWindow + +// #[cfg(target_os = "macos")] +// use objc2_app_kit::{NSWindow, NSWindowCollectionBehavior}; +// #[cfg(target_os = "macos")] +// use tauri::v2_compat::MsgSend; // Removed: Not needed/doesn't exist + +/// Helper to disable/enable interaction with a window +#[cfg(target_os = "windows")] +fn set_window_interaction(window: &WebviewWindow, enable: bool) { + { + // Add explicit import for the trait method + use raw_window_handle::HasWindowHandle; + + if let Ok(handle) = window.window_handle() { + // raw-window-handle 0.6 uses a match pattern + // The trait returns a WindowHandle wrapper which has as_raw() + match handle.as_raw() { + raw_window_handle::RawWindowHandle::Win32(win32_handle) => { + // win32_handle.hwnd is a NonZeroIsize (or similar depending on version), cast to isize then HWND + // windows crate expects HWND(isize) or HWND(*mut c_void) depending on version + // raw-window-handle 0.6: hwnd is NonZero + let hwnd_isize = win32_handle.hwnd.get(); + let hwnd = HWND(hwnd_isize as _); + + unsafe { + // TRUE (1) to enable, FALSE (0) to disable + let _ = EnableWindow(hwnd, BOOL::from(enable)); + } + } + _ => { + error!("Unsupported window handle type on Windows"); + } + } + } else { + error!("Failed to get window handle"); + } + } +} + #[tauri::command] pub async fn open_doll_editor_window(doll_id: Option) { let app_handle = get_app_handle().clone(); @@ -28,6 +73,14 @@ pub async fn open_doll_editor_window(doll_id: Option) { error!("Failed to focus existing doll editor window: {}", e); } + // Ensure overlay is active on parent (redundancy for safety) + #[cfg(target_os = "macos")] + if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { + if let Err(e) = parent.emit("set-interaction-overlay", true) { + error!("Failed to ensure interaction overlay on parent: {}", e); + } + } + // Emit event to update context if let Some(id) = doll_id { if let Err(e) = window.emit("edit-doll", id) { @@ -65,13 +118,60 @@ pub async fn open_doll_editor_window(doll_id: Option) { .visible_on_all_workspaces(false); // Set parent if app menu exists + // Also disable interaction with parent while child is open + + // macOS Specific: Focus Trap Listener ID + // We need to capture this to unlisten later. + + let mut parent_focus_listener_id: Option = None; + if let Some(parent) = app_handle.get_webview_window(APP_MENU_WINDOW_LABEL) { + // 1. Disable parent interaction immediately (Windows only) + #[cfg(target_os = "windows")] + set_window_interaction(&parent, false); + + // 2. Setup Focus Trap (macOS only) + #[cfg(target_os = "macos")] + { + let child_label = window_label.clone(); + let app_handle_clone = get_app_handle().clone(); + + // Emit event to show overlay + if let Err(e) = parent.emit("set-interaction-overlay", true) { + error!("Failed to emit set-interaction-overlay event: {}", e); + } + + // Listen for when the PARENT gets focus + let id = parent.listen("tauri://focus", move |_| { + info!( + "Parent focused, redirecting focus to child: {}", + child_label + ); + if let Some(child) = app_handle_clone.get_webview_window(&child_label) { + if let Err(e) = child.set_focus() { + error!("Failed to refocus child window: {}", e); + } + } + }); + parent_focus_listener_id = Some(id); + } + match builder.parent(&parent) { Ok(b) => builder = b, Err(e) => { error!("Failed to set parent for doll editor window: {}", e); - // If we fail to set parent, we effectively lost the builder because .parent() consumes it. - // We must return here to avoid using moved value. + // If we fail, revert changes + #[cfg(target_os = "windows")] + set_window_interaction(&parent, true); + + #[cfg(target_os = "macos")] + { + if let Some(id) = parent_focus_listener_id { + parent.unlisten(id); + } + // Remove overlay if we failed + let _ = parent.emit("set-interaction-overlay", false); + } return; } }; @@ -80,11 +180,60 @@ pub async fn open_doll_editor_window(doll_id: Option) { match builder.build() { Ok(window) => { info!("{} window builder succeeded", window_label); + + // 3. Setup cleanup hook: When this child window is destroyed, re-enable the parent + let app_handle_clone = get_app_handle().clone(); + + // Capture the listener ID for cleanup + let listener_to_remove = parent_focus_listener_id; + + window.on_window_event(move |event| { + if let tauri::WindowEvent::Destroyed = event { + if let Some(parent) = + app_handle_clone.get_webview_window(APP_MENU_WINDOW_LABEL) + { + info!("Doll editor destroyed, restoring parent state"); + + // Windows: Re-enable input + #[cfg(target_os = "windows")] + set_window_interaction(&parent, true); + + // macOS: Remove focus trap listener + #[cfg(target_os = "macos")] + { + if let Some(id) = listener_to_remove { + parent.unlisten(id); + } + // Remove overlay + if let Err(e) = parent.emit("set-interaction-overlay", false) { + error!("Failed to remove interaction overlay: {}", e); + } + } + + // Optional: Focus parent after child closes for good UX + let _ = parent.set_focus(); + } + } + }); + // #[cfg(debug_assertions)] // window.open_devtools(); } Err(e) => { error!("Failed to build {} window: {}", window_label, e); + // If build failed, revert + if let Some(parent) = get_app_handle().get_webview_window(APP_MENU_WINDOW_LABEL) { + #[cfg(target_os = "windows")] + set_window_interaction(&parent, true); + + #[cfg(target_os = "macos")] + { + if let Some(id) = parent_focus_listener_id { + parent.unlisten(id); + } + let _ = parent.emit("set-interaction-overlay", false); + } + } } }; }); diff --git a/src/routes/app-menu/+page.svelte b/src/routes/app-menu/+page.svelte index 8a9478c..2fd54dd 100644 --- a/src/routes/app-menu/+page.svelte +++ b/src/routes/app-menu/+page.svelte @@ -3,11 +3,40 @@ import Preferences from "./tabs/preferences.svelte"; import YourDolls from "./tabs/your-dolls/index.svelte"; import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow"; + import { listen } from "@tauri-apps/api/event"; + import { onMount } from "svelte"; + + let showInteractionOverlay = false; + + onMount(() => { + const unlisten = listen("set-interaction-overlay", (event) => { + showInteractionOverlay = event.payload as boolean; + }); + + return () => { + unlisten.then((u) => u()); + }; + });
+ {#if showInteractionOverlay} +
{ + e.stopPropagation(); + e.preventDefault(); + }} + onkeydown={(e) => { + e.stopPropagation(); + e.preventDefault(); + }} + tabindex="-1" + >
+ {/if}