diff --git a/.github/workflows/sync-upstream.yml b/.github/workflows/sync-upstream.yml new file mode 100644 index 00000000..e1be7d01 --- /dev/null +++ b/.github/workflows/sync-upstream.yml @@ -0,0 +1,45 @@ +name: Sync with Upstream + +on: + schedule: + - cron: '0 6 * * *' + workflow_dispatch: + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: feat/gtk4 + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Add upstream remote + run: git remote add upstream https://github.com/tauri-apps/muda.git || true + + - name: Fetch upstream + run: git fetch upstream + + - name: Check and merge upstream + run: | + BEHIND=$(git rev-list --count HEAD..upstream/feat/gtk4) + if [ "$BEHIND" -gt 0 ]; then + echo "Found $BEHIND new commits from upstream" + if git merge upstream/feat/gtk4 --no-edit; then + echo "Successfully merged upstream changes" + git push origin feat/gtk4 + else + echo "Merge conflicts detected - manual intervention required" + git merge --abort + exit 1 + fi + else + echo "Already up to date with upstream" + fi diff --git a/Cargo.lock b/Cargo.lock index 2ee82412..da694bc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ab_glyph" @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.8.0", + "bitflags 2.10.0", "cc", "cesu8", "jni", @@ -82,6 +82,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -90,7 +96,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -189,11 +195,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -268,7 +274,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "cairo-sys-rs 0.18.2", "glib 0.18.5", "libc", @@ -278,13 +284,13 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" +checksum = "b01fe135c0bd16afe262b6dea349bd5ea30e6de50708cec639aae7c5c14cc7e4" dependencies = [ - "bitflags 2.8.0", - "cairo-sys-rs 0.20.7", - "glib 0.20.7", + "bitflags 2.10.0", + "cairo-sys-rs 0.21.5", + "glib 0.21.5", "libc", ] @@ -301,11 +307,11 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df" +checksum = "06c28280c6b12055b5e39e4554271ae4e6630b27c0da9148c4cf6485fc6d245c" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.21.5", "libc", "system-deps 7.0.3", ] @@ -316,7 +322,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "log", "polling", "rustix", @@ -391,7 +397,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block", "cocoa-foundation", "core-foundation 0.10.0", @@ -407,7 +413,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -491,7 +497,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -515,7 +521,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "core-foundation 0.10.0", "libc", ] @@ -612,7 +618,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -631,7 +637,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -658,7 +664,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -690,7 +696,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -819,7 +825,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -887,7 +893,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -949,13 +955,13 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6efc7705f7863d37b12ad6974cbb310d35d054f5108cdc1e69037742f573c4c" +checksum = "debb0d39e3cdd84626edfd54d6e4a6ba2da9a0ef2e796e691c4e9f8646fda00c" dependencies = [ - "gdk-pixbuf-sys 0.20.7", - "gio 0.20.7", - "glib 0.20.7", + "gdk-pixbuf-sys 0.21.5", + "gio 0.21.5", + "glib 0.21.5", "libc", ] @@ -974,13 +980,13 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f2587c9202bf997476bbba6aaed4f78a11538a2567df002a5f57f5331d0b5c" +checksum = "bd95ad50b9a3d2551e25dd4f6892aff0b772fe5372d84514e9d0583af60a0ce7" dependencies = [ - "gio-sys 0.20.8", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "gio-sys 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "libc", "system-deps 7.0.3", ] @@ -1004,32 +1010,32 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0196720118f880f71fe7da971eff58cc43a89c9cf73f46076b7cb1e60889b15" +checksum = "756564212bbe4a4ce05d88ffbd2582581ac6003832d0d32822d0825cca84bfbf" dependencies = [ - "cairo-rs 0.20.7", - "gdk-pixbuf 0.20.7", + "cairo-rs 0.21.5", + "gdk-pixbuf 0.21.5", "gdk4-sys", - "gio 0.20.7", - "glib 0.20.7", + "gio 0.21.5", + "glib 0.21.5", "libc", - "pango 0.20.7", + "pango 0.21.5", ] [[package]] name = "gdk4-sys" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b0e1340bd15e7a78810cf39fed9e5d85f0a8f80b1d999d384ca17dcc452b60" +checksum = "a6d4e5b3ccf591826a4adcc83f5f57b4e59d1925cb4bf620b0d645f79498b034" dependencies = [ - "cairo-sys-rs 0.20.7", - "gdk-pixbuf-sys 0.20.7", - "gio-sys 0.20.8", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "cairo-sys-rs 0.21.5", + "gdk-pixbuf-sys 0.21.5", + "gio-sys 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "libc", - "pango-sys 0.20.7", + "pango-sys 0.21.5", "pkg-config", "system-deps 7.0.3", ] @@ -1148,16 +1154,16 @@ dependencies = [ [[package]] name = "gio" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a517657589a174be9f60c667f1fec8b7ac82ed5db4ebf56cf073a3b5955d8e2e" +checksum = "c5ff48bf600c68b476e61dc6b7c762f2f4eb91deef66583ba8bb815c30b5811a" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-util", - "gio-sys 0.20.8", - "glib 0.20.7", + "gio-sys 0.21.5", + "glib 0.21.5", "libc", "pin-project-lite", "smallvec", @@ -1178,12 +1184,12 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.8" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" dependencies = [ - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "libc", "system-deps 7.0.3", "windows-sys 0.59.0", @@ -1195,7 +1201,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "futures-channel", "futures-core", "futures-executor", @@ -1214,20 +1220,20 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "futures-channel", "futures-core", "futures-executor", "futures-task", "futures-util", - "gio-sys 0.20.8", - "glib-macros 0.20.7", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "gio-sys 0.21.5", + "glib-macros 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "libc", "memchr", "smallvec", @@ -1244,20 +1250,20 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] name = "glib-macros" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" dependencies = [ "heck 0.5.0", - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -1272,9 +1278,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" dependencies = [ "libc", "system-deps 7.0.3", @@ -1293,33 +1299,33 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.21.5", "libc", "system-deps 7.0.3", ] [[package]] name = "graphene-rs" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39d3bcd2e24fd9c2874a56f277b72c03e728de9bdc95a8d4ef4c962f10ced98" +checksum = "2730030ac9db663fd8bfe1e7093742c1cafb92db9c315c9417c29032341fe2f9" dependencies = [ - "glib 0.20.7", + "glib 0.21.5", "graphene-sys", "libc", ] [[package]] name = "graphene-sys" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a68d39515bf340e879b72cecd4a25c1332557757ada6e8aba8654b4b81d23a" +checksum = "915e32091ea9ad241e4b044af62b7351c2d68aeb24f489a0d7f37a0fc484fd93" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.21.5", "libc", "pkg-config", "system-deps 7.0.3", @@ -1327,32 +1333,32 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b9188db0a6219e708b6b6e7225718e459def664023dbddb8395ca1486d8102" +checksum = "e755de9d8c5896c5beaa028b89e1969d067f1b9bf1511384ede971f5983aa153" dependencies = [ - "cairo-rs 0.20.7", + "cairo-rs 0.21.5", "gdk4", - "glib 0.20.7", + "glib 0.21.5", "graphene-rs", "gsk4-sys", "libc", - "pango 0.20.7", + "pango 0.21.5", ] [[package]] name = "gsk4-sys" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca10fc65d68528a548efa3d8747934adcbe7058b73695c9a7f43a25352fce14" +checksum = "7ce91472391146f482065f1041876d8f869057b195b95399414caa163d72f4f7" dependencies = [ - "cairo-sys-rs 0.20.7", + "cairo-sys-rs 0.21.5", "gdk4-sys", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "graphene-sys", "libc", - "pango-sys 0.20.7", + "pango-sys 0.21.5", "system-deps 7.0.3", ] @@ -1405,58 +1411,58 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] name = "gtk4" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697ff938136625f6acf75f01951220f47a45adcf0060ee55b4671cf734dac44" +checksum = "acb21d53cfc6f7bfaf43549731c43b67ca47d87348d81c8cfc4dcdd44828e1a4" dependencies = [ - "cairo-rs 0.20.7", + "cairo-rs 0.21.5", "field-offset", "futures-channel", - "gdk-pixbuf 0.20.7", + "gdk-pixbuf 0.21.5", "gdk4", - "gio 0.20.7", - "glib 0.20.7", + "gio 0.21.5", + "glib 0.21.5", "graphene-rs", "gsk4", "gtk4-macros", "gtk4-sys", "libc", - "pango 0.20.7", + "pango 0.21.5", ] [[package]] name = "gtk4-macros" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "3ccfb5a14a3d941244815d5f8101fa12d4577b59cc47245778d8d907b0003e42" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] name = "gtk4-sys" -version = "0.9.5" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af4b680cee5d2f786a2f91f1c77e95ecf2254522f0ca4edf3a2dce6cb35cecf" +checksum = "842577fe5a1ee15d166cd3afe804ce0cab6173bc789ca32e21308834f20088dd" dependencies = [ - "cairo-sys-rs 0.20.7", - "gdk-pixbuf-sys 0.20.7", + "cairo-sys-rs 0.21.5", + "gdk-pixbuf-sys 0.21.5", "gdk4-sys", - "gio-sys 0.20.8", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "gio-sys 0.21.5", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "graphene-sys", "gsk4-sys", "libc", - "pango-sys 0.20.7", + "pango-sys 0.21.5", "system-deps 7.0.3", ] @@ -1478,9 +1484,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1640,7 +1646,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -1715,12 +1721,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.16.1", ] [[package]] @@ -1740,7 +1746,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -1840,7 +1846,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "serde", "unicode-segmentation", ] @@ -1902,7 +1908,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "libc", "redox_syscall 0.5.8", ] @@ -1991,9 +1997,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memmap2" @@ -2031,8 +2037,9 @@ dependencies = [ [[package]] name = "muda" -version = "0.16.1" +version = "0.17.1" dependencies = [ + "arc-swap", "crossbeam-channel", "dpi", "gtk4", @@ -2058,7 +2065,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "jni-sys", "log", "ndk-sys", @@ -2128,7 +2135,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -2175,10 +2182,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 2.0.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -2222,7 +2229,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "libc", "objc2 0.5.2", @@ -2238,7 +2245,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "objc2 0.6.0", "objc2-core-foundation", "objc2-foundation 0.3.0", @@ -2250,7 +2257,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -2274,7 +2281,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2286,7 +2293,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "objc2 0.6.0", ] @@ -2326,7 +2333,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "dispatch", "libc", @@ -2339,7 +2346,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "objc2 0.6.0", "objc2-core-foundation", ] @@ -2362,7 +2369,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2374,7 +2381,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2397,7 +2404,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-cloud-kit", @@ -2429,7 +2436,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -2493,14 +2500,14 @@ dependencies = [ [[package]] name = "pango" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e89bd74250a03a05cec047b43465469102af803be2bf5e5a1088f8b8455e087" +checksum = "52d1d85e2078077a065bb7fc072783d5bcd4e51b379f22d67107d0a16937eb69" dependencies = [ - "gio 0.20.7", - "glib 0.20.7", + "gio 0.21.5", + "glib 0.21.5", "libc", - "pango-sys 0.20.7", + "pango-sys 0.21.5", ] [[package]] @@ -2517,12 +2524,12 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.7" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71787e0019b499a5eda889279e4adb455a4f3fdd6870cd5ab7f4a5aa25df6699" +checksum = "b4f06627d36ed5ff303d2df65211fc2e52ba5b17bf18dd80ff3d9628d6e06cfd" dependencies = [ - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.21.5", + "gobject-sys 0.21.5", "libc", "system-deps 7.0.3", ] @@ -2671,7 +2678,7 @@ checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -2688,9 +2695,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "png" @@ -2756,11 +2763,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.22.23", + "toml_edit 0.23.9", ] [[package]] @@ -2818,7 +2825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3026,7 +3033,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", ] [[package]] @@ -3050,7 +3057,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "errno", "libc", "linux-raw-sys", @@ -3125,22 +3132,32 @@ checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3211,9 +3228,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "smithay-client-toolkit" @@ -3221,7 +3238,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -3326,9 +3343,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -3343,7 +3360,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3378,7 +3395,7 @@ version = "0.30.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6682a07cf5bab0b8a2bd20d0a542917ab928b5edb75ebd4eda6b05cbaab872da" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "cocoa", "core-foundation 0.10.0", "core-graphics 0.24.0", @@ -3419,7 +3436,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3471,7 +3488,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3482,7 +3499,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -3539,7 +3556,7 @@ checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.8", "toml_edit 0.20.2", ] @@ -3552,14 +3569,23 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.7.1", - "toml_datetime", + "indexmap 2.12.1", + "toml_datetime 0.6.8", "winnow 0.5.40", ] @@ -3569,22 +3595,32 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.12.1", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.8", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ - "indexmap 2.7.1", - "toml_datetime", - "winnow 0.7.0", + "winnow 0.7.14", ] [[package]] @@ -3723,7 +3759,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", "wasm-bindgen-shared", ] @@ -3758,7 +3794,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3792,7 +3828,7 @@ version = "0.31.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "rustix", "wayland-backend", "wayland-scanner", @@ -3804,7 +3840,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "cursor-icon", "wayland-backend", ] @@ -3826,7 +3862,7 @@ version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -3838,7 +3874,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3851,7 +3887,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3967,7 +4003,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -4049,7 +4085,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -4060,7 +4096,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -4305,7 +4341,7 @@ dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.8.0", + "bitflags 2.10.0", "block2", "bytemuck", "calloop", @@ -4359,9 +4395,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.0" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -4472,7 +4508,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.10.0", "dlib", "log", "once_cell", @@ -4505,7 +4541,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", "synstructure", ] @@ -4527,7 +4563,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] @@ -4547,7 +4583,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", "synstructure", ] @@ -4570,7 +4606,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fd98cec5..4a5ae045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "muda" -version = "0.16.1" +version = "0.17.1" description = "Menu Utilities for Desktop Applications" edition = "2021" keywords = ["windowing", "menu"] @@ -9,12 +9,18 @@ readme = "README.md" repository = "https://github.com/amrbashir/muda" documentation = "https://docs.rs/muda" categories = ["gui"] -rust-version = "1.71" +rust-version = "1.83" [features] default = [] common-controls-v6 = [] serde = ["dep:serde", "dpi/serde"] +# ksni support for tray-icon on Linux (GTK-agnostic DBus tray) +linux-ksni = ["dep:arc-swap"] +# No-op for GTK4 - libxdo is X11-only, GTK4 is Wayland-focused +libxdo = [] +# No-op - GTK4 is always used on Linux, this exists for API compatibility +gtk = [] [dependencies] crossbeam-channel = "0.5" @@ -41,8 +47,9 @@ features = [ ] [target.'cfg(target_os = "linux")'.dependencies] -gtk4 = "0.9" +gtk4 = "0.10" png = "0.17" +arc-swap = { version = "1.7.1", optional = true } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.0" diff --git a/src/items/check.rs b/src/items/check.rs index 8ab4a056..1d984b44 100644 --- a/src/items/check.rs +++ b/src/items/check.rs @@ -4,8 +4,16 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use super::compat::{CompatMenuItem, CompatCheckmarkItem, strip_mnemonic}; + /// A check menu item inside a [`Menu`] or [`Submenu`] /// and usually contains a text and a check mark or a similar toggle /// that corresponds to a checked and unchecked states. @@ -16,6 +24,8 @@ use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId pub struct CheckMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for CheckMenuItem {} @@ -51,9 +61,19 @@ impl CheckMenuItem { accelerator, None, ); + let id = item.id().clone(); Self { - id: Rc::new(item.id().clone()), + id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Checkmark( + CompatCheckmarkItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + checked, + }, + ))), } } @@ -76,7 +96,16 @@ impl CheckMenuItem { enabled, checked, accelerator, - Some(id), + Some(id.clone()), + ))), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Checkmark( + CompatCheckmarkItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + checked, + }, ))), } } diff --git a/src/items/compat.rs b/src/items/compat.rs new file mode 100644 index 00000000..f04b48ca --- /dev/null +++ b/src/items/compat.rs @@ -0,0 +1,167 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! GTK-agnostic menu item representations for ksni tray support. +//! +//! These types provide a platform-independent way to represent menu items +//! that can be consumed by the ksni DBus tray implementation. +//! +//! These types are intentionally Send+Sync safe for use across threads +//! (e.g., ksni's DBus thread). + +use std::sync::Arc; + +use arc_swap::ArcSwap; + +/// Thread-safe about metadata for ksni tray About dialogs. +/// +/// This is a Send+Sync safe subset of `AboutMetadata` without the `Icon` field. +#[derive(Debug, Clone, Default)] +pub struct CompatAboutMetadata { + /// The application name. + pub name: Option, + /// The application version. + pub version: Option, + /// The short version. + pub short_version: Option, + /// The application authors. + pub authors: Option>, + /// Application comments. + pub comments: Option, + /// The copyright. + pub copyright: Option, + /// The license text. + pub license: Option, + /// The website URL. + pub website: Option, + /// The website label. + pub website_label: Option, + /// Credits text. + pub credits: Option, +} + +impl CompatAboutMetadata { + /// Creates a CompatAboutMetadata from an AboutMetadata reference. + pub fn from_about_metadata(meta: &crate::AboutMetadata) -> Self { + Self { + name: meta.name.clone(), + version: meta.version.clone(), + short_version: meta.short_version.clone(), + authors: meta.authors.clone(), + comments: meta.comments.clone(), + copyright: meta.copyright.clone(), + license: meta.license.clone(), + website: meta.website.clone(), + website_label: meta.website_label.clone(), + credits: meta.credits.clone(), + } + } +} + +/// A standard menu item with an optional icon and predefined behavior. +/// +/// This type is Send+Sync safe for cross-thread use. +#[derive(Debug, Clone)] +pub struct CompatStandardItem { + /// Unique identifier for this menu item. + pub id: String, + /// Display label (mnemonics already stripped). + pub label: String, + /// Whether the item is enabled/clickable. + pub enabled: bool, + /// Optional icon as PNG bytes. + pub icon: Option>, + /// If this is a predefined menu item, a string identifier for its kind. + /// Examples: "separator", "copy", "cut", "paste", "quit", "about", etc. + /// None for regular menu items. + pub predefined_item_id: Option, + /// About metadata for "about" predefined items. + /// Only populated when predefined_item_id is Some("about"). + pub about_metadata: Option, +} + +/// A checkmark/toggle menu item. +#[derive(Debug, Clone)] +pub struct CompatCheckmarkItem { + /// Unique identifier for this menu item. + pub id: String, + /// Display label (mnemonics already stripped). + pub label: String, + /// Whether the item is enabled/clickable. + pub enabled: bool, + /// Whether the item is currently checked. + pub checked: bool, +} + +/// A submenu containing child items. +#[derive(Debug, Clone)] +pub struct CompatSubMenuItem { + /// Display label (mnemonics already stripped). + pub label: String, + /// Whether the submenu is enabled. + pub enabled: bool, + /// Child menu items. + pub submenu: Vec>>, +} + +/// A menu item that can be one of several types. +#[derive(Debug, Clone)] +pub enum CompatMenuItem { + /// A standard clickable menu item. + Standard(CompatStandardItem), + /// A checkmark/toggle menu item. + Checkmark(CompatCheckmarkItem), + /// A submenu containing child items. + SubMenu(CompatSubMenuItem), + /// A separator line. + Separator, +} + +impl From for CompatMenuItem { + fn from(item: CompatStandardItem) -> Self { + CompatMenuItem::Standard(item) + } +} + +impl From for CompatMenuItem { + fn from(item: CompatCheckmarkItem) -> Self { + CompatMenuItem::Checkmark(item) + } +} + +impl From for CompatMenuItem { + fn from(item: CompatSubMenuItem) -> Self { + CompatMenuItem::SubMenu(item) + } +} + +/// Removes mnemonic markers (&) from a label string. +/// +/// - Single `&` is removed (e.g., "H&ello" -> "Hello") +/// - Double `&&` becomes a literal `&` (e.g., "Save && Exit" -> "Save & Exit") +/// +/// This is used when populating compat labels for ksni, which doesn't +/// support GTK-style mnemonic markers. +pub fn strip_mnemonic(text: impl AsRef) -> String { + text.as_ref() + .replace("&&", "\x00") // Temporarily replace && with null + .replace('&', "") // Remove single & + .replace('\x00', "&") // Restore && as single & +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_strip_mnemonic() { + assert_eq!(strip_mnemonic("Hello"), "Hello"); + assert_eq!(strip_mnemonic("H&ello"), "Hello"); + assert_eq!(strip_mnemonic("&Hello"), "Hello"); + assert_eq!(strip_mnemonic("Hello&"), "Hello"); + assert_eq!(strip_mnemonic("Save && Exit"), "Save & Exit"); + assert_eq!(strip_mnemonic("&&Hello"), "&Hello"); + assert_eq!(strip_mnemonic("H&&ello"), "H&ello"); + } +} diff --git a/src/items/icon.rs b/src/items/icon.rs index 907a5492..c36d4671 100644 --- a/src/items/icon.rs +++ b/src/items/icon.rs @@ -4,6 +4,11 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ accelerator::Accelerator, icon::{Icon, NativeIcon}, @@ -11,6 +16,9 @@ use crate::{ IsMenuItem, MenuId, MenuItemKind, }; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use super::compat::{CompatMenuItem, CompatStandardItem, strip_mnemonic}; + /// An icon menu item inside a [`Menu`] or [`Submenu`] /// and usually contains an icon and a text. /// @@ -20,6 +28,8 @@ use crate::{ pub struct IconMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for IconMenuItem {} @@ -48,6 +58,8 @@ impl IconMenuItem { icon: Option, accelerator: Option, ) -> Self { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let icon_bytes = icon.as_ref().map(|i| i.inner.png_data().to_vec()); let item = crate::platform_impl::MenuChild::new_icon( text.as_ref(), enabled, @@ -55,9 +67,21 @@ impl IconMenuItem { accelerator, None, ); + let id = item.id().clone(); Self { - id: Rc::new(item.id().clone()), + id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + icon: icon_bytes, + predefined_item_id: None, + about_metadata: None, + }, + ))), } } @@ -73,6 +97,8 @@ impl IconMenuItem { accelerator: Option, ) -> Self { let id = id.into(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let icon_bytes = icon.as_ref().map(|i| i.inner.png_data().to_vec()); Self { id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new_icon( @@ -80,7 +106,18 @@ impl IconMenuItem { enabled, icon, accelerator, - Some(id), + Some(id.clone()), + ))), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + icon: icon_bytes, + predefined_item_id: None, + about_metadata: None, + }, ))), } } @@ -105,9 +142,21 @@ impl IconMenuItem { accelerator, None, ); + let id = item.id().clone(); Self { - id: Rc::new(item.id().clone()), + id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + icon: None, + predefined_item_id: None, + about_metadata: None, + }, + ))), } } @@ -134,9 +183,20 @@ impl IconMenuItem { enabled, native_icon, accelerator, - Some(id), + Some(id.clone()), ), )), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + icon: None, + predefined_item_id: None, + about_metadata: None, + }, + ))), } } diff --git a/src/items/mod.rs b/src/items/mod.rs index 39c9b157..855d67c0 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -8,12 +8,18 @@ mod normal; mod predefined; mod submenu; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +mod compat; + pub use check::*; pub use icon::*; pub use normal::*; pub use predefined::*; pub use submenu::*; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub use compat::*; + #[cfg(test)] mod test { use crate::{CheckMenuItem, IconMenuItem, MenuId, MenuItem, PredefinedMenuItem, Submenu}; diff --git a/src/items/normal.rs b/src/items/normal.rs index 8e1499d5..04650546 100644 --- a/src/items/normal.rs +++ b/src/items/normal.rs @@ -1,7 +1,15 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use super::compat::{CompatMenuItem, CompatStandardItem, strip_mnemonic}; + /// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. /// /// [`Menu`]: crate::Menu @@ -10,6 +18,8 @@ use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId pub struct MenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for MenuItem {} @@ -33,15 +43,22 @@ impl MenuItem { /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic /// for this menu item. To display a `&` without assigning a mnemenonic, use `&&`. pub fn new>(text: S, enabled: bool, accelerator: Option) -> Self { - let item = crate::platform_impl::MenuChild::new_menu_item( - text.as_ref(), - enabled, - accelerator, - None, - ); + let item = crate::platform_impl::MenuChild::new(text.as_ref(), enabled, accelerator, None); + let id = item.id().clone(); Self { - id: Rc::new(item.id().clone()), + id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), + enabled, + icon: None, + predefined_item_id: None, + about_metadata: None, + }, + ))), } } @@ -58,14 +75,23 @@ impl MenuItem { let id = id.into(); Self { id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new( - crate::platform_impl::MenuChild::new_menu_item( - text.as_ref(), + inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new( + text.as_ref(), + enabled, + accelerator, + Some(id.clone()), + ))), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(text.as_ref()), enabled, - accelerator, - Some(id), - ), - )), + icon: None, + predefined_item_id: None, + about_metadata: None, + }, + ))), } } diff --git a/src/items/predefined.rs b/src/items/predefined.rs index d229b0f2..11e078b5 100644 --- a/src/items/predefined.rs +++ b/src/items/predefined.rs @@ -4,6 +4,11 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ accelerator::{Accelerator, CMD_OR_CTRL}, sealed::IsMenuItemBase, @@ -11,11 +16,16 @@ use crate::{ }; use keyboard_types::{Code, Modifiers}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use super::compat::{CompatMenuItem, CompatStandardItem, strip_mnemonic}; + /// A predefined (native) menu item which has a predfined behavior by the OS or by this crate. #[derive(Clone)] pub struct PredefinedMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for PredefinedMenuItem {} @@ -36,27 +46,27 @@ impl IsMenuItem for PredefinedMenuItem { impl PredefinedMenuItem { /// Separator menu item pub fn separator() -> PredefinedMenuItem { - PredefinedMenuItem::new::<&str>(PredefinedMenuItemType::Separator, None) + PredefinedMenuItem::new::<&str>(PredefinedMenuItemKind::Separator, None) } /// Copy menu item pub fn copy(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Copy, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Copy, text) } /// Cut menu item pub fn cut(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Cut, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Cut, text) } /// Paste menu item pub fn paste(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Paste, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Paste, text) } /// SelectAll menu item pub fn select_all(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::SelectAll, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::SelectAll, text) } /// Undo menu item @@ -65,7 +75,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn undo(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Undo, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Undo, text) } /// Redo menu item /// @@ -73,7 +83,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn redo(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Redo, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Redo, text) } /// Minimize window menu item @@ -82,7 +92,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn minimize(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Minimize, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Minimize, text) } /// Maximize window menu item @@ -91,7 +101,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn maximize(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Maximize, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Maximize, text) } /// Fullscreen menu item @@ -100,7 +110,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn fullscreen(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Fullscreen, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Fullscreen, text) } /// Hide window menu item @@ -109,7 +119,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn hide(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Hide, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Hide, text) } /// Hide other windows menu item @@ -118,7 +128,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn hide_others(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::HideOthers, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::HideOthers, text) } /// Show all app windows menu item @@ -127,7 +137,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn show_all(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::ShowAll, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::ShowAll, text) } /// Close window menu item @@ -136,7 +146,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn close_window(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::CloseWindow, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::CloseWindow, text) } /// Quit app menu item @@ -145,12 +155,12 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn quit(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Quit, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Quit, text) } /// About app menu item pub fn about(text: Option<&str>, metadata: Option) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::About(metadata), text) + PredefinedMenuItem::new(PredefinedMenuItemKind::About(metadata), text) } /// Services menu item @@ -159,7 +169,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn services(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Services, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Services, text) } /// 'Bring all to front' menu item @@ -168,17 +178,35 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn bring_all_to_front(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::BringAllToFront, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::BringAllToFront, text) } - fn new>(item: PredefinedMenuItemType, text: Option) -> Self { + fn new>(item_type: PredefinedMenuItemKind, text: Option) -> Self { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let is_separator = matches!(item_type, PredefinedMenuItemKind::Separator); let item = crate::platform_impl::MenuChild::new_predefined( - item, - text.map(|t| t.as_ref().to_string()), + item_type, + text.as_ref().map(|t| t.as_ref().to_string()), ); + let id = item.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let label = text.as_ref().map(|t| strip_mnemonic(t.as_ref())).unwrap_or_else(|| item.text()); Self { - id: Rc::new(item.id().clone()), + id: Rc::new(id.clone()), inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(if is_separator { + CompatMenuItem::Separator + } else { + CompatMenuItem::Standard(CompatStandardItem { + id: id.0.clone(), + label, + enabled: true, // Predefined items are always enabled + icon: None, + predefined_item_id: None, // TODO: populate from predefined type + about_metadata: None, + }) + })), } } @@ -241,7 +269,7 @@ fn test_about_metadata() { #[derive(Debug, Clone)] #[non_exhaustive] #[allow(clippy::large_enum_variant)] -pub(crate) enum PredefinedMenuItemType { +pub enum PredefinedMenuItemKind { Separator, Copy, Cut, @@ -263,76 +291,77 @@ pub(crate) enum PredefinedMenuItemType { None, } -impl Default for PredefinedMenuItemType { +impl Default for PredefinedMenuItemKind { fn default() -> Self { Self::None } } -impl PredefinedMenuItemType { +impl PredefinedMenuItemKind { pub(crate) fn text(&self) -> &str { match self { - PredefinedMenuItemType::Separator => "", - PredefinedMenuItemType::Copy => "&Copy", - PredefinedMenuItemType::Cut => "Cu&t", - PredefinedMenuItemType::Paste => "&Paste", - PredefinedMenuItemType::SelectAll => "Select &All", - PredefinedMenuItemType::Undo => "Undo", - PredefinedMenuItemType::Redo => "Redo", - PredefinedMenuItemType::Minimize => "&Minimize", + PredefinedMenuItemKind::Separator => "", + PredefinedMenuItemKind::Copy => "&Copy", + PredefinedMenuItemKind::Cut => "Cu&t", + PredefinedMenuItemKind::Paste => "&Paste", + PredefinedMenuItemKind::SelectAll => "Select &All", + PredefinedMenuItemKind::Undo => "Undo", + PredefinedMenuItemKind::Redo => "Redo", + PredefinedMenuItemKind::Minimize => "&Minimize", #[cfg(target_os = "macos")] - PredefinedMenuItemType::Maximize => "Zoom", + PredefinedMenuItemKind::Maximize => "Zoom", #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::Maximize => "Ma&ximize", - PredefinedMenuItemType::Fullscreen => "Toggle Full Screen", - PredefinedMenuItemType::Hide => "&Hide", - PredefinedMenuItemType::HideOthers => "Hide Others", - PredefinedMenuItemType::ShowAll => "Show All", + PredefinedMenuItemKind::Maximize => "Ma&ximize", + PredefinedMenuItemKind::Fullscreen => "Toggle Full Screen", + PredefinedMenuItemKind::Hide => "&Hide", + PredefinedMenuItemKind::HideOthers => "Hide Others", + PredefinedMenuItemKind::ShowAll => "Show All", #[cfg(windows)] - PredefinedMenuItemType::CloseWindow => "Close", + PredefinedMenuItemKind::CloseWindow => "Close", #[cfg(not(windows))] - PredefinedMenuItemType::CloseWindow => "C&lose Window", + PredefinedMenuItemKind::CloseWindow => "C&lose Window", #[cfg(windows)] - PredefinedMenuItemType::Quit => "&Exit", + PredefinedMenuItemKind::Quit => "&Exit", #[cfg(not(windows))] - PredefinedMenuItemType::Quit => "&Quit", - PredefinedMenuItemType::About(_) => "&About", - PredefinedMenuItemType::Services => "Services", - PredefinedMenuItemType::BringAllToFront => "Bring All to Front", - PredefinedMenuItemType::None => "", + PredefinedMenuItemKind::Quit => "&Quit", + PredefinedMenuItemKind::About(_) => "&About", + PredefinedMenuItemKind::Services => "Services", + PredefinedMenuItemKind::BringAllToFront => "Bring All to Front", + PredefinedMenuItemKind::None => "", } } + #[cfg_attr(target_os = "linux", allow(dead_code))] pub(crate) fn accelerator(&self) -> Option { match self { - PredefinedMenuItemType::Copy => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyC)), - PredefinedMenuItemType::Cut => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyX)), - PredefinedMenuItemType::Paste => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyV)), - PredefinedMenuItemType::Undo => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyZ)), + PredefinedMenuItemKind::Copy => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyC)), + PredefinedMenuItemKind::Cut => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyX)), + PredefinedMenuItemKind::Paste => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyV)), + PredefinedMenuItemKind::Undo => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyZ)), #[cfg(target_os = "macos")] - PredefinedMenuItemType::Redo => Some(Accelerator::new( + PredefinedMenuItemKind::Redo => Some(Accelerator::new( Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyZ, )), #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::Redo => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyY)), - PredefinedMenuItemType::SelectAll => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyA)), - PredefinedMenuItemType::Minimize => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyM)), + PredefinedMenuItemKind::Redo => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyY)), + PredefinedMenuItemKind::SelectAll => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyA)), + PredefinedMenuItemKind::Minimize => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyM)), #[cfg(target_os = "macos")] - PredefinedMenuItemType::Fullscreen => Some(Accelerator::new( + PredefinedMenuItemKind::Fullscreen => Some(Accelerator::new( Some(Modifiers::META | Modifiers::CONTROL), Code::KeyF, )), - PredefinedMenuItemType::Hide => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyH)), - PredefinedMenuItemType::HideOthers => { + PredefinedMenuItemKind::Hide => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyH)), + PredefinedMenuItemKind::HideOthers => { Some(Accelerator::new(CMD_OR_CTRL | Modifiers::ALT, Code::KeyH)) } #[cfg(target_os = "macos")] - PredefinedMenuItemType::CloseWindow => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyW)), + PredefinedMenuItemKind::CloseWindow => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyW)), #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::CloseWindow => Some(Accelerator::new(Modifiers::ALT, Code::F4)), + PredefinedMenuItemKind::CloseWindow => Some(Accelerator::new(Modifiers::ALT, Code::F4)), #[cfg(target_os = "macos")] - PredefinedMenuItemType::Quit => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyQ)), + PredefinedMenuItemKind::Quit => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyQ)), _ => None, } } diff --git a/src/items/submenu.rs b/src/items/submenu.rs index 89cb8834..36394048 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -4,11 +4,19 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ - dpi::Position, sealed::IsMenuItemBase, util::AddOp, ContextMenu, IsMenuItem, MenuId, - MenuItemKind, + dpi::Position, icon::Icon, sealed::IsMenuItemBase, util::AddOp, ContextMenu, IsMenuItem, MenuId, + MenuItemKind, NativeIcon, }; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use super::compat::{CompatMenuItem, CompatSubMenuItem, strip_mnemonic}; + /// A menu that can be added to a [`Menu`] or another [`Submenu`]. /// /// [`Menu`]: crate::Menu @@ -16,6 +24,8 @@ use crate::{ pub struct Submenu { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for Submenu {} @@ -43,6 +53,14 @@ impl Submenu { Self { id: Rc::new(submenu.id().clone()), inner: Rc::new(RefCell::new(submenu)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::SubMenu( + CompatSubMenuItem { + label: strip_mnemonic(text.as_ref()), + enabled, + submenu: Vec::new(), + }, + ))), } } @@ -60,6 +78,14 @@ impl Submenu { enabled, Some(id), ))), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::SubMenu( + CompatSubMenuItem { + label: strip_mnemonic(text.as_ref()), + enabled, + submenu: Vec::new(), + }, + ))), } } @@ -199,6 +225,27 @@ impl Submenu { self.inner.borrow_mut().set_as_help_menu_for_nsapp() } + /// Set the icon for this submenu. + /// + /// ## Platform-specific: + /// + /// - **Linux (GTK4):** Icons on submenu headers may not render due to GTK4's + /// PopoverMenuBar limitations. The icon data is stored and will be available + /// when custom widget support is added. + pub fn set_icon(&self, icon: Option) { + self.inner.borrow_mut().set_icon(icon) + } + + /// Set the native icon for this submenu. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux:** Unsupported, the icon is not rendered. + pub fn set_native_icon(&self, _icon: Option) { + #[cfg(target_os = "macos")] + self.inner.borrow_mut().set_native_icon(_icon) + } + /// Convert this submenu into its menu ID. pub fn into_id(mut self) -> MenuId { // Note: `Rc::into_inner` is available from Rust 1.70 @@ -259,4 +306,20 @@ impl ContextMenu for Submenu { fn ns_menu(&self) -> *mut std::ffi::c_void { self.inner.borrow().ns_menu() } + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>> { + use crate::MenuItemKind; + + self.items() + .into_iter() + .map(|item| match item { + MenuItemKind::MenuItem(i) => i.compat.clone(), + MenuItemKind::Submenu(i) => i.compat.clone(), + MenuItemKind::Predefined(i) => i.compat.clone(), + MenuItemKind::Check(i) => i.compat.clone(), + MenuItemKind::Icon(i) => i.compat.clone(), + }) + .collect() + } } diff --git a/src/lib.rs b/src/lib.rs index 753e9966..207762f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -176,6 +176,9 @@ pub use items::*; pub use menu::*; pub use menu_id::MenuId; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub use platform_impl::AboutDialog; + /// An enumeration of all available menu types, useful to match against /// the items returned from [`Menu::items`] or [`Submenu::items`] #[derive(Clone)] @@ -404,6 +407,13 @@ pub trait ContextMenu { /// you need it to be alive for longer, retain it. #[cfg(target_os = "macos")] fn ns_menu(&self) -> *mut std::ffi::c_void; + + /// Returns a GTK-agnostic representation of all menu items for ksni tray support. + /// + /// This returns `Arc>` references that can be atomically + /// updated to reflect menu state changes. + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>>; } /// Describes a menu event emitted when a menu item is activated @@ -421,6 +431,51 @@ type MenuEventHandler = Box; static MENU_CHANNEL: Lazy<(Sender, MenuEventReceiver)> = Lazy::new(unbounded); static MENU_EVENT_HANDLER: OnceCell> = OnceCell::new(); +/// Describes a menu update event emitted when a menu item's state changes. +/// +/// This is used by ksni tray implementations to know when to refresh the menu. +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +#[derive(Debug, Clone)] +pub struct MenuUpdateEvent; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +static MENU_UPDATE_CHANNEL: Lazy<(Sender, Receiver)> = Lazy::new(unbounded); + +/// Receives a menu update event from the update channel. +/// +/// This function blocks until an update event is available. +/// Used by ksni tray implementations to know when to refresh the menu. +/// +/// Returns `Ok(MenuUpdateEvent)` when an update is received, or `Err` if the channel is closed. +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn recv_menu_update() -> std::result::Result { + MENU_UPDATE_CHANNEL.1.recv() +} + +/// Try to receive a menu update event without blocking. +/// +/// Returns `Ok(MenuUpdateEvent)` if an update is available, or `Err` otherwise. +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn try_recv_menu_update() -> std::result::Result { + MENU_UPDATE_CHANNEL.1.try_recv() +} + +/// Sends a menu update event to notify listeners that the menu has changed. +/// +/// This should be called when menu items are added, removed, or modified. +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn send_menu_update() { + let _ = MENU_UPDATE_CHANNEL.0.send(MenuUpdateEvent); +} + +/// Gets a reference to the menu update event receiver. +/// +/// This can be used to integrate with custom event loops. +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn menu_update_receiver() -> &'static Receiver { + &MENU_UPDATE_CHANNEL.1 +} + impl MenuEvent { /// Returns the id of the menu item which triggered this event pub fn id(&self) -> &MenuId { @@ -451,7 +506,21 @@ impl MenuEvent { } } + /// Sends a menu event. + /// + /// This is public when the `linux-ksni` feature is enabled to allow ksni + /// tray implementations to dispatch menu activation events. + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub fn send(event: MenuEvent) { + Self::send_internal(event); + } + + #[cfg(not(all(feature = "linux-ksni", target_os = "linux")))] pub(crate) fn send(event: MenuEvent) { + Self::send_internal(event); + } + + fn send_internal(event: MenuEvent) { if let Some(handler) = MENU_EVENT_HANDLER.get_or_init(|| None) { handler(event); } else { diff --git a/src/menu.rs b/src/menu.rs index 023d47c1..8d7ff4ed 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -420,6 +420,22 @@ impl ContextMenu for Menu { fn ns_menu(&self) -> *mut std::ffi::c_void { self.inner.borrow().ns_menu() } + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>> { + use crate::MenuItemKind; + + self.items() + .into_iter() + .map(|item| match item { + MenuItemKind::MenuItem(i) => i.compat.clone(), + MenuItemKind::Submenu(i) => i.compat.clone(), + MenuItemKind::Predefined(i) => i.compat.clone(), + MenuItemKind::Check(i) => i.compat.clone(), + MenuItemKind::Icon(i) => i.compat.clone(), + }) + .collect() + } } /// The window menu bar theme diff --git a/src/platform_impl/gtk/about_dialog.rs b/src/platform_impl/gtk/about_dialog.rs new file mode 100644 index 00000000..52ced262 --- /dev/null +++ b/src/platform_impl/gtk/about_dialog.rs @@ -0,0 +1,123 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! Thread-safe AboutDialog wrapper for ksni tray support. +//! +//! This module provides an AboutDialog that can be safely called from +//! any thread (including ksni's DBus thread) by dispatching to the +//! GTK main thread. + +use gtk4::prelude::GtkWindowExt; + +use crate::AboutMetadata; +#[cfg(feature = "linux-ksni")] +use crate::items::CompatAboutMetadata; + +/// A thread-safe wrapper around the GTK AboutDialog. +/// +/// This can be safely called from any thread, including the ksni DBus thread. +/// The `show()` method dispatches to the GTK main thread internally. +#[derive(Debug, Clone)] +pub struct AboutDialog { + metadata: AboutMetadata, +} + +impl AboutDialog { + /// Creates a new AboutDialog with the given metadata. + pub fn new(metadata: AboutMetadata) -> Self { + Self { metadata } + } + + /// Creates a new AboutDialog from compat metadata (for ksni use). + /// + /// This is useful when you have a `CompatAboutMetadata` from the compat layer. + #[cfg(feature = "linux-ksni")] + pub fn from_compat(compat: &CompatAboutMetadata) -> Self { + Self { + metadata: AboutMetadata { + name: compat.name.clone(), + version: compat.version.clone(), + short_version: compat.short_version.clone(), + authors: compat.authors.clone(), + comments: compat.comments.clone(), + copyright: compat.copyright.clone(), + license: compat.license.clone(), + website: compat.website.clone(), + website_label: compat.website_label.clone(), + credits: compat.credits.clone(), + icon: None, // Icon is intentionally not included in compat + }, + } + } + + /// Shows the about dialog. + /// + /// This method is safe to call from any thread - it dispatches + /// to the GTK main thread internally using `glib::MainContext::default().invoke()`. + pub fn show(&self) { + let metadata = self.metadata.clone(); + + gtk4::glib::MainContext::default().invoke(move || { + let dialog = gtk4::AboutDialog::new(); + + // Set dialog properties from metadata + if let Some(ref name) = metadata.name { + dialog.set_program_name(Some(name)); + } + + if let Some(ref version) = metadata.version { + dialog.set_version(Some(version)); + } + + if let Some(ref short_version) = metadata.short_version { + // GTK4 doesn't have a separate short version, but we can include it + // in the version string if both are set + if metadata.version.is_some() { + // Version is already set, we could append short_version but + // GTK4's AboutDialog handles this differently than macOS + let _ = short_version; // Acknowledge unused on GTK4 + } else { + dialog.set_version(Some(short_version)); + } + } + + if let Some(ref copyright) = metadata.copyright { + dialog.set_copyright(Some(copyright)); + } + + if let Some(ref comments) = metadata.comments { + dialog.set_comments(Some(comments)); + } + + if let Some(ref license) = metadata.license { + dialog.set_license(Some(license)); + } + + if let Some(ref website) = metadata.website { + dialog.set_website(Some(website)); + } + + if let Some(ref website_label) = metadata.website_label { + dialog.set_website_label(website_label); + } + + if let Some(ref authors) = metadata.authors { + let authors_strs: Vec<&str> = authors.iter().map(|s| s.as_str()).collect(); + dialog.set_authors(&authors_strs); + } + + // Note: credits in muda is Option, not used directly in GTK4 + // The credits field is primarily for macOS + let _ = &metadata.credits; + + // Present the dialog + dialog.present(); + }); + } + + /// Returns a reference to the metadata. + pub fn metadata(&self) -> &AboutMetadata { + &self.metadata + } +} diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/accelerator.rs index d1b32795..8c714145 100644 --- a/src/platform_impl/gtk/accelerator.rs +++ b/src/platform_impl/gtk/accelerator.rs @@ -35,7 +35,7 @@ fn code_to_gtk(code: Code) -> &'static str { Code::BracketLeft => "bracketleft", Code::BracketRight => "bracketright", Code::Comma => "comma", - Code::Digit0 => "1", + Code::Digit0 => "0", Code::Digit1 => "1", Code::Digit2 => "2", Code::Digit3 => "3", diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs index 825b8a45..c67cf45b 100644 --- a/src/platform_impl/gtk/icon.rs +++ b/src/platform_impl/gtk/icon.rs @@ -5,8 +5,18 @@ use crate::icon::BadIcon; /// An icon used for the window titlebar, taskbar, etc. +/// +/// Stores raw PNG bytes to be `Send + Sync` safe. The GTK `BytesIcon` +/// is created lazily when needed on the GTK main thread. #[derive(Debug, Clone)] -pub struct PlatformIcon(gtk4::gio::BytesIcon); +pub struct PlatformIcon { + png_data: Vec, +} + +// PlatformIcon is Send + Sync because it only contains Vec +// The BytesIcon is created lazily on the GTK main thread when needed +unsafe impl Send for PlatformIcon {} +unsafe impl Sync for PlatformIcon {} impl PlatformIcon { /// Creates an `Icon` from 32bpp RGBA data. @@ -14,9 +24,9 @@ impl PlatformIcon { /// The length of `rgba` must be divisible by 4, and `width * height` must equal /// `rgba.len() / 4`. Otherwise, this will return a `BadIcon` error. pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { - let mut w = Vec::with_capacity(rgba.len()); + let mut png_data = Vec::with_capacity(rgba.len()); - let mut encoder = png::Encoder::new(&mut w, width, height); + let mut encoder = png::Encoder::new(&mut png_data, width, height); encoder.set_color(png::ColorType::Rgba); encoder.set_depth(png::BitDepth::Eight); let mut writer = encoder.write_header().map_err(BadIcon::PngEncodingError)?; @@ -25,12 +35,20 @@ impl PlatformIcon { .map_err(BadIcon::PngEncodingError)?; writer.finish().map_err(BadIcon::PngEncodingError)?; - let bytes = gtk4::glib::Bytes::from_owned(w); + Ok(Self { png_data }) + } - Ok(Self(gtk4::gio::BytesIcon::new(&bytes))) + /// Creates a GTK `BytesIcon` from the stored PNG data. + /// + /// This should only be called on the GTK main thread. + pub fn to_bytes_icon(&self) -> gtk4::gio::BytesIcon { + let bytes = gtk4::glib::Bytes::from(&self.png_data); + gtk4::gio::BytesIcon::new(&bytes) } - pub fn bytes_icon(&self) -> >k4::gio::BytesIcon { - &self.0 + /// Returns the raw PNG data. + #[cfg_attr(not(feature = "linux-ksni"), allow(dead_code))] + pub fn png_data(&self) -> &[u8] { + &self.png_data } } diff --git a/src/platform_impl/gtk/mnemonic.rs b/src/platform_impl/gtk/mnemonic.rs index dba54ca1..012e5db3 100644 --- a/src/platform_impl/gtk/mnemonic.rs +++ b/src/platform_impl/gtk/mnemonic.rs @@ -25,6 +25,7 @@ pub fn to_gtk_mnemonic>(string: S) -> String { /// gtk uses underline (_) for mnemonic /// and two underlines (__) to escape it into a single underline /// while we use (&) and (&&), so we have to do a few conversions +#[cfg(test)] pub fn from_gtk_mnemonic>(string: S) -> String { string .as_ref() diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 06e4e93d..1ad4152c 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -6,6 +6,12 @@ mod accelerator; mod icon; mod mnemonic; +#[cfg(feature = "linux-ksni")] +mod about_dialog; + +#[cfg(feature = "linux-ksni")] +pub use about_dialog::AboutDialog; + use std::{ cell::RefCell, collections::{hash_map::Entry, HashMap}, @@ -21,7 +27,7 @@ use crate::{ accelerator::Accelerator, util::{AddOp, Counter}, Icon, IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType, NativeIcon, - PredefinedMenuItemType, + PredefinedMenuItemKind, }; static COUNTER: Counter = Counter::new(); @@ -55,7 +61,7 @@ impl GtkMenuBar { Self::ContextMenu { widget, menu, app } } - fn applicaiton(&self) -> >k4::Application { + fn application(&self) -> >k4::Application { match self { GtkMenuBar::MenuBar { app, .. } => app, GtkMenuBar::ContextMenu { app, .. } => app, @@ -112,10 +118,11 @@ impl Menu { } for (menu_id, menu_bar) in &self.instances { - let gtk_item = item.make_gtk_menu_item(menu_bar.applicaiton(), *menu_id)?; + let parent_menu = menu_bar.menu(); + let gtk_item = item.make_gtk_menu_item(menu_bar.application(), *menu_id, parent_menu)?; match op { - AddOp::Append => menu_bar.menu().append_item(>k_item), - AddOp::Insert(position) => menu_bar.menu().insert_item(position as i32, >k_item), + AddOp::Append => parent_menu.append_item(>k_item), + AddOp::Insert(position) => parent_menu.insert_item(position as i32, >k_item), } } @@ -124,15 +131,39 @@ impl Menu { pub fn add_menu_item_with_id(&mut self, item: &dyn IsMenuItem, id: u32) -> crate::Result<()> { for (menu_id, menu_bar) in self.instances.iter().filter(|m| *m.0 == id) { - let gtk_item = item.make_gtk_menu_item(menu_bar.applicaiton(), *menu_id)?; - menu_bar.menu().append_item(>k_item); + let parent_menu = menu_bar.menu(); + let gtk_item = item.make_gtk_menu_item(menu_bar.application(), *menu_id, parent_menu)?; + parent_menu.append_item(>k_item); } Ok(()) } - pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - todo!() + pub fn remove(&mut self, item: &dyn IsMenuItem) -> crate::Result<()> { + let child = item.child(); + let child_id = child.borrow().id().clone(); + + // Find position of item in children + let position = self + .children + .iter() + .position(|c| c.borrow().id() == &child_id); + + let Some(position) = position else { + return Err(crate::Error::NotAChildOfThisMenu); + }; + + // Remove from all GIO menus at the same position + for (menu_id, menu_bar) in &self.instances { + menu_bar.menu().remove(position as i32); + // Clean up the item's instances for this menu + item.child().borrow_mut().instances.remove(menu_id); + } + + // Remove from children + self.children.remove(position); + + Ok(()) } pub fn items(&self) -> Vec { @@ -204,13 +235,19 @@ impl Menu { { let id = window.as_ptr() as u32; - let Some(_menu_bar) = self.instances.remove(&id) else { + let Some(menu_bar) = self.instances.remove(&id) else { return Err(crate::Error::NotInitialized); }; window.insert_action_group(DEFAULT_ACTION_GROUP, None::<&gio::SimpleActionGroup>); - // TODO: destroy the menu bar + // Unparent the menu bar widget to remove it from the window + menu_bar.menu_bar().unparent(); + + // Clean up children instances for this menu + for child in &self.children { + child.borrow_mut().instances.remove(&id); + } Ok(()) } @@ -307,12 +344,14 @@ enum GtkMenuChild { Item { item: gio::MenuItem, app: gtk4::Application, + parent_menu: gio::Menu, }, Submenu { id: u32, item: gio::MenuItem, menu: gio::Menu, app: gtk4::Application, + parent_menu: gio::Menu, }, ContextMenu { id: u32, @@ -339,10 +378,10 @@ impl GtkMenuChild { } } - fn item(&self) -> &gio::MenuItem { + fn parent_menu(&self) -> &gio::Menu { match self { - GtkMenuChild::Submenu { item, .. } => item, - GtkMenuChild::Item { item, .. } => item, + GtkMenuChild::Item { parent_menu, .. } => parent_menu, + GtkMenuChild::Submenu { parent_menu, .. } => parent_menu, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } @@ -373,6 +412,8 @@ pub struct MenuChild { icon: Option, + predefined_item_type: Option, + type_: MenuItemType, instances: HashMap>, @@ -391,6 +432,7 @@ impl MenuChild { checked: false, icon: None, accelerator: None, + predefined_item_type: None, type_: MenuItemType::Submenu, ctx_menu_id: COUNTER.next(), instances: HashMap::new(), @@ -403,6 +445,7 @@ impl MenuChild { &mut self, app: >k4::Application, menu_id: u32, + parent_menu: &gio::Menu, ) -> crate::Result { let menu = gio::Menu::new(); let item = gio::MenuItem::new_submenu(Some(&to_gtk_mnemonic(&self.text)), &menu); @@ -425,6 +468,7 @@ impl MenuChild { menu, id, app: app.clone(), + parent_menu: parent_menu.clone(), }; self.instances.entry(menu_id).or_default().push(child); @@ -444,12 +488,13 @@ impl MenuChild { for menus in self.instances.values() { for gtk_child in menus { - let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id())?; + let parent_menu = gtk_child.menu(); + let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id(), parent_menu)?; match op { - AddOp::Append => gtk_child.menu().append_item(>k_item), + AddOp::Append => parent_menu.append_item(>k_item), AddOp::Insert(position) => { - gtk_child.menu().insert_item(position as i32, >k_item) + parent_menu.insert_item(position as i32, >k_item) } } } @@ -461,16 +506,51 @@ impl MenuChild { pub fn add_menu_item_with_id(&self, item: &dyn IsMenuItem, id: u32) -> crate::Result<()> { for menus in self.instances.values() { for gtk_child in menus.iter().filter(|m| m.id() == id) { - let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id())?; - gtk_child.menu().append_item(>k_item); + let parent_menu = gtk_child.menu(); + let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id(), parent_menu)?; + parent_menu.append_item(>k_item); } } Ok(()) } - pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - todo!() + pub fn remove(&mut self, item: &dyn IsMenuItem) -> crate::Result<()> { + let child = item.child(); + let child_id = child.borrow().id().clone(); + + // Find position of item in children + let position = self + .children + .iter() + .position(|c| c.borrow().id() == &child_id); + + let Some(position) = position else { + return Err(crate::Error::NotAChildOfThisMenu); + }; + + // Remove from all submenu GIO menus at the same position + for menus in self.instances.values() { + for gtk_child in menus { + // Get the submenu's gio::Menu and remove at position + gtk_child.menu().remove(position as i32); + } + } + + // Clean up the item's instances + // For submenus, we need to clear instances that belong to this submenu's children + let child_ref = item.child(); + let mut item_child = child_ref.borrow_mut(); + for menus in self.instances.values() { + for gtk_child in menus { + item_child.instances.remove(>k_child.id()); + } + } + + // Remove from children + self.children.remove(position); + + Ok(()) } pub fn items(&self) -> Vec { @@ -534,7 +614,7 @@ impl MenuChild { } impl MenuChild { - pub fn new_menu_item( + pub fn new( text: &str, enabled: bool, accelerator: Option, @@ -547,6 +627,7 @@ impl MenuChild { accelerator, icon: None, checked: false, + predefined_item_type: None, type_: MenuItemType::MenuItem, ctx_menu_id: 0, instances: HashMap::new(), @@ -559,6 +640,7 @@ impl MenuChild { &mut self, app: >k4::Application, menu_id: u32, + parent_menu: &gio::Menu, ) -> crate::Result { let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); @@ -582,6 +664,7 @@ impl MenuChild { let child = GtkMenuChild::Item { item: item.clone(), app: app.clone(), + parent_menu: parent_menu.clone(), }; self.instances.entry(menu_id).or_default().push(child); @@ -604,8 +687,61 @@ impl MenuChild { self.text.clone() } - pub fn set_text(&self, text: &str) { - todo!() + #[cfg_attr(not(feature = "linux-ksni"), allow(dead_code))] + pub fn icon(&self) -> Option<&Icon> { + self.icon.as_ref() + } + + pub fn set_text(&mut self, text: &str) { + self.text = text.to_string(); + + // GIO MenuItems are immutable after insertion, so we need to remove and reinsert + let detailed_action = self.detailed_action(); + + for children in self.instances.values_mut() { + for child in children.iter_mut() { + let parent_menu = child.parent_menu(); + + // Find position of this item in parent menu by matching action name + let mut position = None; + for i in 0..parent_menu.n_items() { + if let Some(action) = parent_menu.item_attribute_value(i as i32, "action", None) { + if let Some(action_str) = action.str() { + if action_str == detailed_action { + position = Some(i as i32); + break; + } + } + } + } + + if let Some(pos) = position { + // Remove old item + parent_menu.remove(pos); + + // Create new item with updated text + let new_item = gio::MenuItem::new( + Some(&to_gtk_mnemonic(&self.text)), + Some(&detailed_action), + ); + + // Copy icon if present + if let Some(icon) = &self.icon { + new_item.set_icon(&icon.inner.to_bytes_icon()); + } + + // Insert at same position + parent_menu.insert_item(pos, &new_item); + + // Update stored reference + match child { + GtkMenuChild::Item { item, .. } => *item = new_item, + GtkMenuChild::Submenu { item, .. } => *item = new_item, + _ => {} + } + } + } + } } pub fn is_enabled(&self) -> bool { @@ -636,7 +772,7 @@ impl MenuChild { } impl MenuChild { - pub fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub fn new_predefined(item_type: PredefinedMenuItemKind, text: Option) -> Self { Self { id: MenuId(COUNTER.next().to_string()), text: text.unwrap_or_else(|| item_type.text().to_string()), @@ -644,6 +780,7 @@ impl MenuChild { accelerator: None, icon: None, checked: false, + predefined_item_type: Some(item_type), type_: MenuItemType::Predefined, ctx_menu_id: 0, instances: HashMap::new(), @@ -668,6 +805,7 @@ impl MenuChild { accelerator, icon: None, checked, + predefined_item_type: None, type_: MenuItemType::Check, ctx_menu_id: 0, instances: HashMap::new(), @@ -680,6 +818,7 @@ impl MenuChild { &mut self, app: >k4::Application, menu_id: u32, + parent_menu: &gio::Menu, ) -> crate::Result { let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); @@ -704,6 +843,7 @@ impl MenuChild { let child = GtkMenuChild::Item { item: item.clone(), app: app.clone(), + parent_menu: parent_menu.clone(), }; self.instances.entry(menu_id).or_default().push(child); @@ -742,6 +882,7 @@ impl MenuChild { accelerator, icon, checked: false, + predefined_item_type: None, type_: MenuItemType::Icon, ctx_menu_id: 0, instances: HashMap::new(), @@ -753,7 +894,7 @@ impl MenuChild { pub fn new_native_icon( text: &str, enabled: bool, - icon: Option, + _icon: Option, accelerator: Option, id: Option, ) -> Self { @@ -764,7 +905,8 @@ impl MenuChild { accelerator, icon: None, checked: false, - type_: MenuItemType::Submenu, + predefined_item_type: None, + type_: MenuItemType::Icon, ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), @@ -776,6 +918,7 @@ impl MenuChild { &mut self, app: >k4::Application, menu_id: u32, + parent_menu: &gio::Menu, ) -> crate::Result { let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); @@ -785,7 +928,7 @@ impl MenuChild { } if let Some(icon) = &self.icon { - item.set_icon(icon.inner.bytes_icon()); + item.set_icon(&icon.inner.to_bytes_icon()); } if self.action.is_none() { @@ -803,13 +946,227 @@ impl MenuChild { let child = GtkMenuChild::Item { item: item.clone(), app: app.clone(), + parent_menu: parent_menu.clone(), }; self.instances.entry(menu_id).or_default().push(child); Ok(item) } - pub fn set_icon(&self, icon: Option) {} + fn create_gtk_item_for_predefined_menu_item( + &mut self, + app: >k4::Application, + menu_id: u32, + parent_menu: &gio::Menu, + ) -> crate::Result { + let predefined_item_type = self.predefined_item_type.clone().unwrap(); + + let (label, action_name) = match &predefined_item_type { + // Separator - create an empty section label (GIO way of doing separators) + PredefinedMenuItemKind::Separator => { + // For separators, we return an item with no action that acts as a visual break + // In GIO menus, true separators are done via sections, but this provides a fallback + let item = gio::MenuItem::new(None, None); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + parent_menu: parent_menu.clone(), + }; + self.instances.entry(menu_id).or_default().push(child); + return Ok(item); + } + + // Clipboard actions (widget-scoped, work on focused text widgets) + PredefinedMenuItemKind::Copy => (self.text.clone(), "clipboard.copy"), + PredefinedMenuItemKind::Cut => (self.text.clone(), "clipboard.cut"), + PredefinedMenuItemKind::Paste => (self.text.clone(), "clipboard.paste"), + PredefinedMenuItemKind::SelectAll => (self.text.clone(), "selection.select-all"), + + // Text actions (widget-scoped, work on focused text widgets) + PredefinedMenuItemKind::Undo => (self.text.clone(), "text.undo"), + PredefinedMenuItemKind::Redo => (self.text.clone(), "text.redo"), + + // Window actions (built-in on GtkWindow) + PredefinedMenuItemKind::Minimize => (self.text.clone(), "window.minimize"), + PredefinedMenuItemKind::Maximize => (self.text.clone(), "window.toggle-maximized"), + PredefinedMenuItemKind::CloseWindow => (self.text.clone(), "window.close"), + + // Fullscreen - no built-in GAction, need custom action + PredefinedMenuItemKind::Fullscreen => { + let action_name = format!("{DEFAULT_ACTION_GROUP}.{}_fullscreen", self.id.as_ref()); + + if self.action.is_none() { + let action_group = action_group_from_app(app); + let action = gio::SimpleAction::new(&format!("{}_fullscreen", self.id.as_ref()), None); + action.connect_activate(|_, _| { + // Get the focused window and toggle fullscreen + if let Some(app) = gio::Application::default() { + if let Some(app) = app.downcast_ref::() { + if let Some(window) = app.active_window() { + if window.is_fullscreen() { + window.unfullscreen(); + } else { + window.fullscreen(); + } + } + } + } + }); + action_group.add_action(&action); + self.action = Some(action); + } + + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&action_name)); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + parent_menu: parent_menu.clone(), + }; + self.instances.entry(menu_id).or_default().push(child); + return Ok(item); + } + + // About - custom action showing AboutDialog + PredefinedMenuItemKind::About(metadata) => { + let action_name = format!("{DEFAULT_ACTION_GROUP}.{}_about", self.id.as_ref()); + + if self.action.is_none() { + let action_group = action_group_from_app(app); + let metadata = metadata.clone(); + let action = gio::SimpleAction::new(&format!("{}_about", self.id.as_ref()), None); + action.connect_activate(move |_, _| { + if let Some(metadata) = &metadata { + let dialog = gtk4::AboutDialog::new(); + dialog.set_modal(true); + + if let Some(name) = &metadata.name { + dialog.set_program_name(Some(name.as_str())); + } + if let Some(version) = &metadata.full_version() { + dialog.set_version(Some(version.as_str())); + } + if let Some(authors) = &metadata.authors { + let authors_refs: Vec<&str> = authors.iter().map(|s| s.as_str()).collect(); + dialog.set_authors(&authors_refs); + } + if let Some(comments) = &metadata.comments { + dialog.set_comments(Some(comments)); + } + if let Some(copyright) = &metadata.copyright { + dialog.set_copyright(Some(copyright)); + } + if let Some(license) = &metadata.license { + dialog.set_license(Some(license)); + } + if let Some(website) = &metadata.website { + dialog.set_website(Some(website)); + } + if let Some(website_label) = &metadata.website_label { + dialog.set_website_label(website_label); + } + + // Set transient parent if possible + if let Some(app) = gio::Application::default() { + if let Some(app) = app.downcast_ref::() { + if let Some(window) = app.active_window() { + dialog.set_transient_for(Some(&window)); + } + } + } + + dialog.present(); + } + }); + action_group.add_action(&action); + self.action = Some(action); + } + + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&action_name)); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + parent_menu: parent_menu.clone(), + }; + self.instances.entry(menu_id).or_default().push(child); + return Ok(item); + } + + // Unsupported on Linux (matches GTK3 behavior) + PredefinedMenuItemKind::Quit + | PredefinedMenuItemKind::Hide + | PredefinedMenuItemKind::HideOthers + | PredefinedMenuItemKind::ShowAll + | PredefinedMenuItemKind::Services + | PredefinedMenuItemKind::BringAllToFront + | PredefinedMenuItemKind::None => { + unreachable!("Predefined menu item type not supported on Linux") + } + }; + + // Create menu item pointing to the action + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&label)), Some(action_name)); + + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + parent_menu: parent_menu.clone(), + }; + self.instances.entry(menu_id).or_default().push(child); + + Ok(item) + } + + pub fn set_icon(&mut self, icon: Option) { + self.icon = icon; + + // GIO MenuItems are immutable after insertion, so we need to remove and reinsert + let detailed_action = self.detailed_action(); + + for children in self.instances.values_mut() { + for child in children.iter_mut() { + let parent_menu = child.parent_menu(); + + // Find position of this item in parent menu by matching action name + let mut position = None; + for i in 0..parent_menu.n_items() { + if let Some(action) = parent_menu.item_attribute_value(i as i32, "action", None) { + if let Some(action_str) = action.str() { + if action_str == detailed_action { + position = Some(i as i32); + break; + } + } + } + } + + if let Some(pos) = position { + // Remove old item + parent_menu.remove(pos); + + // Create new item with updated icon + let new_item = gio::MenuItem::new( + Some(&to_gtk_mnemonic(&self.text)), + Some(&detailed_action), + ); + + // Set icon if present + if let Some(icon) = &self.icon { + new_item.set_icon(&icon.inner.to_bytes_icon()); + } + + // Insert at same position + parent_menu.insert_item(pos, &new_item); + + // Update stored reference + match child { + GtkMenuChild::Item { item, .. } => *item = new_item, + GtkMenuChild::Submenu { item, .. } => *item = new_item, + _ => {} + } + } + } + } + } } impl dyn IsMenuItem + '_ { @@ -817,23 +1174,21 @@ impl dyn IsMenuItem + '_ { &self, app: >k4::Application, menu_id: u32, + parent_menu: &gio::Menu, ) -> crate::Result { let kind = self.kind(); let mut child = kind.child_mut(); match child.item_type() { - MenuItemType::Submenu => child.create_gtk_item_for_submenu(app, menu_id), - MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(app, menu_id), - MenuItemType::Check => child.create_gtk_item_for_check_menu_item(app, menu_id), - MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item(app, menu_id), - _ => todo!(), - // MenuItemType::Predefined => { - // child.create_gtk_item_for_predefined_menu_item(menu_id, action_group) - // } + MenuItemType::Submenu => child.create_gtk_item_for_submenu(app, menu_id, parent_menu), + MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(app, menu_id, parent_menu), + MenuItemType::Check => child.create_gtk_item_for_check_menu_item(app, menu_id, parent_menu), + MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item(app, menu_id, parent_menu), + MenuItemType::Predefined => child.create_gtk_item_for_predefined_menu_item(app, menu_id, parent_menu), } } } -/// Returns and creates the action group on this applicaiton if necessary. +/// Returns and creates the action group on this application if necessary. fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { let action_group = unsafe { app.data::(ACTION_GROUP_DATA_KEY) }; diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 494e2c48..06859a66 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -200,7 +200,7 @@ pub struct MenuChild { accelerator: Option, // predefined menu item fields - predefined_item_type: Option, + predefined_item_type: Option, // check menu item fields checked: Cell, @@ -299,7 +299,7 @@ impl MenuChild { } } - pub(crate) fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub(crate) fn new_predefined(item_type: PredefinedMenuItemKind, text: Option) -> Self { let text = strip_mnemonic(text.unwrap_or_else(|| { // Gets the app's name from `NSRunningApplication::localizedName`. let app_name = || unsafe { @@ -308,11 +308,11 @@ impl MenuChild { }; match item_type { - PredefinedMenuItemType::About(_) => { + PredefinedMenuItemKind::About(_) => { format!("About {}", app_name()).trim().to_string() } - PredefinedMenuItemType::Hide => format!("Hide {}", app_name()).trim().to_string(), - PredefinedMenuItemType::Quit => format!("Quit {}", app_name()).trim().to_string(), + PredefinedMenuItemKind::Hide => format!("Hide {}", app_name()).trim().to_string(), + PredefinedMenuItemKind::Quit => format!("Quit {}", app_name()).trim().to_string(), _ => item_type.text().to_string(), } })); @@ -768,12 +768,12 @@ impl MenuChild { let mtm = MainThreadMarker::new().expect("can only create menu item on the main thread"); let item_type = self.predefined_item_type.as_ref().unwrap(); let ns_menu_item = match item_type { - PredefinedMenuItemType::Separator => NSMenuItem::separatorItem(mtm), + PredefinedMenuItemKind::Separator => NSMenuItem::separatorItem(mtm), _ => { let ns_menu_item = MenuItem::create(mtm, &self.text, item_type.selector(), &self.accelerator)?; - if let PredefinedMenuItemType::About(_) = item_type { + if let PredefinedMenuItemKind::About(_) = item_type { unsafe { ns_menu_item.setTarget(Some(&ns_menu_item)); @@ -789,7 +789,7 @@ impl MenuChild { unsafe { ns_menu_item.setEnabled(self.enabled); - if let PredefinedMenuItemType::Services = item_type { + if let PredefinedMenuItemKind::Services = item_type { // we have to assign an empty menu as the app's services menu, and macOS will populate it let services_menu = NSMenu::new(mtm); NSApplication::sharedApplication(mtm).setServicesMenu(Some(&services_menu)); @@ -883,29 +883,29 @@ impl MenuChild { } } -impl PredefinedMenuItemType { +impl PredefinedMenuItemKind { pub(crate) fn selector(&self) -> Option { match self { - PredefinedMenuItemType::Separator => None, - PredefinedMenuItemType::Copy => Some(sel!(copy:)), - PredefinedMenuItemType::Cut => Some(sel!(cut:)), - PredefinedMenuItemType::Paste => Some(sel!(paste:)), - PredefinedMenuItemType::SelectAll => Some(sel!(selectAll:)), - PredefinedMenuItemType::Undo => Some(sel!(undo:)), - PredefinedMenuItemType::Redo => Some(sel!(redo:)), - PredefinedMenuItemType::Minimize => Some(sel!(performMiniaturize:)), - PredefinedMenuItemType::Maximize => Some(sel!(performZoom:)), - PredefinedMenuItemType::Fullscreen => Some(sel!(toggleFullScreen:)), - PredefinedMenuItemType::Hide => Some(sel!(hide:)), - PredefinedMenuItemType::HideOthers => Some(sel!(hideOtherApplications:)), - PredefinedMenuItemType::ShowAll => Some(sel!(unhideAllApplications:)), - PredefinedMenuItemType::CloseWindow => Some(sel!(performClose:)), - PredefinedMenuItemType::Quit => Some(sel!(terminate:)), + PredefinedMenuItemKind::Separator => None, + PredefinedMenuItemKind::Copy => Some(sel!(copy:)), + PredefinedMenuItemKind::Cut => Some(sel!(cut:)), + PredefinedMenuItemKind::Paste => Some(sel!(paste:)), + PredefinedMenuItemKind::SelectAll => Some(sel!(selectAll:)), + PredefinedMenuItemKind::Undo => Some(sel!(undo:)), + PredefinedMenuItemKind::Redo => Some(sel!(redo:)), + PredefinedMenuItemKind::Minimize => Some(sel!(performMiniaturize:)), + PredefinedMenuItemKind::Maximize => Some(sel!(performZoom:)), + PredefinedMenuItemKind::Fullscreen => Some(sel!(toggleFullScreen:)), + PredefinedMenuItemKind::Hide => Some(sel!(hide:)), + PredefinedMenuItemKind::HideOthers => Some(sel!(hideOtherApplications:)), + PredefinedMenuItemKind::ShowAll => Some(sel!(unhideAllApplications:)), + PredefinedMenuItemKind::CloseWindow => Some(sel!(performClose:)), + PredefinedMenuItemKind::Quit => Some(sel!(terminate:)), // manual implementation in `fire_menu_item_click` - PredefinedMenuItemType::About(_) => Some(sel!(fireMenuItemAction:)), - PredefinedMenuItemType::Services => None, - PredefinedMenuItemType::BringAllToFront => Some(sel!(arrangeInFront:)), - PredefinedMenuItemType::None => None, + PredefinedMenuItemKind::About(_) => Some(sel!(fireMenuItemAction:)), + PredefinedMenuItemKind::Services => None, + PredefinedMenuItemKind::BringAllToFront => Some(sel!(arrangeInFront:)), + PredefinedMenuItemKind::None => None, } } } @@ -967,7 +967,7 @@ impl MenuItem { let item = unsafe { self.ivars().get().as_ref() }.expect("MenuItem's MenuChild pointer was unset"); - if let Some(PredefinedMenuItemType::About(about_meta)) = &item.predefined_item_type { + if let Some(PredefinedMenuItemKind::About(about_meta)) = &item.predefined_item_type { match about_meta { Some(about_meta) => { let mut keys: Vec<&NSString> = Default::default(); diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index c6abb720..77312c68 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -17,10 +17,21 @@ use std::{ rc::Rc, }; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{items::*, IsMenuItem, MenuItemKind, MenuItemType}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use crate::items::{CompatMenuItem, CompatStandardItem, CompatCheckmarkItem, CompatSubMenuItem, strip_mnemonic}; + pub(crate) use self::platform::*; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub use self::platform::AboutDialog; + impl dyn IsMenuItem + '_ { fn child(&self) -> Rc> { match self.kind() { @@ -38,38 +49,122 @@ impl MenuChild { fn kind(&self, c: Rc>) -> MenuItemKind { match self.item_type() { MenuItemType::Submenu => { - let id = c.borrow().id().clone(); + let borrowed = c.borrow(); + let id = borrowed.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let text = borrowed.text(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let enabled = borrowed.is_enabled(); + drop(borrowed); MenuItemKind::Submenu(Submenu { id: Rc::new(id), inner: c, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::SubMenu( + CompatSubMenuItem { + label: strip_mnemonic(&text), + enabled, + submenu: Vec::new(), // Will be populated by compat_items() + }, + ))), }) } MenuItemType::MenuItem => { - let id = c.borrow().id().clone(); + let borrowed = c.borrow(); + let id = borrowed.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let text = borrowed.text(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let enabled = borrowed.is_enabled(); + drop(borrowed); MenuItemKind::MenuItem(MenuItem { - id: Rc::new(id), + id: Rc::new(id.clone()), inner: c, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(&text), + enabled, + icon: None, + predefined_item_id: None, + about_metadata: None, + }, + ))), }) } MenuItemType::Predefined => { - let id = c.borrow().id().clone(); + let borrowed = c.borrow(); + let id = borrowed.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let text = borrowed.text(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let enabled = borrowed.is_enabled(); + drop(borrowed); MenuItemKind::Predefined(PredefinedMenuItem { - id: Rc::new(id), + id: Rc::new(id.clone()), inner: c, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(&text), + enabled, + icon: None, + predefined_item_id: None, // TODO: populate from predefined type + about_metadata: None, + }, + ))), }) } MenuItemType::Check => { - let id = c.borrow().id().clone(); + let borrowed = c.borrow(); + let id = borrowed.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let text = borrowed.text(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let enabled = borrowed.is_enabled(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let checked = borrowed.is_checked(); + drop(borrowed); MenuItemKind::Check(CheckMenuItem { - id: Rc::new(id), + id: Rc::new(id.clone()), inner: c, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Checkmark( + CompatCheckmarkItem { + id: id.0.clone(), + label: strip_mnemonic(&text), + enabled, + checked, + }, + ))), }) } MenuItemType::Icon => { - let id = c.borrow().id().clone(); + let borrowed = c.borrow(); + let id = borrowed.id().clone(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let text = borrowed.text(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let enabled = borrowed.is_enabled(); + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let icon_bytes = borrowed.icon().map(|i| i.inner.png_data().to_vec()); + drop(borrowed); MenuItemKind::Icon(IconMenuItem { - id: Rc::new(id), + id: Rc::new(id.clone()), inner: c, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(CompatMenuItem::Standard( + CompatStandardItem { + id: id.0.clone(), + label: strip_mnemonic(&text), + enabled, + icon: icon_bytes, + predefined_item_id: None, + about_metadata: None, + }, + ))), }) } } @@ -88,7 +183,7 @@ impl MenuItemKind { } } - pub(crate) fn child(&self) -> Ref { + pub(crate) fn child(&self) -> Ref<'_, MenuChild> { match self { MenuItemKind::MenuItem(i) => i.inner.borrow(), MenuItemKind::Submenu(i) => i.inner.borrow(), @@ -98,7 +193,7 @@ impl MenuItemKind { } } - pub(crate) fn child_mut(&self) -> RefMut { + pub(crate) fn child_mut(&self) -> RefMut<'_, MenuChild> { match self { MenuItemKind::MenuItem(i) => i.inner.borrow_mut(), MenuItemKind::Submenu(i) => i.inner.borrow_mut(), diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index b087d013..2f9a0fda 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -14,7 +14,7 @@ use crate::{ accelerator::Accelerator, dpi::Position, icon::{Icon, NativeIcon}, - items::PredefinedMenuItemType, + items::PredefinedMenuItemKind, util::{AddOp, Counter}, AboutMetadata, IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType, MenuTheme, }; @@ -68,8 +68,8 @@ macro_rules! inner_menu_child_and_flags { let child = i.inner; let child_ = child.borrow(); match child_.predefined_item_type.as_ref().unwrap() { - PredefinedMenuItemType::None => return Ok(()), - PredefinedMenuItemType::Separator => { + PredefinedMenuItemKind::None => return Ok(()), + PredefinedMenuItemKind::Separator => { flags |= MF_SEPARATOR; } _ => { @@ -465,7 +465,7 @@ pub(crate) struct MenuChild { accelerator: Option, // predefined menu item fields - predefined_item_type: Option, + predefined_item_type: Option, // check menu item fields checked: bool, @@ -543,7 +543,7 @@ impl MenuChild { } } - pub fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub fn new_predefined(item_type: PredefinedMenuItemKind, text: Option) -> Self { let internal_id = COUNTER.next(); Self { item_type: MenuItemType::Predefined, @@ -1193,29 +1193,29 @@ unsafe fn menu_selected(hwnd: windows_sys::Win32::Foundation::HWND, item: &mut M MenuItemType::Predefined => { if let Some(predefined_item_type) = &item.predefined_item_type { match predefined_item_type { - PredefinedMenuItemType::Copy => execute_edit_command(EditCommand::Copy), - PredefinedMenuItemType::Cut => execute_edit_command(EditCommand::Cut), - PredefinedMenuItemType::Paste => execute_edit_command(EditCommand::Paste), - PredefinedMenuItemType::SelectAll => { + PredefinedMenuItemKind::Copy => execute_edit_command(EditCommand::Copy), + PredefinedMenuItemKind::Cut => execute_edit_command(EditCommand::Cut), + PredefinedMenuItemKind::Paste => execute_edit_command(EditCommand::Paste), + PredefinedMenuItemKind::SelectAll => { execute_edit_command(EditCommand::SelectAll) } - PredefinedMenuItemType::Separator => {} - PredefinedMenuItemType::Minimize => { + PredefinedMenuItemKind::Separator => {} + PredefinedMenuItemKind::Minimize => { ShowWindow(hwnd, SW_MINIMIZE); } - PredefinedMenuItemType::Maximize => { + PredefinedMenuItemKind::Maximize => { ShowWindow(hwnd, SW_MAXIMIZE); } - PredefinedMenuItemType::Hide => { + PredefinedMenuItemKind::Hide => { ShowWindow(hwnd, SW_HIDE); } - PredefinedMenuItemType::CloseWindow => { + PredefinedMenuItemKind::CloseWindow => { SendMessageW(hwnd, WM_CLOSE, 0, 0); } - PredefinedMenuItemType::Quit => { + PredefinedMenuItemKind::Quit => { PostQuitMessage(0); } - PredefinedMenuItemType::About(Some(ref metadata)) => { + PredefinedMenuItemKind::About(Some(ref metadata)) => { show_about_dialog(hwnd as _, metadata) }