From ac9ec95fe01c912edc50e131d1d1eea9f140f93b Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Mon, 30 Jun 2025 02:12:16 +0800 Subject: [PATCH] feat(stage-tamagotchi): window state persistence --- Cargo.lock | 935 +++++++++++++++++- apps/stage-tamagotchi/src-tauri/Cargo.toml | 1 + apps/stage-tamagotchi/src-tauri/src/lib.rs | 6 + .../src-tauri/src/plugins/mod.rs | 2 + .../src-tauri/src/plugins/window/mod.rs | 56 ++ .../src/plugins/window_persistence/mod.rs | 21 + .../composables/tauri-window-persistence.ts | 551 +++++++++++ .../stage-tamagotchi/src/composables/tauri.ts | 78 +- apps/stage-tamagotchi/src/pages/index.vue | 19 +- 9 files changed, 1611 insertions(+), 58 deletions(-) create mode 100644 apps/stage-tamagotchi/src-tauri/src/plugins/mod.rs create mode 100644 apps/stage-tamagotchi/src-tauri/src/plugins/window/mod.rs create mode 100644 apps/stage-tamagotchi/src-tauri/src/plugins/window_persistence/mod.rs create mode 100644 apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts diff --git a/Cargo.lock b/Cargo.lock index 606ed6a49..0410b610a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -120,6 +120,16 @@ dependencies = [ "libc", ] +[[package]] +name = "annotate-snippets" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaf7e9dfbb6ab22c82e473cd1a8a7bd313c19a5b7e40970f3d89ef5a5c9e81e" +dependencies = [ + "unicode-width 0.1.14", + "yansi-term", +] + [[package]] name = "anstream" version = "0.6.19" @@ -210,6 +220,7 @@ dependencies = [ "tokio", "url", "windows 0.61.3", + "xcap", ] [[package]] @@ -227,6 +238,139 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.0.7", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.0.7", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + +[[package]] +name = "async-signal" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.0.7", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "atk" version = "0.18.2" @@ -295,6 +439,27 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "annotate-snippets", + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.103", +] + [[package]] name = "bindgen_cuda" version = "0.1.5" @@ -381,6 +546,19 @@ dependencies = [ "objc2 0.6.1", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "borsh" version = "1.5.7" @@ -694,6 +872,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + [[package]] name = "cfb" version = "0.7.3" @@ -742,6 +929,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading 0.8.8", +] + [[package]] name = "clap" version = "4.5.40" @@ -813,6 +1011,15 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.11" @@ -822,7 +1029,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.1", "windows-sys 0.59.0", ] @@ -832,6 +1039,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -842,6 +1058,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "cookie-factory" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2" +dependencies = [ + "futures", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1166,7 +1391,7 @@ version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1217,6 +1442,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", + "libc", "objc2 0.6.1", ] @@ -1231,6 +1458,15 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.8", +] + [[package]] name = "dlopen2" version = "0.7.0" @@ -1254,6 +1490,12 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -1263,6 +1505,46 @@ dependencies = [ "serde", ] +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.9.1", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +dependencies = [ + "drm-sys", + "rustix 0.38.44", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +dependencies = [ + "libc", + "linux-raw-sys 0.6.5", +] + [[package]] name = "dtoa" version = "1.0.10" @@ -1350,6 +1632,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -1362,6 +1650,27 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.103", +] + [[package]] name = "env_filter" version = "0.1.3" @@ -1407,6 +1716,27 @@ dependencies = [ "cc", ] +[[package]] +name = "event-listener" +version = "5.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "extended" version = "0.1.0" @@ -1595,6 +1925,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1645,6 +1988,30 @@ dependencies = [ "byteorder", ] +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.9.1", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + [[package]] name = "gdk" version = "0.18.2" @@ -2083,6 +2450,26 @@ dependencies = [ "winapi", ] +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + [[package]] name = "glib" version = "0.18.5" @@ -2633,7 +3020,7 @@ dependencies = [ "console", "number_prefix", "portable-atomic", - "unicode-width", + "unicode-width 0.2.1", "web-time", ] @@ -2780,6 +3167,22 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + [[package]] name = "kuchikiki" version = "0.8.8-speedreader" @@ -2798,6 +3201,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2864,12 +3273,67 @@ dependencies = [ "libc", ] +[[package]] +name = "libspa" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65f3a4b81b2a2d8c7f300643676202debd1b7c929dbf5c9bb89402ea11d19810" +dependencies = [ + "bitflags 2.9.1", + "cc", + "convert_case 0.6.0", + "cookie-factory", + "libc", + "libspa-sys", + "nix 0.27.1", + "nom", + "system-deps", +] + +[[package]] +name = "libspa-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf0d9716420364790e85cbb9d3ac2c950bde16a7dd36f3209b7dfdfc4a24d01f" +dependencies = [ + "bindgen", + "cc", + "system-deps", +] + +[[package]] +name = "libwayshot-xcap" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558a3a7ca16a17a14adf8f051b3adcd7766d397532f5f6d6a48034db11e54c22" +dependencies = [ + "drm", + "gbm", + "gl", + "image", + "khronos-egl", + "memmap2", + "rustix 1.0.7", + "thiserror 2.0.12", + "tracing", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" +[[package]] +name = "linux-raw-sys" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -3161,6 +3625,30 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -3385,6 +3873,35 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-av-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e085a2e16c61dadbad7a808fc9d5b5f8472b1b825b53d529c9f64ccac78e722" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2", + "objc2 0.6.1", + "objc2-avf-audio", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-video", + "objc2-foundation 0.3.1", + "objc2-quartz-core 0.3.1", +] + +[[package]] +name = "objc2-avf-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910" +dependencies = [ + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-cloud-kit" version = "0.3.1" @@ -3436,7 +3953,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", "dispatch2", + "libc", "objc2 0.6.1", ] @@ -3447,10 +3966,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" dependencies = [ "bitflags 2.9.1", + "block2 0.6.1", "dispatch2", + "libc", "objc2 0.6.1", "objc2-core-foundation", "objc2-io-surface", + "objc2-metal 0.3.1", ] [[package]] @@ -3463,6 +3985,37 @@ dependencies = [ "objc2-foundation 0.3.1", ] +[[package]] +name = "objc2-core-media" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b7afa6822e2fa20dfc88d10186b2432bf8560b5ed73ec9d31efd78277bc878" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "dispatch2", + "objc2 0.6.1", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-core-video", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1989c3e76c7e978cab0ba9e6f4961cd00ed14ca21121444cc26877403bfb6303" +dependencies = [ + "bitflags 2.9.1", + "block2 0.6.1", + "objc2 0.6.1", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-io-surface", + "objc2-metal 0.3.1", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -3526,6 +4079,17 @@ dependencies = [ "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-metal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874" +dependencies = [ + "bitflags 2.9.1", + "objc2 0.6.1", + "objc2-foundation 0.3.1", +] + [[package]] name = "objc2-quartz-core" version = "0.2.2" @@ -3536,7 +4100,7 @@ dependencies = [ "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", - "objc2-metal", + "objc2-metal 0.2.2", ] [[package]] @@ -3678,6 +4242,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_info" version = "3.12.0" @@ -3715,6 +4289,12 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.4" @@ -3906,6 +4486,45 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pipewire" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08e645ba5c45109106d56610b3ee60eb13a6f2beb8b74f8dc8186cf261788dda" +dependencies = [ + "anyhow", + "bitflags 2.9.1", + "libc", + "libspa", + "libspa-sys", + "nix 0.27.1", + "once_cell", + "pipewire-sys", + "thiserror 1.0.69", +] + +[[package]] +name = "pipewire-sys" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "849e188f90b1dda88fe2bfe1ad31fe5f158af2c98f80fb5d13726c44f3f01112" +dependencies = [ + "bindgen", + "libspa-sys", + "system-deps", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3920,7 +4539,7 @@ checksum = "3d77244ce2d584cd84f6a15f86195b8c9b2a0dfbfd817c09e0464244091a58ed" dependencies = [ "base64 0.22.1", "indexmap 2.9.0", - "quick-xml", + "quick-xml 0.37.5", "serde", "time", ] @@ -3938,6 +4557,21 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.0.7", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -4159,6 +4793,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -4609,6 +5252,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.1" @@ -4772,6 +5421,12 @@ dependencies = [ "syn 2.0.103", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6287,6 +6942,17 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "ug" version = "0.4.0" @@ -6397,6 +7063,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.1" @@ -6663,6 +7335,94 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" +dependencies = [ + "cc", + "downcast-rs", + "rustix 0.38.44", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" +dependencies = [ + "bitflags 2.9.1", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf" +dependencies = [ + "bitflags 2.9.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485dfb8ccf0daa0d34625d34e6ac15f99e550a7999b6fd88a0835ccd37655785" +dependencies = [ + "bitflags 2.9.1", + "downcast-rs", + "rustix 0.38.44", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" +dependencies = [ + "dlib", + "libc", + "log", + "memoffset", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.77" @@ -6781,6 +7541,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "widestring" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7cf3379ca1aac9eea11fba24fd7e315d621f8dfe35c8d7d2be8b793726e07d" + [[package]] name = "winapi" version = "0.3.9" @@ -7399,12 +8165,70 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "xcap" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7e3a8886dc29e53598bcdddc55869b76ea0d53b775dabaad6c5b9c6408f158" +dependencies = [ + "dispatch2", + "image", + "lazy_static", + "libwayshot-xcap", + "log", + "objc2 0.6.1", + "objc2-app-kit", + "objc2-av-foundation", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-media", + "objc2-core-video", + "objc2-foundation 0.3.1", + "percent-encoding", + "pipewire", + "rand 0.9.1", + "scopeguard", + "serde", + "thiserror 2.0.12", + "url", + "widestring", + "windows 0.61.3", + "xcb", + "zbus", +] + +[[package]] +name = "xcb" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e2f212bb1a92cd8caac8051b829a6582ede155ccb60b5d5908b81b100952be" +dependencies = [ + "bitflags 1.3.2", + "libc", + "quick-xml 0.30.0", +] + [[package]] name = "xkeysym" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml-rs" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" + +[[package]] +name = "yansi-term" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" +dependencies = [ + "winapi", +] + [[package]] name = "yoke" version = "0.7.5" @@ -7453,6 +8277,66 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "nix 0.30.1", + "ordered-stream", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "windows-sys 0.59.0", + "winnow 0.7.11", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.103", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +dependencies = [ + "serde", + "static_assertions", + "winnow 0.7.11", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.25" @@ -7547,3 +8431,44 @@ dependencies = [ "num_enum", "thiserror 1.0.69", ] + +[[package]] +name = "zvariant" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.11", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +dependencies = [ + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "syn 2.0.103", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "static_assertions", + "syn 2.0.103", + "winnow 0.7.11", +] diff --git a/apps/stage-tamagotchi/src-tauri/Cargo.toml b/apps/stage-tamagotchi/src-tauri/Cargo.toml index 8597ad3a9..c9e500c7d 100644 --- a/apps/stage-tamagotchi/src-tauri/Cargo.toml +++ b/apps/stage-tamagotchi/src-tauri/Cargo.toml @@ -50,6 +50,7 @@ tokenizers = "0.21.2" url = "2.5.4" tauri-plugin-window-state = "2.3.0" tauri-plugin-positioner = "2.3.0" +xcap = "0.6.1" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.1" diff --git a/apps/stage-tamagotchi/src-tauri/src/lib.rs b/apps/stage-tamagotchi/src-tauri/src/lib.rs index 2afb75175..6d4637bc3 100644 --- a/apps/stage-tamagotchi/src-tauri/src/lib.rs +++ b/apps/stage-tamagotchi/src-tauri/src/lib.rs @@ -16,6 +16,7 @@ use tokio::time::sleep; mod app_click_through; mod app_windows; mod commands; +mod plugins; mod whisper; #[cfg(target_os = "macos")] @@ -253,6 +254,11 @@ pub fn run() { commands::open_chat_window, commands::debug_println, start_monitor, + plugins::window::plugins_window_get_current_window_info, + plugins::window::plugin_window_get_display_info, + plugins::window::plugins_window_set_position, + plugins::window_persistence::plugins_window_persistence_save, + plugins::window_persistence::plugins_window_persistence_restore, stop_monitor, start_click_through, stop_click_through, diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/mod.rs new file mode 100644 index 000000000..20dea5b2d --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/mod.rs @@ -0,0 +1,2 @@ +pub mod window; +pub mod window_persistence; diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/window/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/window/mod.rs new file mode 100644 index 000000000..79d93fc69 --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/window/mod.rs @@ -0,0 +1,56 @@ +use tauri::Monitor; + +#[tauri::command] +pub async fn plugin_window_get_display_info( + window: tauri::Window +) -> Result<(Vec, Monitor), String> { + let monitors = match window.available_monitors() { + std::result::Result::Ok(monitors) => monitors, + _ => vec![], + }; + let primary_monitor = match window.primary_monitor() { + std::result::Result::Ok(monitor) => match monitor { + Some(monitor) => monitor, + None => { + return Err("Primary monitor not found".to_string()); + }, + }, + _ => return Err("Failed to get primary monitor".to_string()), + }; + + Ok((monitors, primary_monitor)) +} + +#[tauri::command] +pub async fn plugins_window_get_current_window_info( + window: tauri::Window +) -> Result<((u32, u32), (i32, i32)), String> { + match window.current_monitor() { + std::result::Result::Ok(optional_monitor) => match optional_monitor { + Some(monitor) => { + let monitor_size = monitor.size(); + let position = monitor.position(); + + Ok(( + (monitor_size.width, monitor_size.height), + (position.x, position.y), + )) + }, + _ => Ok(((0, 0), (0, 0))), + }, + _ => Ok(((0, 0), (0, 0))), + } +} + +#[tauri::command] +pub async fn plugins_window_set_position( + window: tauri::Window, + x: i32, + y: i32, +) -> Result<(), String> { + use tauri::Position; + + window + .set_position(Position::Physical(tauri::PhysicalPosition { x, y })) + .map_err(|e| format!("Failed to set window position: {}", e)) +} diff --git a/apps/stage-tamagotchi/src-tauri/src/plugins/window_persistence/mod.rs b/apps/stage-tamagotchi/src-tauri/src/plugins/window_persistence/mod.rs new file mode 100644 index 000000000..e446c9264 --- /dev/null +++ b/apps/stage-tamagotchi/src-tauri/src/plugins/window_persistence/mod.rs @@ -0,0 +1,21 @@ +use log::info; +use tauri::Window; +use tauri_plugin_window_state::{AppHandleExt, WindowExt}; + +#[tauri::command] +pub async fn plugins_window_persistence_save(app: tauri::AppHandle) -> Result<(), String> { + info!("Saving window state..."); + app + .save_window_state(tauri_plugin_window_state::StateFlags::all()) + .map_err(|e| format!("Failed to save window state: {}", e)) +} + +#[tauri::command] +pub async fn plugins_window_persistence_restore(window: Window) -> Result<(), String> { + info!("Restoring window state..."); + // The window state is automatically restored when the plugin is initialized + // This command can be used to manually trigger a restore + window + .restore_state(tauri_plugin_window_state::StateFlags::all()) + .map_err(|e| format!("Failed to restore window state: {}", e)) +} diff --git a/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts b/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts new file mode 100644 index 000000000..38e2d9966 --- /dev/null +++ b/apps/stage-tamagotchi/src/composables/tauri-window-persistence.ts @@ -0,0 +1,551 @@ +import type { + DisplayInfo, + Monitor, + Point, + Size, +} from './tauri' + +import { useDebounceFn } from '@vueuse/core' +import { computed, nextTick, onUnmounted, readonly, ref, watch } from 'vue' + +import { useAppRuntime } from './runtime' +import { useTauriCore } from './tauri' +import { useTauriPointAndWindowFrame } from './tauri-click-through' + +export interface WindowPersistenceConfig { + autoSave?: boolean + autoRestore?: boolean + savePeriod?: number // milliseconds + constrainToDisplays?: boolean + centerPointConstraint?: boolean // Use center point for boundary checks + monitorDisplayChanges?: boolean +} + +export interface WindowBoundaryConstraints { + minCenterX: number + maxCenterX: number + minCenterY: number + maxCenterY: number + recommendedMonitor: Monitor | null +} + +/** + * Enhanced window positioning system with persistence and boundary management + * Integrates with the existing tauri-click-through system + */ +export function useWindowPersistence(config: WindowPersistenceConfig = {}) { + const { + autoSave = true, + autoRestore = true, + savePeriod = 10000, + constrainToDisplays = true, + centerPointConstraint = true, + monitorDisplayChanges = true, + } = config + + const { platform } = useAppRuntime() + const { invoke } = useTauriCore() + const { windowFrame } = useTauriPointAndWindowFrame() + + // Debounced save function (declare early to avoid usage before definition) + const debouncedSave = useDebounceFn(() => savePosition(), savePeriod) + + // Reactive state + const displayInfo = ref() + const currentWindowPosition = computed(() => windowFrame.value.origin) + const currentWindowSize = computed(() => windowFrame.value.size) + const isPositioning = ref(false) + const isRestoring = ref(false) + + // State tracking (using Tauri plugins only) + const isStateRestored = ref(false) + + const primaryMonitor = computed(() => displayInfo.value?.primaryMonitor) + + const currentMonitor = computed(() => { + if (!currentWindowPosition.value || !displayInfo.value) + return null + return findMonitorContainingCenterPoint( + displayInfo.value.monitors, + getWindowCenterPoint({ + x: currentWindowPosition.value.x, + y: currentWindowPosition.value.y, + }, { + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + }), + ) + }) + + const boundaryConstraints = computed((): WindowBoundaryConstraints | null => { + if (!displayInfo.value || !currentWindowPosition.value) + return null + + const allMonitors = displayInfo.value.monitors + const windowSize = { + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + } + + return calculateBoundaryConstraints(allMonitors, windowSize, centerPointConstraint) + }) + + const isWindowInValidPosition = computed(() => { + if (!currentWindowPosition.value || !boundaryConstraints.value) + return false + + const center = getWindowCenterPoint({ + x: currentWindowPosition.value.x, + y: currentWindowPosition.value.y, + }, { + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + }) + + const constraints = boundaryConstraints.value + + return ( + center.x >= constraints.minCenterX + && center.x <= constraints.maxCenterX + && center.y >= constraints.minCenterY + && center.y <= constraints.maxCenterY + ) + }) + + // Initialize the system + async function initialize() { + if (platform.value === 'web') + return + + try { + // Get initial display information + refreshDisplayInfo() + + // Set up event listeners + setupEventListeners() + + // Auto-restore if enabled + if (autoRestore) { + restorePosition() + } + + // Set up display change monitoring + if (monitorDisplayChanges) { + setupDisplayMonitoring() + } + + // Success - using console.warn to comply with linting rules + } + catch (error) { + console.error('[WindowPersistence] Failed to initialize:', error) + } + } + + function setupEventListeners() { + watch(windowFrame, () => { + if (!isPositioning.value && !isRestoring.value) { + if (autoSave) { + debouncedSave() + } + } + }, { + deep: true, + }) + } + + function setupDisplayMonitoring() { + // Monitor for display configuration changes + // This would typically be handled by the Rust backend emitting display-changed events + // For now, we'll poll periodically as a fallback + const monitorInterval = setInterval(async () => { + try { + const [monitors, primaryMonitor] = (await invoke('plugin_window_get_display_info'))! + + const newDisplayInfo = { + monitors, + primaryMonitor, + } + + // Check for significant display changes using detailed comparison + if (displayInfo.value && hasSignificantDisplayChange(displayInfo.value, newDisplayInfo)) { + handleDisplayChange(newDisplayInfo) + } + } + catch (error) { + console.error('[WindowPersistence] Failed to monitor display changes:', error) + } + }, 10000) // Check every 10 seconds + + onUnmounted(() => { + clearInterval(monitorInterval) + }) + } + + async function handleDisplayChange(newDisplayInfo: DisplayInfo) { + // Display configuration changed + + const oldDisplayInfo = displayInfo.value + displayInfo.value = newDisplayInfo + + // Check if current window position is still valid + if (currentWindowPosition.value && constrainToDisplays) { + await nextTick() + + if (!isWindowInValidPosition.value) { + console.warn('[WindowPersistence] Window is outside valid area after display change, repositioning...') + await ensureWindowInBounds() + } + } + + // For significant display changes, the plugin will handle state invalidation + if (oldDisplayInfo && hasSignificantDisplayChange(oldDisplayInfo, newDisplayInfo)) { + console.warn('[WindowPersistence] Significant display change detected') + } + } + + // Core positioning functions + async function savePosition(): Promise { + if (!currentWindowPosition.value || !displayInfo.value) + return false + + try { + console.warn('[WindowPersistence] Saving position:', currentWindowPosition.value, currentWindowSize.value) + await invoke('plugins_window_persistence_save') + + return true + } + catch (error) { + console.error('[WindowPersistence] Failed to save position:', error) + return false + } + } + + async function restorePosition(): Promise { + if (!displayInfo.value) { + return false + } + + try { + isRestoring.value = true + + // Restore window state using Tauri plugin + await invoke('plugins_window_persistence_restore') + + // Ensure the restored position is within bounds + if (constrainToDisplays && currentWindowPosition.value) { + if (!isWindowInValidPosition.value) { + console.warn('[WindowPersistence] Restored position is not valid for current displays') + await centerOnPrimaryMonitor() + return false + } + } + + isStateRestored.value = true + return true + } + catch (error) { + console.error('[WindowPersistence] Failed to restore position:', error) + return false + } + finally { + isRestoring.value = false + } + } + + async function ensureWindowInBounds(): Promise { + if (!currentWindowPosition.value || !constrainToDisplays) + return true + + try { + const constrainedPosition = constrainPositionToBounds({ + x: currentWindowPosition.value.x, + y: currentWindowPosition.value.y, + }, { + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + }) + + if (!positionsEqual({ + x: currentWindowPosition.value.x, + y: currentWindowPosition.value.y, + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + }, { + x: constrainedPosition.x, + y: constrainedPosition.y, + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + })) { + await applyPosition(constrainedPosition) + return true + } + + return true + } + catch (error) { + console.error('[WindowPersistence] Failed to ensure window bounds:', error) + return false + } + } + + async function applyPosition(pos: Point): Promise { + if (platform.value === 'web') + return false + + try { + isPositioning.value = true + await invoke('plugins_window_set_position', { x: pos.x, y: pos.y }) + + if (autoSave) { + debouncedSave() + } + + return true + } + catch (error) { + console.error('[WindowPersistence] Failed to apply position:', error) + return false + } + finally { + isPositioning.value = false + } + } + + async function centerOnPrimaryMonitor(): Promise { + if (!primaryMonitor.value || !currentWindowSize.value) + return false + + const monitor = primaryMonitor.value + const windowSize = { + width: currentWindowSize.value.width, + height: currentWindowSize.value.height, + } + + const centerPosition: Point = { + x: monitor.workArea.position.x + (monitor.workArea.size.width - windowSize.width) / 2, + y: monitor.workArea.position.y + (monitor.workArea.size.height - windowSize.height) / 2, + } + + return await applyPosition(centerPosition) + } + + // Utility functions + function getWindowCenterPoint(pos: Point, size: Size): Point { + return { + x: pos.x + size.width / 2, + y: pos.y + size.height / 2, + } + } + + function findMonitorContainingCenterPoint(monitors: Monitor[], center: Point): Monitor | null { + return monitors.find((monitor) => { + const workArea = monitor.workArea + return ( + center.x >= workArea.position.x + && center.x <= workArea.position.x + workArea.size.width + && center.y >= workArea.position.y + && center.y <= workArea.position.y + workArea.size.height + ) + }) || null + } + + function calculateBoundaryConstraints( + monitors: Monitor[], + windowSize: Size, + useCenterPoint: boolean, + ): WindowBoundaryConstraints { + if (monitors.length === 0) { + return { + minCenterX: 0, + maxCenterX: 0, + minCenterY: 0, + maxCenterY: 0, + recommendedMonitor: null, + } + } + + // Calculate the combined bounds of all monitors + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + let bestMonitor = monitors[0] + + monitors.forEach((monitor) => { + const workArea = monitor.workArea + minX = Math.min(minX, workArea.position.x) + maxX = Math.max(maxX, workArea.position.x + workArea.size.width) + minY = Math.min(minY, workArea.position.y) + maxY = Math.max(maxY, workArea.position.y + workArea.size.height) + + // Use primary monitor from displayInfo instead of is_primary field + if (displayInfo.value && monitor.name === displayInfo.value.primaryMonitor.name) { + bestMonitor = monitor + } + }) + + if (useCenterPoint) { + // Calculate constraints for center point + return { + minCenterX: minX + windowSize.width / 2, + maxCenterX: maxX - windowSize.width / 2, + minCenterY: minY + windowSize.height / 2, + maxCenterY: maxY - windowSize.height / 2, + recommendedMonitor: bestMonitor, + } + } + else { + // Calculate constraints for top-left corner + return { + minCenterX: minX, + maxCenterX: maxX - windowSize.width, + minCenterY: minY, + maxCenterY: maxY - windowSize.height, + recommendedMonitor: bestMonitor, + } + } + } + + function constrainPositionToBounds(pos: Point, size: Size): Point { + if (!boundaryConstraints.value) + return { x: pos.x, y: pos.y } + + const constraints = boundaryConstraints.value + const center = getWindowCenterPoint(pos, size) + + // Constrain center point + const constrainedCenter: Point = { + x: Math.max(constraints.minCenterX, Math.min(constraints.maxCenterX, center.x)), + y: Math.max(constraints.minCenterY, Math.min(constraints.maxCenterY, center.y)), + } + + // Convert back to position + return { + x: constrainedCenter.x - size.width / 2, + y: constrainedCenter.y - size.height / 2, + } + } + + function isPositionValidForCurrentDisplays(pos: Point, size: Size): boolean { + if (!displayInfo.value) + return false + + const center = getWindowCenterPoint(pos, size) + return displayInfo.value.monitors.some((monitor) => { + const workArea = monitor.workArea + return ( + center.x >= workArea.position.x + && center.x <= workArea.position.x + workArea.size.width + && center.y >= workArea.position.y + && center.y <= workArea.position.y + workArea.size.height + ) + }) + } + + function hasSignificantDisplayChange(oldInfo: DisplayInfo, newInfo: DisplayInfo): boolean { + // Check if the number of monitors changed + if (oldInfo.monitors.length !== newInfo.monitors.length) + return true + + // Check if primary monitor changed + if (oldInfo.primaryMonitor.name !== newInfo.primaryMonitor.name) + return true + + // Check if any monitor resolution/position changed significantly + for (const oldMonitor of oldInfo.monitors) { + const newMonitor = newInfo.monitors.find(m => m.name === oldMonitor.name) + if (!newMonitor) + return true + + const oldWorkArea = oldMonitor.workArea + const newWorkArea = newMonitor.workArea + + if ( + Math.abs(oldWorkArea.size.width - newWorkArea.size.width) > 50 + || Math.abs(oldWorkArea.size.height - newWorkArea.size.height) > 50 + || Math.abs(oldWorkArea.position.x - newWorkArea.position.x) > 50 + || Math.abs(oldWorkArea.position.y - newWorkArea.position.y) > 50 + ) { + return true + } + } + + return false + } + + function positionsEqual(a: (Point & Size), b: (Point & Size)): boolean { + if (!a || !b) + return a === b + + return ( + Math.abs(a.x - b.x) < 1 + && Math.abs(a.y - b.y) < 1 + && Math.abs(a.width - b.width) < 1 + && Math.abs(a.height - b.height) < 1 + ) + } + + async function refreshDisplayInfo(): Promise { + if (platform.value === 'web') + return + + try { + const [monitors, primaryMonitor] = (await invoke('plugin_window_get_display_info'))! + displayInfo.value = { + monitors, + primaryMonitor, + } + } + catch (error) { + console.error('[WindowPersistence] Failed to refresh display info:', error) + } + } + + // Manual control functions + async function manualSave(): Promise { + return await savePosition() + } + + async function manualRestore(): Promise { + return await restorePosition() + } + + function clearPersistedState(): void { + isStateRestored.value = false + // Plugin will handle clearing persisted state internally + console.warn('[WindowPersistence] State marked as cleared locally') + } + + return { + // State + displayInfo: readonly(displayInfo), + currentWindowPosition: readonly(currentWindowPosition), + windowFrame: readonly(windowFrame), + isPositioning: readonly(isPositioning), + isRestoring: readonly(isRestoring), + isStateRestored: readonly(isStateRestored), + + // Computed + primaryMonitor, + currentMonitor, + boundaryConstraints, + isWindowInValidPosition, + + // Core functions + initialize, + savePosition: manualSave, + restorePosition: manualRestore, + ensureWindowInBounds, + applyPosition, + centerOnPrimaryMonitor, + + // Utilities + refreshDisplayInfo, + clearPersistedState, + getWindowCenterPoint, + + // Internal utilities (exposed for debugging) + constrainPositionToBounds, + isPositionValidForCurrentDisplays, + } +} diff --git a/apps/stage-tamagotchi/src/composables/tauri.ts b/apps/stage-tamagotchi/src/composables/tauri.ts index 8622c57a3..fcfdf16d2 100644 --- a/apps/stage-tamagotchi/src/composables/tauri.ts +++ b/apps/stage-tamagotchi/src/composables/tauri.ts @@ -52,33 +52,23 @@ interface Events { export interface AiriTamagotchiEvents extends Events { 'tauri-app:window-click-through:position-cursor-and-window-frame': [Point, WindowFrame] 'tauri-app:model-load-progress': [string, number] - 'tauri-app:window-position-changed': WindowPosition - 'tauri-app:window-state-saved': undefined - 'tauri-app:window-state-restored': undefined - 'tauri-app:display-changed': DisplayInfo 'mcp_plugin_destroyed': undefined - 'tauri-app:invoke-returns:plugins-window-get-display-info': [[number, number], [number, number]] } export interface Monitor { - id: number name: string - is_primary: boolean - bounds: WindowFrame - work_area: WindowFrame + size: { width: number, height: number } + position: { x: number, y: number } + workArea: { + position: { x: number, y: number } + size: { width: number, height: number } + } scale_factor: number } export interface DisplayInfo { monitors: Monitor[] - primary_monitor_id: number -} - -export interface WindowPosition { - x: number - y: number - width: number - height: number + primaryMonitor: Monitor } export enum PlacementStrategy { @@ -99,7 +89,7 @@ export interface WindowPlacementRequest { } export interface WindowPlacementResult { - position: WindowPosition + position: Point target_monitor_id: number is_constrained: boolean } @@ -149,14 +139,19 @@ export function useTauriEvent() { } export interface InvokeMethods { - open_settings_window: { args: undefined, options: undefined, returns: void } + // Model related + load_models: { args: undefined, options: undefined, returns: void } + + // Click Through start_monitor: { args: undefined, options: undefined, returns: void } stop_monitor: { args: undefined, options: undefined, returns: void } - open_chat_window: { args: undefined, options: undefined, returns: void } - load_models: { args: undefined, options: undefined, returns: void } stop_click_through: { args: undefined, options: undefined, returns: void } start_click_through: { args: undefined, options: undefined, returns: void } + // Windows + open_settings_window: { args: undefined, options: undefined, returns: void } + open_chat_window: { args: undefined, options: undefined, returns: void } + // WindowLink.vue open_route_in_window: { args: { route: string, windowLabel?: string } | undefined @@ -165,35 +160,30 @@ export interface InvokeMethods { } // Window positioning methods - plugins_window_get_display_info: { + plugin_window_get_display_info: { args: undefined options: undefined - returns: DisplayInfo - } - plugins_window_calculate_window_placement: { - args: { request: WindowPlacementRequest } - options: undefined - returns: WindowPlacementResult - } - plugins_window_apply_window_position: { - args: { position: WindowPosition } - options: undefined - returns: void - } - plugins_window_save_window_state: { - args: undefined - options: undefined - returns: void - } - plugins_window_restore_window_state: { - args: undefined - options: undefined - returns: void + returns: [Monitor[], Monitor] } plugins_window_get_current_window_info: { args: undefined options: undefined - returns: WindowPosition + returns: [[number, number], [number, number]] + } + plugins_window_set_position: { + args: { x: number, y: number } + options: undefined + returns: void + } + plugins_window_persistence_save: { + args: undefined + options: undefined + returns: void + } + plugins_window_persistence_restore: { + args: undefined + options: undefined + returns: void } } diff --git a/apps/stage-tamagotchi/src/pages/index.vue b/apps/stage-tamagotchi/src/pages/index.vue index b33c0c9f6..ea8bb0b19 100644 --- a/apps/stage-tamagotchi/src/pages/index.vue +++ b/apps/stage-tamagotchi/src/pages/index.vue @@ -11,6 +11,7 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue' import ResourceStatusIsland from '../components/Widgets/ResourceStatusIsland/index.vue' import { useTauriCore, useTauriEvent } from '../composables/tauri' import { useTauriWindowClickThrough } from '../composables/tauri-click-through' +import { useWindowPersistence } from '../composables/tauri-window-persistence' import { useWindowShortcuts } from '../composables/window-shortcuts' import { useResourcesStore } from '../stores/resources' import { useWindowControlStore } from '../stores/window-controls' @@ -34,15 +35,14 @@ const { connected, serverCmd, serverArgs } = storeToRefs(mcpStore) watch([live2dLookAtX, live2dLookAtY], ([x, y]) => live2dFocusAt.value = { x, y }, { immediate: true }) -// // Initialize window persistence system -// const windowPersistence = useWindowPersistence({ -// autoSave: true, -// autoRestore: true, -// constrainToDisplays: true, -// centerPointConstraint: true, -// scaleAware: true, -// monitorDisplayChanges: true -// }) +const windowPersistence = useWindowPersistence({ + autoSave: true, + autoRestore: true, + constrainToDisplays: true, + centerPointConstraint: true, + monitorDisplayChanges: true, + +}) const modeIndicatorClass = computed(() => { switch (windowStore.controlMode) { @@ -60,6 +60,7 @@ const modeIndicatorClass = computed(() => { onMounted(async () => { await invoke('start_monitor') await startClickThrough() + await windowPersistence.initialize() }) onUnmounted(async () => {