feat(stage-tamagotchi): allowing click through and fade away when hovering (currently macOS only) (#186)

Co-authored-by: Midori Kochiya <1381736+kmod-midori@users.noreply.github.com>
Co-authored-by: LemonNeko <17664845+LemonNekoGH@users.noreply.github.com>
This commit is contained in:
Neko 2025-05-30 16:43:33 +08:00 committed by GitHub
parent a99ba7f50a
commit da4da0aeb6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 335 additions and 185 deletions

236
Cargo.lock generated
View file

@ -95,12 +95,18 @@ name = "app"
version = "0.4.27"
dependencies = [
"log",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
"serde",
"serde_json",
"tauri",
"tauri-build",
"tauri-plugin-log",
"tauri-plugin-mcp",
"tauri-plugin-os",
"tauri-plugin-prevent-default",
"tokio",
"windows",
]
[[package]]
@ -487,9 +493,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
@ -735,9 +741,9 @@ dependencies = [
[[package]]
name = "dlopen2_derive"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
dependencies = [
"proc-macro2",
"quote",
@ -788,9 +794,9 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
version = "3.0.2"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3"
checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489"
dependencies = [
"cc",
"memchr",
@ -1451,17 +1457,21 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"tokio",
@ -1665,6 +1675,16 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "itertools"
version = "0.14.0"
@ -1857,9 +1877,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@ -2323,9 +2343,9 @@ dependencies = [
[[package]]
name = "parking_lot"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
@ -2333,9 +2353,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
@ -2828,9 +2848,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.15"
version = "0.12.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5"
dependencies = [
"base64 0.22.1",
"bytes",
@ -2855,13 +2875,13 @@ dependencies = [
"tokio",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"windows-registry",
]
[[package]]
@ -3273,9 +3293,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.9"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
@ -4059,6 +4079,24 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e"
dependencies = [
"bitflags 2.9.1",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@ -4579,7 +4617,7 @@ dependencies = [
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.4.2",
"windows-strings",
]
[[package]]
@ -4631,17 +4669,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@ -4651,15 +4678,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@ -4678,15 +4696,6 @@ dependencies = [
"windows-targets 0.42.2",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
@ -4720,21 +4729,6 @@ dependencies = [
"windows_x86_64_msvc 0.42.2",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
@ -4744,29 +4738,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@ -4791,180 +4769,90 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.5.40"
@ -4985,12 +4873,12 @@ dependencies = [
[[package]]
name = "winreg"
version = "0.52.0"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]

View file

@ -31,6 +31,9 @@ tauri-plugin-log = "2.0.0-rc"
tauri-plugin-os = "2"
tauri-plugin-mcp = { workspace = true }
tauri-plugin-prevent-default = "1.3"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = "1.45.1"
[profile.dev]
incremental = true
@ -40,3 +43,10 @@ debug = true
[profile.dev.package."*"]
opt-level = 1
debug = false
[target.'cfg(target_os = "macos")'.dependencies]
objc2 = "0.6.1"
objc2-foundation = "0.3.1"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.61.1", features = ["Win32_UI_WindowsAndMessaging", "Win32_Foundation"] }

View file

@ -3,14 +3,234 @@ use tauri::tray::TrayIconBuilder;
use tauri::Emitter;
use tauri::RunEvent;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::time::sleep;
#[cfg(target_os = "macos")]
use objc2::{class, msg_send};
#[cfg(target_os = "macos")]
use objc2_foundation::{NSPoint, NSRect};
#[cfg(target_os = "macos")]
use tauri::{ActivationPolicy, TitleBarStyle};
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
use tauri_plugin_prevent_default::Flags;
#[cfg(target_os = "windows")]
use windows::Win32::UI::WindowsAndMessaging::{
GetWindowLongW, SetWindowLongW, GWL_EXSTYLE, WS_EX_TRANSPARENT,
};
mod commands;
mod windows;
#[derive(Default)]
struct WindowClickThroughState {
is_click_through_monitoring_enabled: Arc<AtomicBool>,
is_click_through_enabled: Arc<AtomicBool>,
is_click_through_cursor_inside: Arc<AtomicBool>,
}
fn set_cursor_inside(window: &tauri::Window, is_inside: bool) -> Result<(), String> {
let state = window.state::<WindowClickThroughState>();
state
.is_click_through_cursor_inside
.store(is_inside, Ordering::Relaxed);
window
.set_ignore_cursor_events(is_inside)
.map_err(|e| format!("Failed to set click-through state: {}", e))?;
let _ = window.emit("tauri-app:window-click-through:is-inside", is_inside);
Ok(())
}
fn set_click_through_enabled(window: &tauri::Window, enabled: bool) -> Result<(), String> {
let state = window.state::<WindowClickThroughState>();
state
.is_click_through_enabled
.store(enabled, Ordering::Relaxed);
window
.set_ignore_cursor_events(enabled)
.map_err(|e| format!("Failed to set click-through state: {}", e))?;
let _ = window.emit("tauri-app:window-click-through:enabled", enabled);
Ok(())
}
/// Get cursor position relative to the window
#[cfg(target_os = "macos")]
async fn is_cursor_in_window(window: &tauri::Window) -> bool {
unsafe {
// Get cursor position in screen coordinates (macOS coordinates - origin at bottom left)
let mouse_location: NSPoint = msg_send![class!(NSEvent), mouseLocation];
// Get all screens
//
// We need screens count because for multiple-display users,
// in macOS, the Native API returns the mouse coordinates relative to the primary
// display, for example, if we say the primary display is 1920x1080, another two lies
// on both sides of the primary display with the size of 1920x1080 too, mouse coordinates
// will be 0 at the left edge of p-display, and 1920 at the right edge of the p-display,
// -1080 at the left edge of the left-side display, and 2160 at the right edge of the right-side
// display.
let screens: *const objc2::runtime::AnyObject = msg_send![class!(NSScreen), screens];
let screens_count: usize = msg_send![screens, count];
// Get window position and size from Tauri
// Get the NSWindow from Tauri window to access native properties
//
// While Tauri does provide us window.outer_position() and window.outer_size()
// it's not enough to determine if the cursor is inside the window.
// The value of those two functions returns the in-compatible coordinates (top-left origin)
// with OBJ-C Native API values. This produces a lot of issues when calculating the collision
// of the cursor relative to the window.
//
// We need to get the window's frame in macOS coordinates (bottom-left origin)
// and check if the cursor is inside that frame.
let ns_window: *mut objc2::runtime::AnyObject = window.ns_window().unwrap() as *mut _;
let window_frame: NSRect = msg_send![ns_window, frame];
// Log all screens information
for _ in 0..screens_count {
// For debugging purpose, screen object, frame size of the screen, visible frame size of the screen,
// and the scale factor of the screen can be obtained as follows:
//
// let screen: *const objc2::runtime::AnyObject = msg_send![screens, objectAtIndex: i];
// let frame: NSRect = msg_send![screen, frame];
// let visible_frame: NSRect = msg_send![screen, visibleFrame];
// let scale_factor: f64 = msg_send![screen, backingScaleFactor];
// Check if mouse is inside our window's frame
let is_inside = mouse_location.x >= window_frame.origin.x
&& mouse_location.x <= (window_frame.origin.x + window_frame.size.width)
&& mouse_location.y >= window_frame.origin.y
&& mouse_location.y <= (window_frame.origin.y + window_frame.size.height);
if is_inside {
return true;
}
}
}
false
}
/// Check if modifier key is pressed (Option/Alt or Control)
#[cfg(target_os = "macos")]
fn is_modifier_pressed() -> bool {
unsafe {
let event_class: &'static objc2::runtime::AnyClass = class!(NSEvent);
let flags: u64 = msg_send![event_class, modifierFlags];
// Check for Alt/Option (1 << 19) or Control (1 << 18)
let alt_pressed = (flags & (1 << 19)) != 0;
// let ctrl_pressed = (flags & (1 << 18)) != 0;
// alt_pressed || ctrl_pressed
alt_pressed
}
}
#[tauri::command]
async fn start_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
log::info!("Starting monitor for clicking through");
let window = window.clone();
let state = window.state::<WindowClickThroughState>();
let is_click_through_enabled = state.is_click_through_enabled.clone();
let is_click_through_monitoring_enabled = state.is_click_through_monitoring_enabled.clone();
// Already monitoring?
if is_click_through_monitoring_enabled.load(Ordering::Relaxed) {
return Ok(());
} else {
// Set to true
state
.is_click_through_monitoring_enabled
.store(true, Ordering::Relaxed);
}
// Then start interval timer for monitoring
tauri::async_runtime::spawn(async move {
loop {
sleep(Duration::from_millis(32)).await; // ~30FPS check rate
// If monitoring is already stopped, break the loop
if !is_click_through_monitoring_enabled.load(Ordering::Relaxed) {
log::info!("Monitoring is stopped, breaking loop");
break;
}
// If is disabled already, skip until next check
if !is_click_through_enabled.load(Ordering::Relaxed) {
log::info!("Click-through is disabled, skipping check");
continue;
}
#[cfg(target_os = "macos")]
{
let cursor_inside = is_cursor_in_window(&window).await;
let modifier_pressed = is_modifier_pressed();
log::info!(
"Cursor inside: {}, Modifier pressed: {}",
cursor_inside,
modifier_pressed
);
// Only allow disabling click-through when:
// 1. Cursor is OUTSIDE the window AND
// 2. Modifier key is pressed
let should_be_click_through = cursor_inside && !modifier_pressed;
if should_be_click_through {
let _ = set_cursor_inside(&window, true);
} else {
let _ = set_cursor_inside(&window, false);
}
}
}
});
return Ok(());
}
#[tauri::command]
async fn stop_monitor_for_clicking_through(window: tauri::Window) -> Result<(), String> {
log::info!("Stopping monitor for clicking through");
let window = window.clone();
let state = window.state::<WindowClickThroughState>();
// Set to false
// Termination will be triggered in the next interval check (tick)
state
.is_click_through_monitoring_enabled
.store(false, Ordering::Relaxed);
Ok(())
}
#[tauri::command]
async fn start_click_through(window: tauri::Window) -> Result<(), String> {
set_click_through_enabled(&window, true)?;
Ok(())
}
#[tauri::command]
async fn stop_click_through(window: tauri::Window) -> Result<(), String> {
set_click_through_enabled(&window, false)?;
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let prevent_default_plugin = tauri_plugin_prevent_default::Builder::new()
@ -21,6 +241,7 @@ pub fn run() {
.plugin(prevent_default_plugin)
.plugin(tauri_plugin_mcp::Builder.build())
.plugin(tauri_plugin_os::init())
.manage(WindowClickThroughState::default())
.setup(|app| {
let mut builder = WebviewWindowBuilder::new(app, "main", WebviewUrl::default());
@ -98,6 +319,10 @@ pub fn run() {
.invoke_handler(tauri::generate_handler![
commands::open_settings_window,
commands::open_chat_window,
start_monitor_for_clicking_through,
stop_monitor_for_clicking_through,
start_click_through,
stop_click_through,
])
.build(tauri::generate_context!())
.expect("error while building tauri application")

View file

@ -3,8 +3,9 @@ import { WidgetStage } from '@proj-airi/stage-ui/components'
import { useMcpStore } from '@proj-airi/stage-ui/stores'
import { connectServer } from '@proj-airi/tauri-plugin-mcp'
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
import { storeToRefs } from 'pinia'
import { computed, onMounted } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useWindowShortcuts } from '../composables/window-shortcuts'
import { useWindowControlStore } from '../stores/window-controls'
@ -13,6 +14,7 @@ import { WindowControlMode } from '../types/window-controls'
const windowStore = useWindowControlStore()
useWindowShortcuts()
const viewRef = ref<HTMLDivElement>()
const mcpStore = useMcpStore()
const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore)
@ -29,6 +31,18 @@ const modeIndicatorClass = computed(() => {
}
})
onMounted(async () => {
await invoke('start_monitor_for_clicking_through')
await invoke('start_click_through')
})
onUnmounted(async () => {
await invoke('stop_click_through')
await invoke('stop_monitor_for_clicking_through')
})
const unlisten: (() => void)[] = []
function openSettings() {
invoke('open_settings_window')
}
@ -38,6 +52,19 @@ function openChat() {
}
onMounted(async () => {
// Listen for click-through state changes
unlisten.push(await listen('tauri-app:window-click-through:is-inside', (event: { payload: boolean }) => {
if (!viewRef.value)
return
if (event.payload) {
viewRef.value.style.opacity = '0'
}
else {
viewRef.value.style.opacity = '1'
}
}))
if (connected.value)
return
if (!serverCmd.value || !serverArgs.value)
@ -50,10 +77,15 @@ onMounted(async () => {
console.error(error)
}
})
onUnmounted(() => {
unlisten.forEach(fn => fn?.())
})
</script>
<template>
<div
ref="viewRef"
:class="[modeIndicatorClass]"
relative
max-h="[100vh]"
@ -63,6 +95,7 @@ onMounted(async () => {
z-2
h-full
overflow-hidden
transition="opacity duration-500 ease-in-out"
>
<div relative h-full w-full items-end gap-2 class="view">
<WidgetStage h-full w-full flex-1 mb="<md:18" />
@ -91,7 +124,6 @@ onMounted(async () => {
</div>
</div>
</div>
<!-- Debug Mode UI -->
<div v-if="windowStore.controlMode === WindowControlMode.DEBUG" class="debug-controls">
<!-- Add debug controls here -->
@ -138,12 +170,7 @@ onMounted(async () => {
<style scoped>
.view {
&:hover {
.interaction-area {
opacity: 1;
pointer-events: auto;
}
}
transition: opacity 0.5s ease-in-out;
}
.drag-region {

View file

@ -5,7 +5,7 @@ import { ref } from 'vue'
import { WindowControlMode } from '../types/window-controls'
export const useWindowControlStore = defineStore('windowControl', () => {
const controlMode = ref<WindowControlMode>(WindowControlMode.DEFAULT)
const controlMode = ref<WindowControlMode>(WindowControlMode.NONE)
const isControlActive = ref(false)
const size = useLocalStorage('window/control/size', { width: 300 * 1.5, height: 400 * 1.5 })
const position = useLocalStorage('window/control/position', { x: 0, y: 0 })
@ -21,7 +21,7 @@ export const useWindowControlStore = defineStore('windowControl', () => {
function toggleControl() {
isControlActive.value = !isControlActive.value
if (!isControlActive.value) {
controlMode.value = WindowControlMode.DEFAULT
controlMode.value = WindowControlMode.NONE
}
}

View file

@ -1,5 +1,5 @@
export enum WindowControlMode {
DEFAULT = 'default',
NONE = 'none',
MOVE = 'move',
RESIZE = 'resize',
DEBUG = 'debug',