From 3b5bbfe5ebc78b6840ff6d15a2334191d8df0ed7 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Sun, 2 Feb 2025 01:28:56 +0200 Subject: [PATCH 01/21] feat!: gtk4 closes #270 kicking off work on gtk4 migration help is appreciated --- Cargo.lock | 560 +++++++--- Cargo.toml | 7 +- examples/gtk.rs | 65 ++ examples/tao.rs | 20 +- examples/winit.rs | 4 +- examples/wry.rs | 38 +- src/accelerator.rs | 14 +- src/icon.rs | 5 + src/items/predefined.rs | 37 +- src/items/submenu.rs | 7 +- src/lib.rs | 8 +- src/menu.rs | 49 +- src/menu_id.rs | 5 + src/platform_impl/gtk/accelerator.rs | 201 +--- src/platform_impl/gtk/icon.rs | 61 +- src/platform_impl/gtk/mod.rs | 1421 +++----------------------- 16 files changed, 782 insertions(+), 1720 deletions(-) create mode 100644 examples/gtk.rs diff --git a/Cargo.lock b/Cargo.lock index 094da987..ba7b3a93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,7 +118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", - "glib", + "glib 0.18.5", "libc", ] @@ -128,10 +128,10 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -269,22 +269,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ "bitflags 2.8.0", - "cairo-sys-rs", - "glib", + "cairo-sys-rs 0.18.2", + "glib 0.18.5", "libc", "once_cell", "thiserror", ] +[[package]] +name = "cairo-rs" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" +dependencies = [ + "bitflags 2.8.0", + "cairo-sys-rs 0.20.7", + "glib 0.20.7", + "libc", +] + [[package]] name = "cairo-sys-rs" version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ - "glib-sys", + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df" +dependencies = [ + "glib-sys 0.20.7", "libc", - "system-deps", + "system-deps 7.0.3", ] [[package]] @@ -340,6 +363,16 @@ dependencies = [ "target-lexicon", ] +[[package]] +name = "cfg-expr" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +dependencies = [ + "smallvec", + "target-lexicon", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -892,13 +925,13 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ - "cairo-rs", - "gdk-pixbuf", + "cairo-rs 0.18.5", + "gdk-pixbuf 0.18.5", "gdk-sys", - "gio", - "glib", + "gio 0.18.4", + "glib 0.18.5", "libc", - "pango", + "pango 0.18.3", ] [[package]] @@ -907,24 +940,49 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", + "gdk-pixbuf-sys 0.18.0", + "gio 0.18.4", + "glib 0.18.5", "libc", "once_cell", ] +[[package]] +name = "gdk-pixbuf" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6efc7705f7863d37b12ad6974cbb310d35d054f5108cdc1e69037742f573c4c" +dependencies = [ + "gdk-pixbuf-sys 0.20.7", + "gio 0.20.7", + "glib 0.20.7", + "libc", +] + [[package]] name = "gdk-pixbuf-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67f2587c9202bf997476bbba6aaed4f78a11538a2567df002a5f57f5331d0b5c" +dependencies = [ + "gio-sys 0.20.8", + "glib-sys 0.20.7", + "gobject-sys 0.20.7", "libc", - "system-deps", + "system-deps 7.0.3", ] [[package]] @@ -933,15 +991,47 @@ version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "cairo-sys-rs 0.18.2", + "gdk-pixbuf-sys 0.18.0", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "pango-sys", + "pango-sys 0.18.0", "pkg-config", - "system-deps", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk4" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0196720118f880f71fe7da971eff58cc43a89c9cf73f46076b7cb1e60889b15" +dependencies = [ + "cairo-rs 0.20.7", + "gdk-pixbuf 0.20.7", + "gdk4-sys", + "gio 0.20.7", + "glib 0.20.7", + "libc", + "pango 0.20.7", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b0e1340bd15e7a78810cf39fed9e5d85f0a8f80b1d999d384ca17dcc452b60" +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", + "libc", + "pango-sys 0.20.7", + "pkg-config", + "system-deps 7.0.3", ] [[package]] @@ -951,11 +1041,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "pkg-config", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -966,8 +1056,8 @@ checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" dependencies = [ "gdk", "gdkx11-sys", - "gio", - "glib", + "gio 0.18.4", + "glib 0.18.5", "libc", "x11", ] @@ -979,9 +1069,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", - "glib-sys", + "glib-sys 0.18.1", "libc", - "system-deps", + "system-deps 6.2.2", "x11", ] @@ -1047,8 +1137,8 @@ dependencies = [ "futures-core", "futures-io", "futures-util", - "gio-sys", - "glib", + "gio-sys 0.18.1", + "glib 0.18.5", "libc", "once_cell", "pin-project-lite", @@ -1056,19 +1146,49 @@ dependencies = [ "thiserror", ] +[[package]] +name = "gio" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a517657589a174be9f60c667f1fec8b7ac82ed5db4ebf56cf073a3b5955d8e2e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys 0.20.8", + "glib 0.20.7", + "libc", + "pin-project-lite", + "smallvec", +] + [[package]] name = "gio-sys" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", "winapi", ] +[[package]] +name = "gio-sys" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +dependencies = [ + "glib-sys 0.20.7", + "gobject-sys 0.20.7", + "libc", + "system-deps 7.0.3", + "windows-sys 0.59.0", +] + [[package]] name = "glib" version = "0.18.5" @@ -1081,10 +1201,10 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-macros 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "memchr", "once_cell", @@ -1092,6 +1212,27 @@ dependencies = [ "thiserror", ] +[[package]] +name = "glib" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +dependencies = [ + "bitflags 2.8.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", + "libc", + "memchr", + "smallvec", +] + [[package]] name = "glib-macros" version = "0.18.5" @@ -1099,13 +1240,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.2", + "proc-macro-crate 2.0.0", "proc-macro-error", "proc-macro2", "quote", "syn 2.0.96", ] +[[package]] +name = "glib-macros" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +dependencies = [ + "heck 0.5.0", + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "glib-sys" version = "0.18.1" @@ -1113,7 +1267,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", - "system-deps", + "system-deps 6.2.2", +] + +[[package]] +name = "glib-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +dependencies = [ + "libc", + "system-deps 7.0.3", ] [[package]] @@ -1122,9 +1286,74 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ - "glib-sys", + "glib-sys 0.18.1", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gobject-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +dependencies = [ + "glib-sys 0.20.7", "libc", - "system-deps", + "system-deps 7.0.3", +] + +[[package]] +name = "graphene-rs" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f39d3bcd2e24fd9c2874a56f277b72c03e728de9bdc95a8d4ef4c962f10ced98" +dependencies = [ + "glib 0.20.7", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a68d39515bf340e879b72cecd4a25c1332557757ada6e8aba8654b4b81d23a" +dependencies = [ + "glib-sys 0.20.7", + "libc", + "pkg-config", + "system-deps 7.0.3", +] + +[[package]] +name = "gsk4" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b9188db0a6219e708b6b6e7225718e459def664023dbddb8395ca1486d8102" +dependencies = [ + "cairo-rs 0.20.7", + "gdk4", + "glib 0.20.7", + "graphene-rs", + "gsk4-sys", + "libc", + "pango 0.20.7", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca10fc65d68528a548efa3d8747934adcbe7058b73695c9a7f43a25352fce14" +dependencies = [ + "cairo-sys-rs 0.20.7", + "gdk4-sys", + "glib-sys 0.20.7", + "gobject-sys 0.20.7", + "graphene-sys", + "libc", + "pango-sys 0.20.7", + "system-deps 7.0.3", ] [[package]] @@ -1134,17 +1363,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" dependencies = [ "atk", - "cairo-rs", + "cairo-rs 0.18.5", "field-offset", "futures-channel", "gdk", - "gdk-pixbuf", - "gio", - "glib", + "gdk-pixbuf 0.18.5", + "gio 0.18.4", + "glib 0.18.5", "gtk-sys", "gtk3-macros", "libc", - "pango", + "pango 0.18.3", "pkg-config", ] @@ -1155,15 +1384,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" dependencies = [ "atk-sys", - "cairo-sys-rs", - "gdk-pixbuf-sys", + "cairo-sys-rs 0.18.2", + "gdk-pixbuf-sys 0.18.0", "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "pango-sys", - "system-deps", + "pango-sys 0.18.0", + "system-deps 6.2.2", ] [[package]] @@ -1179,6 +1408,58 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "gtk4" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697ff938136625f6acf75f01951220f47a45adcf0060ee55b4671cf734dac44" +dependencies = [ + "cairo-rs 0.20.7", + "field-offset", + "futures-channel", + "gdk-pixbuf 0.20.7", + "gdk4", + "gio 0.20.7", + "glib 0.20.7", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango 0.20.7", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate 3.2.0", + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af4b680cee5d2f786a2f91f1c77e95ecf2254522f0ca4edf3a2dce6cb35cecf" +dependencies = [ + "cairo-sys-rs 0.20.7", + "gdk-pixbuf-sys 0.20.7", + "gdk4-sys", + "gio-sys 0.20.8", + "glib-sys 0.20.7", + "gobject-sys 0.20.7", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys 0.20.7", + "system-deps 7.0.3", +] + [[package]] name = "half" version = "2.4.1" @@ -1490,7 +1771,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" dependencies = [ "bitflags 1.3.2", - "glib", + "glib 0.18.5", "javascriptcore-rs-sys", ] @@ -1500,10 +1781,10 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -1626,25 +1907,6 @@ dependencies = [ "redox_syscall 0.5.8", ] -[[package]] -name = "libxdo" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" -dependencies = [ - "libxdo-sys", -] - -[[package]] -name = "libxdo-sys" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" -dependencies = [ - "libc", - "x11", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1773,10 +2035,9 @@ version = "0.15.3" dependencies = [ "crossbeam-channel", "dpi", - "gtk", + "gtk4", "image", "keyboard-types", - "libxdo", "objc2", "objc2-app-kit", "objc2-foundation", @@ -1913,7 +2174,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 2.0.2", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.96", @@ -2180,11 +2441,23 @@ version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" dependencies = [ - "gio", - "glib", + "gio 0.18.4", + "glib 0.18.5", "libc", "once_cell", - "pango-sys", + "pango-sys 0.18.0", +] + +[[package]] +name = "pango" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e89bd74250a03a05cec047b43465469102af803be2bf5e5a1088f8b8455e087" +dependencies = [ + "gio 0.20.7", + "glib 0.20.7", + "libc", + "pango-sys 0.20.7", ] [[package]] @@ -2193,10 +2466,22 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", +] + +[[package]] +name = "pango-sys" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71787e0019b499a5eda889279e4adb455a4f3fdd6870cd5ab7f4a5aa25df6699" +dependencies = [ + "glib-sys 0.20.7", + "gobject-sys 0.20.7", + "libc", + "system-deps 7.0.3", ] [[package]] @@ -2419,14 +2704,22 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" dependencies = [ - "toml_datetime", "toml_edit 0.20.2", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit 0.22.23", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -2628,7 +2921,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "simd_helpers", - "system-deps", + "system-deps 6.2.2", "thiserror", "v_frame", "wasm-bindgen", @@ -2920,8 +3213,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" dependencies = [ "futures-channel", - "gio", - "glib", + "gio 0.18.4", + "glib 0.18.5", "libc", "soup3-sys", ] @@ -2932,11 +3225,11 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -3016,7 +3309,20 @@ version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr", + "cfg-expr 0.15.8", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "system-deps" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +dependencies = [ + "cfg-expr 0.17.2", "heck 0.5.0", "pkg-config", "toml", @@ -3176,9 +3482,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] @@ -3191,7 +3497,7 @@ checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.7.1", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] @@ -3204,7 +3510,18 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +dependencies = [ + "indexmap 2.7.1", + "toml_datetime", + "winnow 0.7.0", ] [[package]] @@ -3528,14 +3845,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" dependencies = [ "bitflags 1.3.2", - "cairo-rs", + "cairo-rs 0.18.5", "gdk", "gdk-sys", - "gio", - "gio-sys", - "glib", - "glib-sys", - "gobject-sys", + "gio 0.18.4", + "gio-sys 0.18.1", + "glib 0.18.5", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "gtk", "gtk-sys", "javascriptcore-rs", @@ -3552,17 +3869,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" dependencies = [ "bitflags 1.3.2", - "cairo-sys-rs", + "cairo-sys-rs 0.18.2", "gdk-sys", - "gio-sys", - "glib-sys", - "gobject-sys", + "gio-sys 0.18.1", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "gtk-sys", "javascriptcore-rs-sys", "libc", "pkg-config", "soup3-sys", - "system-deps", + "system-deps 6.2.2", ] [[package]] @@ -3977,6 +4294,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +dependencies = [ + "memchr", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4440677f..a4bebfe7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,7 @@ categories = ["gui"] rust-version = "1.71" [features] -default = ["libxdo"] -libxdo = ["dep:libxdo"] +default = [] common-controls-v6 = [] serde = ["dep:serde", "dpi/serde"] @@ -42,8 +41,8 @@ features = [ ] [target.'cfg(target_os = "linux")'.dependencies] -gtk = "0.18" -libxdo = { version = "0.6.0", optional = true } +gtk4 = "0.9" +png = "0.17" [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.5.2" diff --git a/examples/gtk.rs b/examples/gtk.rs new file mode 100644 index 00000000..0b807d7d --- /dev/null +++ b/examples/gtk.rs @@ -0,0 +1,65 @@ +#[cfg(target_os = "linux")] +use gtk4::prelude::*; +#[cfg(target_os = "linux")] +use keyboard_types::{Code, Modifiers}; +#[cfg(target_os = "linux")] +use muda::{accelerator::Accelerator, MenuEvent}; + +#[cfg(target_os = "linux")] +fn main() { + // Create a new application + let application = gtk4::Application::builder() + .application_id("com.github.gtk4-rs.examples.menubar") + .build(); + application.connect_startup(on_startup); + application.connect_activate(on_activate); + application.run(); +} + +#[cfg(target_os = "linux")] +fn on_startup(_: >k4::Application) { + MenuEvent::set_event_handler(Some(|event| { + println!("{event:?}"); + })); +} + +#[cfg(target_os = "linux")] +fn on_activate(application: >k4::Application) { + let window = gtk4::ApplicationWindow::builder() + .application(application) + .title("Menubar Example") + .default_width(350) + .default_height(350) + .show_menubar(true) + .build(); + + window.present(); + + let menubar = { + // let file_menu = { + // let about_menu_item = muda::MenuItem::new("About", true, None); + // let quit_menu_item = muda::MenuItem::new( + // "About", + // true, + // Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + // ); + + // let file_menu = muda::Submenu::new("File", true); + // file_menu.append(&about_menu_item).unwrap(); + // file_menu.append(&quit_menu_item).unwrap(); + // file_menu + // }; + + let menubar = muda::Menu::new(); + // menubar.append(&file_menu).unwrap(); + + menubar + }; + + menubar + .init_for_gtk_window(&window, None::<>k4::Window>) + .unwrap(); +} + +#[cfg(not(target_os = "linux"))] +fn main() {} diff --git a/examples/tao.rs b/examples/tao.rs index afa95f68..38cb6e5b 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -88,7 +88,7 @@ fn main() { "custom-i-1", "C&ustom 1", true, - Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), + Some(Accelerator::new(Modifiers::ALT, Code::KeyC)), ); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); @@ -98,7 +98,7 @@ fn main() { "Image custom 1", true, Some(icon), - Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)), + Some(Accelerator::new(Modifiers::CONTROL, Code::KeyC)), ); let check_custom_i_1 = @@ -110,7 +110,7 @@ fn main() { "Check Custom 3", true, true, - Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), + Some(Accelerator::new(Modifiers::SHIFT, Code::KeyD)), ); let copy_i = PredefinedMenuItem::copy(None); @@ -153,11 +153,11 @@ fn main() { menu_bar.init_for_hwnd(window.hwnd() as _); menu_bar.init_for_hwnd(window2.hwnd() as _); } - #[cfg(target_os = "linux")] - { - menu_bar.init_for_gtk_window(window.gtk_window(), window.default_vbox()); - menu_bar.init_for_gtk_window(window2.gtk_window(), window2.default_vbox()); - } + // #[cfg(target_os = "linux")] + // { + // menu_bar.init_for_gtk_window(window.gtk_window(), window.default_vbox()); + // menu_bar.init_for_gtk_window(window2.gtk_window(), window2.default_vbox()); + // } #[cfg(target_os = "macos")] { menu_bar.init_for_nsapp(); @@ -230,8 +230,8 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option

wry::Result<()> { let custom_i_1 = MenuItem::new( "C&ustom 1", true, - Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), + Some(Accelerator::new(Modifiers::ALT, Code::KeyC)), ); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); @@ -106,7 +106,7 @@ fn main() -> wry::Result<()> { "Image custom 1", true, Some(icon), - Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)), + Some(Accelerator::new(Modifiers::CONTROL, Code::KeyC)), ); let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None); @@ -115,7 +115,7 @@ fn main() -> wry::Result<()> { "Check Custom 3", true, true, - Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), + Some(Accelerator::new(Modifiers::SHIFT, Code::KeyD)), ); let copy_i = PredefinedMenuItem::copy(None); @@ -172,12 +172,12 @@ fn main() -> wry::Result<()> { } #[cfg(target_os = "linux")] { - menu_bar - .init_for_gtk_window(window.gtk_window(), window.default_vbox()) - .unwrap(); - menu_bar - .init_for_gtk_window(window2.gtk_window(), window2.default_vbox()) - .unwrap(); + // menu_bar + // .init_for_gtk_window(window.gtk_window(), window.default_vbox()) + // .unwrap(); + // menu_bar + // .init_for_gtk_window(window2.gtk_window(), window2.default_vbox()) + // .unwrap(); } #[cfg(target_os = "macos")] { @@ -257,14 +257,14 @@ fn main() -> wry::Result<()> { .map(|(x, y)| (x.parse::().unwrap(), y.parse::().unwrap())) .unwrap(); - #[cfg(target_os = "linux")] - if let Some(menu_bar) = menu_bar - .clone() - .gtk_menubar_for_gtk_window(window.gtk_window()) - { - use gtk::prelude::*; - y += menu_bar.allocated_height(); - } + // #[cfg(target_os = "linux")] + // if let Some(menu_bar) = menu_bar + // .clone() + // .gtk_menubar_for_gtk_window(window.gtk_window()) + // { + // use gtk::prelude::*; + // y += menu_bar.allocated_height(); + // } show_context_menu(&window, &file_m_c, Some(Position::Logical((x, y).into()))) } @@ -315,8 +315,8 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option

, key: Code) -> Self { - let mut mods = mods.unwrap_or_else(Modifiers::empty); + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] are supported. + pub fn new(mut mods: Modifiers, key: Code) -> Self { if mods.contains(Modifiers::META) { mods.remove(Modifiers::META); mods.insert(Modifiers::SUPER); @@ -71,6 +70,11 @@ impl Accelerator { Self { mods, key, id } } + /// Same as [`Accelerator::new`] but consists of key without a modifier. + pub fn key_only(key: Code) -> Self { + Self::new(Modifiers::empty(), key) + } + fn generate_hash(mods: Modifiers, key: Code) -> u32 { let mut accelerator_str = String::new(); if mods.contains(Modifiers::SHIFT) { @@ -204,7 +208,7 @@ fn parse_accelerator(accelerator: &str) -> Result Result { @@ -423,7 +427,7 @@ fn test_parse_accelerator() { fn test_equality() { let h1 = parse_accelerator("Shift+KeyR").unwrap(); let h2 = parse_accelerator("Shift+KeyR").unwrap(); - let h3 = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyR); + let h3 = Accelerator::new(Modifiers::SHIFT, Code::KeyR); let h4 = parse_accelerator("Alt+KeyR").unwrap(); let h5 = parse_accelerator("Alt+KeyR").unwrap(); let h6 = parse_accelerator("KeyR").unwrap(); diff --git a/src/icon.rs b/src/icon.rs index 14294538..20d59d62 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -34,6 +34,9 @@ pub enum BadIcon { }, /// Produced when underlying OS functionality failed to create the icon OsError(io::Error), + /// Produced when encoding provided RGBA into png + #[cfg(target_os = "linux")] + PngEncodingError(png::EncodingError), } impl fmt::Display for BadIcon { @@ -53,6 +56,8 @@ impl fmt::Display for BadIcon { width, height, pixel_count, width_x_height, ), BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {:?}", e), + #[cfg(target_os = "linux")] + BadIcon::PngEncodingError(e) => write!(f, "PNG encoding error when instantiating the icon: {:?}", e), } } } diff --git a/src/items/predefined.rs b/src/items/predefined.rs index 8ed4a342..d229b0f2 100644 --- a/src/items/predefined.rs +++ b/src/items/predefined.rs @@ -305,43 +305,34 @@ impl PredefinedMenuItemType { pub(crate) fn accelerator(&self) -> Option { match self { - PredefinedMenuItemType::Copy => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)), - PredefinedMenuItemType::Cut => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)), - PredefinedMenuItemType::Paste => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)), - PredefinedMenuItemType::Undo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)), + 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)), #[cfg(target_os = "macos")] PredefinedMenuItemType::Redo => Some(Accelerator::new( Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyZ, )), #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)), - PredefinedMenuItemType::SelectAll => { - Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA)) - } - PredefinedMenuItemType::Minimize => { - Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyM)) - } + 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)), #[cfg(target_os = "macos")] PredefinedMenuItemType::Fullscreen => Some(Accelerator::new( Some(Modifiers::META | Modifiers::CONTROL), Code::KeyF, )), - PredefinedMenuItemType::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)), - PredefinedMenuItemType::HideOthers => Some(Accelerator::new( - Some(CMD_OR_CTRL | Modifiers::ALT), - Code::KeyH, - )), - #[cfg(target_os = "macos")] - PredefinedMenuItemType::CloseWindow => { - Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyW)) + PredefinedMenuItemType::Hide => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyH)), + PredefinedMenuItemType::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)), #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::CloseWindow => { - Some(Accelerator::new(Some(Modifiers::ALT), Code::F4)) - } + PredefinedMenuItemType::CloseWindow => Some(Accelerator::new(Modifiers::ALT, Code::F4)), #[cfg(target_os = "macos")] - PredefinedMenuItemType::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)), + PredefinedMenuItemType::Quit => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyQ)), _ => None, } } diff --git a/src/items/submenu.rs b/src/items/submenu.rs index 3c21c382..89cb8834 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -236,7 +236,7 @@ impl ContextMenu for Submenu { #[cfg(target_os = "linux")] fn show_context_menu_for_gtk_window( &self, - w: >k::Window, + w: >k4::Window, position: Option, ) -> bool { self.inner @@ -244,11 +244,6 @@ impl ContextMenu for Submenu { .show_context_menu_for_gtk_window(w, position) } - #[cfg(target_os = "linux")] - fn gtk_context_menu(&self) -> gtk::Menu { - self.inner.borrow_mut().gtk_context_menu() - } - #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/lib.rs b/src/lib.rs index 25773831..753e9966 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -378,16 +378,10 @@ pub trait ContextMenu { #[cfg(target_os = "linux")] fn show_context_menu_for_gtk_window( &self, - w: >k::Window, + w: >k4::Window, position: Option, ) -> bool; - /// Get the underlying gtk menu reserved for context menus. - /// - /// The returned [`gtk::Menu`] is valid as long as the `ContextMenu` is. - #[cfg(target_os = "linux")] - fn gtk_context_menu(&self) -> gtk::Menu; - /// Shows this menu as a context menu for the specified `NSView`. /// /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. diff --git a/src/menu.rs b/src/menu.rs index 8d5c79fe..d53e5d0b 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -161,18 +161,20 @@ impl Menu { self.inner.borrow().items() } - /// Adds this menu to a [`gtk::Window`] + /// Adds this menu to a [`gtk4::Window`] /// - /// - `container`: this is an optional paramter to specify a container for the [`gtk::MenuBar`], + /// - `container`: this is an optional paramter to specify a container for the [`gtk4::PopoverMenuBar`], /// it is highly recommended to pass a container, otherwise the menubar will be added directly to the window, /// which is usually not the desired behavior. - /// If using a [`gtk::Box`] as a container, it is added using [`Box::pack_start(menubar, false, false, 0)`](gtk::prelude::BoxExt::pack_start) then - /// reordered to be the first child of [`gtk::Box`] using [`Box::reorder_child(menubar, 0)`](gtk::prelude::BoxExt::reorder_child). + /// Supported types of containers are [`gtk4::Box`], [`gtk4::Stack`] and [`gtk4::Fixed`]: + /// - [`gtk4::Box`], menu bar is add at the beginning using [`gtk4::prelude::BoxExt::prepend`] + /// - [`gtk4::Stack`], menu bar is added using [`gtk4::Stack::add_child`]. + /// - [`gtk4::Fixed`], menu bar is put at 0,0 using [`gtk4::prelude::FixedExt::put`]. /// /// ## Example: /// ```no_run - /// let window = gtk::Window::builder().build(); - /// let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); + /// let window = gtk4::Window::builder().build(); + /// let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); /// let menu = muda::Menu::new(); /// // -- snip, add your menu items -- /// menu.init_for_gtk_window(&window, Some(&vbox)); @@ -185,9 +187,9 @@ impl Menu { #[cfg(target_os = "linux")] pub fn init_for_gtk_window(&self, window: &W, container: Option<&C>) -> crate::Result<()> where - W: gtk::prelude::IsA, - W: gtk::prelude::IsA, - C: gtk::prelude::IsA, + W: gtk4::prelude::IsA, + W: gtk4::prelude::IsA, + C: gtk4::prelude::IsA, { self.inner .borrow_mut() @@ -269,11 +271,11 @@ impl Menu { self.inner.borrow_mut().haccel() } - /// Removes this menu from a [`gtk::Window`] + /// Removes this menu from a [`gtk4::Window`] #[cfg(target_os = "linux")] pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow_mut().remove_for_gtk_window(window) } @@ -288,11 +290,11 @@ impl Menu { self.inner.borrow_mut().remove_for_hwnd(hwnd) } - /// Hides this menu from a [`gtk::Window`] + /// Hides this menu from a [`gtk4::Window`] #[cfg(target_os = "linux")] pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow_mut().hide_for_gtk_window(window) } @@ -307,11 +309,11 @@ impl Menu { self.inner.borrow().hide_for_hwnd(hwnd) } - /// Shows this menu on a [`gtk::Window`] + /// Shows this menu on a [`gtk4::Window`] #[cfg(target_os = "linux")] pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow_mut().show_for_gtk_window(window) } @@ -326,21 +328,21 @@ impl Menu { self.inner.borrow().show_for_hwnd(hwnd) } - /// Returns whether this menu visible on a [`gtk::Window`] + /// Returns whether this menu visible on a [`gtk4::Window`] #[cfg(target_os = "linux")] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow().is_visible_on_gtk_window(window) } #[cfg(target_os = "linux")] - /// Returns the [`gtk::MenuBar`] that is associated with this window if it exists. + /// Returns the [`gtk4::MenuBar`] that is associated with this window if it exists. /// This is useful to get information about the menubar for example its height. - pub fn gtk_menubar_for_gtk_window(self, window: &W) -> Option + pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow().gtk_menubar_for_gtk_window(window) } @@ -394,7 +396,7 @@ impl ContextMenu for Menu { #[cfg(target_os = "linux")] fn show_context_menu_for_gtk_window( &self, - window: >k::Window, + window: >k4::Window, position: Option, ) -> bool { self.inner @@ -402,11 +404,6 @@ impl ContextMenu for Menu { .show_context_menu_for_gtk_window(window, position) } - #[cfg(target_os = "linux")] - fn gtk_context_menu(&self) -> gtk::Menu { - self.inner.borrow_mut().gtk_context_menu() - } - #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/menu_id.rs b/src/menu_id.rs index 24036848..5702eeaf 100644 --- a/src/menu_id.rs +++ b/src/menu_id.rs @@ -10,6 +10,11 @@ impl MenuId { pub fn new>(id: S) -> Self { Self(id.as_ref().to_string()) } + + /// Create a new menu id. + pub fn new_owned(id: String) -> Self { + Self(id) + } } impl AsRef for MenuId { diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/accelerator.rs index 7b31e522..a682c923 100644 --- a/src/platform_impl/gtk/accelerator.rs +++ b/src/platform_impl/gtk/accelerator.rs @@ -2,181 +2,70 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use gtk::gdk; -use keyboard_types::{Code, Modifiers}; - -use crate::accelerator::{Accelerator, AcceleratorParseError}; - +/// Converts from muda mnemonic to gtk mnemonic +/// +/// 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 pub fn to_gtk_mnemonic>(string: S) -> String { string .as_ref() + // escape underlines + .replace("_", "__") + // perserve && .replace("&&", "[~~]") + // transfrom & -> _ .replace('&', "_") - .replace("[~~]", "&&") + // revert back && to unsecaped & .replace("[~~]", "&") } +/// Converts from gtk mnemonic to muda mnemonic +/// +/// 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 pub fn from_gtk_mnemonic>(string: S) -> String { string .as_ref() + // transform escaped & to unescaped && + .replace("&", "&&") + // perserve __ .replace("__", "[~~]") + // transfrom _ -> & .replace('_', "&") - .replace("[~~]", "__") -} - -pub fn parse_accelerator( - accelerator: &Accelerator, -) -> Result<(gdk::ModifierType, u32), AcceleratorParseError> { - let key = match &accelerator.key { - Code::KeyA => 'A' as u32, - Code::KeyB => 'B' as u32, - Code::KeyC => 'C' as u32, - Code::KeyD => 'D' as u32, - Code::KeyE => 'E' as u32, - Code::KeyF => 'F' as u32, - Code::KeyG => 'G' as u32, - Code::KeyH => 'H' as u32, - Code::KeyI => 'I' as u32, - Code::KeyJ => 'J' as u32, - Code::KeyK => 'K' as u32, - Code::KeyL => 'L' as u32, - Code::KeyM => 'M' as u32, - Code::KeyN => 'N' as u32, - Code::KeyO => 'O' as u32, - Code::KeyP => 'P' as u32, - Code::KeyQ => 'Q' as u32, - Code::KeyR => 'R' as u32, - Code::KeyS => 'S' as u32, - Code::KeyT => 'T' as u32, - Code::KeyU => 'U' as u32, - Code::KeyV => 'V' as u32, - Code::KeyW => 'W' as u32, - Code::KeyX => 'X' as u32, - Code::KeyY => 'Y' as u32, - Code::KeyZ => 'Z' as u32, - Code::Digit0 => '0' as u32, - Code::Digit1 => '1' as u32, - Code::Digit2 => '2' as u32, - Code::Digit3 => '3' as u32, - Code::Digit4 => '4' as u32, - Code::Digit5 => '5' as u32, - Code::Digit6 => '6' as u32, - Code::Digit7 => '7' as u32, - Code::Digit8 => '8' as u32, - Code::Digit9 => '9' as u32, - Code::Comma => ',' as u32, - Code::Minus => '-' as u32, - Code::Period => '.' as u32, - Code::Space => ' ' as u32, - Code::Equal => '=' as u32, - Code::Semicolon => ';' as u32, - Code::Slash => '/' as u32, - Code::Backslash => '\\' as u32, - Code::Quote => '\'' as u32, - Code::Backquote => '`' as u32, - Code::BracketLeft => '[' as u32, - Code::BracketRight => ']' as u32, - key => { - if let Some(gdk_key) = key_to_raw_key(key) { - *gdk_key - } else { - return Err(AcceleratorParseError::UnsupportedKey(key.to_string())); - } - } - }; - - Ok((modifiers_to_gdk_modifier_type(accelerator.mods), key)) -} - -fn modifiers_to_gdk_modifier_type(modifiers: Modifiers) -> gdk::ModifierType { - let mut result = gdk::ModifierType::empty(); - - result.set( - gdk::ModifierType::MOD1_MASK, - modifiers.contains(Modifiers::ALT), - ); - result.set( - gdk::ModifierType::CONTROL_MASK, - modifiers.contains(Modifiers::CONTROL), - ); - result.set( - gdk::ModifierType::SHIFT_MASK, - modifiers.contains(Modifiers::SHIFT), - ); - result.set( - gdk::ModifierType::META_MASK, - modifiers.contains(Modifiers::SUPER), - ); - - result + // revert back __ to unescaped _ + .replace("[~~]", "_") } -fn key_to_raw_key(src: &Code) -> Option { - use gdk::keys::constants::*; - Some(match src { - Code::Escape => Escape, - Code::Backspace => BackSpace, - - Code::Tab => Tab, - Code::Enter => Return, - - Code::ControlLeft => Control_L, - Code::AltLeft => Alt_L, - Code::ShiftLeft => Shift_L, - Code::MetaLeft => Super_L, - - Code::ControlRight => Control_R, - Code::AltRight => Alt_R, - Code::ShiftRight => Shift_R, - Code::MetaRight => Super_R, +#[cfg(test)] +mod tests { + use crate::platform_impl::platform::accelerator::{from_gtk_mnemonic, to_gtk_mnemonic}; - Code::CapsLock => Caps_Lock, - Code::F1 => F1, - Code::F2 => F2, - Code::F3 => F3, - Code::F4 => F4, - Code::F5 => F5, - Code::F6 => F6, - Code::F7 => F7, - Code::F8 => F8, - Code::F9 => F9, - Code::F10 => F10, - Code::F11 => F11, - Code::F12 => F12, - Code::F13 => F13, - Code::F14 => F14, - Code::F15 => F15, - Code::F16 => F16, - Code::F17 => F17, - Code::F18 => F18, - Code::F19 => F19, - Code::F20 => F20, - Code::F21 => F21, - Code::F22 => F22, - Code::F23 => F23, - Code::F24 => F24, + #[test] + fn it_converts() { + assert_eq!(to_gtk_mnemonic("H&ello"), "H_ello"); + assert_eq!(to_gtk_mnemonic("H&&ello"), "H&ello"); + assert_eq!(to_gtk_mnemonic("H&&&ello"), "H&_ello"); + assert_eq!(to_gtk_mnemonic("H_ello"), "H__ello"); + assert_eq!(to_gtk_mnemonic("H__ello"), "H____ello"); + } - Code::PrintScreen => Print, - Code::ScrollLock => Scroll_Lock, - // Pause/Break not audio. - Code::Pause => Pause, + #[test] + fn it_converts_back() { + let str = "H&ello"; + assert_eq!(from_gtk_mnemonic(to_gtk_mnemonic(str)), str); - Code::Insert => Insert, - Code::Delete => Delete, - Code::Home => Home, - Code::End => End, - Code::PageUp => Page_Up, - Code::PageDown => Page_Down, + let str = "H&&ello"; + assert_eq!(from_gtk_mnemonic(to_gtk_mnemonic(str)), str); - Code::NumLock => Num_Lock, + let str = "H&&&ello"; + assert_eq!(from_gtk_mnemonic(to_gtk_mnemonic(str)), str); - Code::ArrowUp => Up, - Code::ArrowDown => Down, - Code::ArrowLeft => Left, - Code::ArrowRight => Right, + let str = "H_ello"; + assert_eq!(from_gtk_mnemonic(to_gtk_mnemonic(str)), str); - Code::ContextMenu => Menu, - Code::WakeUp => WakeUp, - _ => return None, - }) + let str = "H__ello"; + assert_eq!(from_gtk_mnemonic(to_gtk_mnemonic(str)), str); + } } diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs index 31ddd003..825b8a45 100644 --- a/src/platform_impl/gtk/icon.rs +++ b/src/platform_impl/gtk/icon.rs @@ -2,32 +2,11 @@ // Copyright 2021-2022 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 -use gtk::gdk_pixbuf::{Colorspace, Pixbuf}; - use crate::icon::BadIcon; /// An icon used for the window titlebar, taskbar, etc. #[derive(Debug, Clone)] -pub struct PlatformIcon { - raw: Vec, - width: i32, - height: i32, - row_stride: i32, -} - -impl From for Pixbuf { - fn from(icon: PlatformIcon) -> Self { - Pixbuf::from_mut_slice( - icon.raw, - gtk::gdk_pixbuf::Colorspace::Rgb, - true, - 8, - icon.width, - icon.height, - icon.row_stride, - ) - } -} +pub struct PlatformIcon(gtk4::gio::BytesIcon); impl PlatformIcon { /// Creates an `Icon` from 32bpp RGBA data. @@ -35,31 +14,23 @@ 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 row_stride = - Pixbuf::calculate_rowstride(Colorspace::Rgb, true, 8, width as i32, height as i32); - Ok(Self { - raw: rgba, - width: width as i32, - height: height as i32, - row_stride, - }) - } + let mut w = Vec::with_capacity(rgba.len()); + + let mut encoder = png::Encoder::new(&mut w, width, height); + encoder.set_color(png::ColorType::Rgba); + encoder.set_depth(png::BitDepth::Eight); + let mut writer = encoder.write_header().map_err(BadIcon::PngEncodingError)?; + writer + .write_image_data(&rgba) + .map_err(BadIcon::PngEncodingError)?; + writer.finish().map_err(BadIcon::PngEncodingError)?; + + let bytes = gtk4::glib::Bytes::from_owned(w); - pub fn to_pixbuf(&self) -> Pixbuf { - Pixbuf::from_mut_slice( - self.raw.clone(), - gtk::gdk_pixbuf::Colorspace::Rgb, - true, - 8, - self.width, - self.height, - self.row_stride, - ) + Ok(Self(gtk4::gio::BytesIcon::new(&bytes))) } - pub fn to_pixbuf_scale(&self, w: i32, h: i32) -> Pixbuf { - self.to_pixbuf() - .scale_simple(w, h, gtk::gdk_pixbuf::InterpType::Bilinear) - .unwrap() + pub fn bytes_icon(&self) -> >k4::gio::BytesIcon { + &self.0 } } diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 7a9875ab..b9388661 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -5,89 +5,45 @@ mod accelerator; mod icon; +use std::collections::{hash_map::Entry, HashMap}; + +use dpi::Position; +use gtk4::{gio::SimpleActionGroup, prelude::*}; pub(crate) use icon::PlatformIcon; use crate::{ accelerator::Accelerator, - dpi::Position, - icon::{Icon, NativeIcon}, - items::*, util::{AddOp, Counter}, - IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType, -}; -use accelerator::{from_gtk_mnemonic, parse_accelerator, to_gtk_mnemonic}; -use glib::translate::ToGlibPtr; -use gtk::{gdk, glib, prelude::*, AboutDialog, Container, Orientation}; -use std::{ - cell::RefCell, - collections::{hash_map::Entry, HashMap}, - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, + Icon, IsMenuItem, MenuId, MenuItemKind, MenuItemType, NativeIcon, PredefinedMenuItemType, }; static COUNTER: Counter = Counter::new(); -macro_rules! is_item_supported { - ($item:tt) => {{ - let child = $item.child(); - let child_ = child.borrow(); - let supported = if let Some(predefined_item_type) = &child_.predefined_item_type { - matches!( - predefined_item_type, - PredefinedMenuItemType::Separator - | PredefinedMenuItemType::Copy - | PredefinedMenuItemType::Cut - | PredefinedMenuItemType::Paste - | PredefinedMenuItemType::SelectAll - | PredefinedMenuItemType::About(_) - ) - } else { - true - }; - drop(child_); - supported - }}; -} +struct GtkMenuBar(gtk4::PopoverMenuBar, gtk4::gio::Menu); -macro_rules! return_if_item_not_supported { - ($item:tt) => { - if !is_item_supported!($item) { - return Ok(()); - } - }; +impl GtkMenuBar { + fn new() -> Self { + let menu = gtk4::gio::Menu::new(); + Self(gtk4::PopoverMenuBar::from_model(Some(&menu)), menu) + } + + fn widget(&self) -> >k4::PopoverMenuBar { + &self.0 + } } pub struct Menu { id: MenuId, - children: Vec>>, - // TODO: maybe save a reference to the window? - gtk_menubars: HashMap, - accel_group: Option, - gtk_menu: (u32, Option), // dedicated menu for tray or context menus -} - -impl Drop for Menu { - fn drop(&mut self) { - for (id, menu) in &self.gtk_menubars { - drop_children_from_menu_and_destroy(*id, menu, &self.children); - unsafe { menu.destroy() } - } - - if let (id, Some(menu)) = &self.gtk_menu { - drop_children_from_menu_and_destroy(*id, menu, &self.children); - unsafe { menu.destroy() } - } - } + gtk_menubars: HashMap, + action_group: Option, } impl Menu { pub fn new(id: Option) -> Self { Self { - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - children: Vec::new(), + id: id.unwrap_or_else(|| MenuId::new_owned(COUNTER.next().to_string())), gtk_menubars: HashMap::new(), - accel_group: None, - gtk_menu: (COUNTER.next(), None), + action_group: None, } } @@ -95,158 +51,16 @@ impl Menu { &self.id } - pub fn add_menu_item(&mut self, item: &dyn crate::IsMenuItem, op: AddOp) -> crate::Result<()> { - if is_item_supported!(item) { - for (menu_id, menu_bar) in &self.gtk_menubars { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, true)?; - match op { - AddOp::Append => menu_bar.append(>k_item), - AddOp::Insert(position) => menu_bar.insert(>k_item, position as i32), - } - gtk_item.show(); - } - - { - if let (menu_id, Some(menu)) = &self.gtk_menu { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - match op { - AddOp::Append => menu.append(>k_item), - AddOp::Insert(position) => menu.insert(>k_item, position as i32), - } - gtk_item.show(); - } - } - } - - match op { - AddOp::Append => self.children.push(item.child()), - AddOp::Insert(position) => self.children.insert(position, item.child()), - } - - Ok(()) - } - - fn add_menu_item_with_id(&self, item: &dyn crate::IsMenuItem, id: u32) -> crate::Result<()> { - return_if_item_not_supported!(item); - - for (menu_id, menu_bar) in self.gtk_menubars.iter().filter(|m| *m.0 == id) { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, true)?; - menu_bar.append(>k_item); - gtk_item.show(); - } - - Ok(()) - } - - fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> { - return_if_item_not_supported!(item); - - let (menu_id, menu) = &self.gtk_menu; - if let Some(menu) = menu { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - menu.append(>k_item); - gtk_item.show(); - } - - Ok(()) + pub fn add_menu_item(&self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { + todo!() } - pub fn remove(&mut self, item: &dyn crate::IsMenuItem) -> crate::Result<()> { - self.remove_inner(item, true, None) - } - - fn remove_inner( - &mut self, - item: &dyn crate::IsMenuItem, - remove_from_cache: bool, - id: Option, - ) -> crate::Result<()> { - // get child - let child = { - let index = self - .children - .iter() - .position(|e| e.borrow().id == item.id()) - .ok_or(crate::Error::NotAChildOfThisMenu)?; - if remove_from_cache { - self.children.remove(index) - } else { - self.children.get(index).cloned().unwrap() - } - }; - - for (menu_id, menu_bar) in &self.gtk_menubars { - // check if we are removing this item from all gtk_menubars - // which is usually when this is the item the user is actaully removing - // or if we are removing from a specific menu (id) - // which is when the actual item being removed is a submenu - // and we are iterating through its children and removing - // each child gtk items that are related to this submenu. - if id.map(|i| i == *menu_id).unwrap_or(true) { - // bail this is not a supported item like a close_window predefined menu item - if is_item_supported!(item) { - let mut child_ = child.borrow_mut(); - - if child_.item_type == MenuItemType::Submenu { - let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned(); - if let Some(menus) = menus { - for (id, menu) in menus { - // iterate through children and only remove the gtk items - // related to this submenu - for item in child_.items() { - child_.remove_inner(item.as_ref(), false, Some(id))?; - } - unsafe { menu.destroy() }; - } - } - child_.gtk_menus.as_mut().unwrap().remove(menu_id); - } - - // remove all the gtk items that are related to this gtk menubar and destroy it - if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(menu_id) { - for item in items { - menu_bar.remove(&item); - if let Some(accel_group) = &child_.accel_group { - if let Some((mods, key)) = child_.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() }; - } - }; - } - } - } - - // remove from the gtk menu assigned to the context menu - if remove_from_cache { - if let (id, Some(menu)) = &self.gtk_menu { - let child_ = child.borrow_mut(); - if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) { - for item in items { - menu.remove(&item); - if let Some(accel_group) = &child_.accel_group { - if let Some((mods, key)) = child_.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() }; - } - }; - } - } - Ok(()) + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + todo!() } pub fn items(&self) -> Vec { - self.children - .iter() - .map(|c| c.borrow().kind(c.clone())) - .collect() + todo!() } pub fn init_for_gtk_window( @@ -255,21 +69,20 @@ impl Menu { container: Option<&C>, ) -> crate::Result<()> where - W: IsA, - W: IsA, - C: IsA, + W: gtk4::prelude::IsA, + W: gtk4::prelude::IsA, + C: gtk4::prelude::IsA, { let id = window.as_ptr() as u32; - if self.accel_group.is_none() { - self.accel_group = Some(gtk::AccelGroup::new()); + if self.action_group.is_none() { + self.action_group = Some(gtk4::gio::SimpleActionGroup::new()); } // This is the first time this method has been called on this window // so we need to create the menubar and its parent box if let Entry::Vacant(e) = self.gtk_menubars.entry(id) { - let menu_bar = gtk::MenuBar::new(); - e.insert(menu_bar); + e.insert(GtkMenuBar::new()); } else { return Err(crate::Error::AlreadyInitialized); } @@ -277,217 +90,82 @@ impl Menu { // Construct the entries of the menubar let menu_bar = &self.gtk_menubars[&id]; - window.add_accel_group(self.accel_group.as_ref().unwrap()); + window.insert_action_group(self.id().as_ref(), self.action_group.as_ref()); - for item in self.items() { - self.add_menu_item_with_id(item.as_ref(), id)?; - } + // TODO: + // for item in self.items() { + // self.add_menu_item_with_id(item.as_ref(), id)?; + // } // add the menubar to the specified widget, otherwise to the window if let Some(container) = container { if container.type_().name() == "GtkBox" { - let gtk_box = container.dynamic_cast_ref::().unwrap(); - gtk_box.pack_start(menu_bar, false, false, 0); - gtk_box.reorder_child(menu_bar, 0); - } else { - container.add(menu_bar); + let gtk_box = container.dynamic_cast_ref::().unwrap(); + gtk_box.prepend(menu_bar.widget()); + } else if container.type_().name() == "GtkFixed" { + let gtk_box = container.dynamic_cast_ref::().unwrap(); + gtk_box.put(menu_bar.widget(), 0., 0.); + } else if container.type_().name() == "GtkStack" { + let gtk_box = container.dynamic_cast_ref::().unwrap(); + gtk_box.add_child(menu_bar.widget()); } } else { - window.add(menu_bar); + window.set_child(Some(menu_bar.widget())); } - // Show the menubar - menu_bar.show(); + // show the menu bar + menu_bar.widget().set_visible(true); Ok(()) } - pub fn remove_for_gtk_window(&mut self, window: &W) -> crate::Result<()> + pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: IsA, + W: gtk4::prelude::IsA, { - let id = window.as_ptr() as u32; - - // Remove from our cache - let menu_bar = self - .gtk_menubars - .remove(&id) - .ok_or(crate::Error::NotInitialized)?; - - for item in self.items() { - let _ = self.remove_inner(item.as_ref(), false, Some(id)); - } - - // Remove the [`gtk::Menubar`] from the widget tree - unsafe { menu_bar.destroy() }; - // Detach the accelerators from the window - window.remove_accel_group(self.accel_group.as_ref().unwrap()); - Ok(()) + todo!() } - pub fn hide_for_gtk_window(&mut self, window: &W) -> crate::Result<()> + pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: IsA, + W: gtk4::prelude::IsA, { - self.gtk_menubars - .get(&(window.as_ptr() as u32)) - .ok_or(crate::Error::NotInitialized)? - .hide(); - Ok(()) + todo!() } pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: IsA, + W: gtk4::prelude::IsA, { - self.gtk_menubars - .get(&(window.as_ptr() as u32)) - .ok_or(crate::Error::NotInitialized)? - .show_all(); - Ok(()) + todo!() } + #[cfg(target_os = "linux")] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where - W: IsA, + W: gtk4::prelude::IsA, { - self.gtk_menubars - .get(&(window.as_ptr() as u32)) - .map(|m| m.get_visible()) - .unwrap_or(false) + todo!() } - pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option + pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option where - W: gtk::prelude::IsA, + W: gtk4::prelude::IsA, { - self.gtk_menubars.get(&(window.as_ptr() as u32)).cloned() + todo!() } pub fn show_context_menu_for_gtk_window( - &mut self, - widget: &impl IsA, + &self, + window: >k4::Window, position: Option, ) -> bool { - show_context_menu(self.gtk_context_menu(), widget, position) - } - - pub fn gtk_context_menu(&mut self) -> gtk::Menu { - let mut add_items = false; - - { - if self.gtk_menu.1.is_none() { - self.gtk_menu.1 = Some(gtk::Menu::new()); - add_items = true; - } - } - - if add_items { - for item in self.items() { - self.add_menu_item_to_context_menu(item.as_ref()).unwrap(); - } - } - - self.gtk_menu.1.as_ref().unwrap().clone() + todo!() } } -/// A generic child in a menu -#[derive(Debug, Default)] -pub struct MenuChild { - // shared fields between submenus and menu items - item_type: MenuItemType, - text: String, - enabled: bool, - id: MenuId, - - gtk_menu_items: Rc>>>, - - // menu item fields - accelerator: Option, - gtk_accelerator: Option<(gdk::ModifierType, u32)>, - - // predefined menu item fields - predefined_item_type: Option, - - // check menu item fields - checked: Option>, - is_syncing_checked_state: Option>, - - // icon menu item fields - icon: Option, - - // submenu fields - pub children: Option>>>, - gtk_menus: Option>>, - gtk_menu: Option<(u32, Option)>, // dedicated menu for tray or context menus - accel_group: Option, -} - -impl Drop for MenuChild { - fn drop(&mut self) { - if self.item_type == MenuItemType::Submenu { - for menus in self.gtk_menus.as_ref().unwrap().values() { - for (id, menu) in menus { - drop_children_from_menu_and_destroy(*id, menu, self.children.as_ref().unwrap()); - unsafe { menu.destroy() }; - } - } +pub struct MenuChild {} - if let Some((id, Some(menu))) = &self.gtk_menu { - drop_children_from_menu_and_destroy(*id, menu, self.children.as_ref().unwrap()); - unsafe { menu.destroy() }; - } - } - - for items in self.gtk_menu_items.borrow().values() { - for item in items { - if let Some(accel_group) = &self.accel_group { - if let Some((mods, key)) = self.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() }; - } - } - } -} - -fn drop_children_from_menu_and_destroy( - id: u32, - menu: &impl IsA, - children: &Vec>>, -) { - for child in children { - let mut child_ = child.borrow_mut(); - { - let mut menu_items = child_.gtk_menu_items.borrow_mut(); - if let Some(items) = menu_items.remove(&id) { - for item in items { - menu.remove(&item); - if let Some(accel_group) = &child_.accel_group { - if let Some((mods, key)) = child_.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() } - } - } - } - - if child_.item_type == MenuItemType::Submenu { - if let Some(menus) = child_.gtk_menus.as_mut().unwrap().remove(&id) { - for (id, menu) in menus { - let children = child_.children.as_ref().unwrap(); - drop_children_from_menu_and_destroy(id, &menu, children); - unsafe { menu.destroy() } - } - } - } - } -} - -/// Constructors impl MenuChild { pub fn new( text: &str, @@ -495,967 +173,110 @@ impl MenuChild { accelerator: Option, id: Option, ) -> Self { - Self { - text: text.to_string(), - enabled, - accelerator, - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - item_type: MenuItemType::MenuItem, - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - accel_group: None, - checked: None, - children: None, - gtk_accelerator: None, - gtk_menu: None, - gtk_menus: None, - icon: None, - is_syncing_checked_state: None, - predefined_item_type: None, - } - } - - pub fn new_submenu(text: &str, enabled: bool, id: Option) -> Self { - Self { - text: text.to_string(), - enabled, - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - children: Some(Vec::new()), - item_type: MenuItemType::Submenu, - gtk_menu: Some((COUNTER.next(), None)), - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - gtk_menus: Some(HashMap::new()), - accel_group: None, - gtk_accelerator: None, - icon: None, - is_syncing_checked_state: None, - predefined_item_type: None, - accelerator: None, - checked: None, - } - } - - pub(crate) fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { - Self { - text: text.unwrap_or_else(|| item_type.text().to_string()), - enabled: true, - accelerator: item_type.accelerator(), - id: MenuId(COUNTER.next().to_string()), - item_type: MenuItemType::Predefined, - predefined_item_type: Some(item_type), - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - accel_group: None, - checked: None, - children: None, - gtk_accelerator: None, - gtk_menu: None, - gtk_menus: None, - icon: None, - is_syncing_checked_state: None, - } - } - - pub fn new_check( - text: &str, - enabled: bool, - checked: bool, - accelerator: Option, - id: Option, - ) -> Self { - Self { - text: text.to_string(), - enabled, - checked: Some(Rc::new(AtomicBool::new(checked))), - is_syncing_checked_state: Some(Rc::new(AtomicBool::new(false))), - accelerator, - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - item_type: MenuItemType::Check, - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - accel_group: None, - children: None, - gtk_accelerator: None, - gtk_menu: None, - gtk_menus: None, - icon: None, - predefined_item_type: None, - } - } - - pub fn new_icon( - text: &str, - enabled: bool, - icon: Option, - accelerator: Option, - id: Option, - ) -> Self { - Self { - text: text.to_string(), - enabled, - icon, - accelerator, - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - item_type: MenuItemType::Icon, - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - accel_group: None, - checked: None, - children: None, - gtk_accelerator: None, - gtk_menu: None, - gtk_menus: None, - is_syncing_checked_state: None, - predefined_item_type: None, - } - } - - pub fn new_native_icon( - text: &str, - enabled: bool, - _native_icon: Option, - accelerator: Option, - id: Option, - ) -> Self { - Self { - text: text.to_string(), - enabled, - accelerator, - id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), - item_type: MenuItemType::Icon, - gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), - accel_group: None, - checked: None, - children: None, - gtk_accelerator: None, - gtk_menu: None, - gtk_menus: None, - icon: None, - is_syncing_checked_state: None, - predefined_item_type: None, - } + Self {} } -} -/// Shared methods -impl MenuChild { - pub(crate) fn item_type(&self) -> MenuItemType { - self.item_type + pub fn id(&self) -> &MenuId { + todo!() } - pub fn id(&self) -> &MenuId { - &self.id + pub fn item_type(&self) -> &MenuItemType { + todo!() } pub fn text(&self) -> String { - match self - .gtk_menu_items - .borrow() - .values() - .collect::>() - .first() - .map(|v| v.first()) - .map(|e| e.map(|i| i.label().map(from_gtk_mnemonic))) - { - Some(Some(Some(text))) => text, - _ => self.text.clone(), - } + todo!() } - pub fn set_text(&mut self, text: &str) { - self.text = text.to_string(); - let text = to_gtk_mnemonic(text); - for items in self.gtk_menu_items.borrow().values() { - for i in items { - i.set_label(&text); - } - } + pub fn set_text(&self, text: &str) { + todo!() } pub fn is_enabled(&self) -> bool { - match self - .gtk_menu_items - .borrow() - .values() - .collect::>() - .first() - .map(|v| v.first()) - .map(|e| e.map(|i| i.is_sensitive())) - { - Some(Some(enabled)) => enabled, - _ => self.enabled, - } - } - - pub fn set_enabled(&mut self, enabled: bool) { - self.enabled = enabled; - for items in self.gtk_menu_items.borrow().values() { - for i in items { - i.set_sensitive(enabled); - } - } - } - - pub fn set_accelerator(&mut self, accelerator: Option) -> crate::Result<()> { - let prev_accel = self.gtk_accelerator.as_ref(); - let new_accel = accelerator.as_ref().map(parse_accelerator).transpose()?; - - for items in self.gtk_menu_items.borrow().values() { - for i in items { - if let Some((mods, key)) = prev_accel { - if let Some(accel_group) = &self.accel_group { - i.remove_accelerator(accel_group, *key, *mods); - } - } - if let Some((mods, key)) = new_accel { - if let Some(accel_group) = &self.accel_group { - i.add_accelerator( - "activate", - accel_group, - key, - mods, - gtk::AccelFlags::VISIBLE, - ) - } - } - } - } - - self.gtk_accelerator = new_accel; - self.accelerator = accelerator; - - Ok(()) - } -} - -/// CheckMenuItem methods -impl MenuChild { - pub fn is_checked(&self) -> bool { - match self - .gtk_menu_items - .borrow() - .values() - .collect::>() - .first() - .map(|v| v.first()) - .map(|e| e.map(|i| i.downcast_ref::().unwrap().is_active())) - { - Some(Some(checked)) => checked, - _ => self.checked.as_ref().unwrap().load(Ordering::Relaxed), - } + todo!() } - pub fn set_checked(&mut self, checked: bool) { - self.checked - .as_ref() - .unwrap() - .store(checked, Ordering::Release); - let is_syncing = self.is_syncing_checked_state.as_ref().unwrap(); - is_syncing.store(true, Ordering::Release); - for items in self.gtk_menu_items.borrow().values() { - for i in items { - i.downcast_ref::() - .unwrap() - .set_active(checked); - } - } - is_syncing.store(false, Ordering::Release); + pub fn set_enabled(&self, enabled: bool) { + todo!() } -} -/// IconMenuItem methods -impl MenuChild { - pub fn set_icon(&mut self, icon: Option) { - self.icon.clone_from(&icon); - - let pixbuf = icon.map(|i| i.inner.to_pixbuf_scale(16, 16)); - for items in self.gtk_menu_items.borrow().values() { - for i in items { - let box_container = i.child().unwrap().downcast::().unwrap(); - box_container.children()[0] - .downcast_ref::() - .unwrap() - .set_pixbuf(pixbuf.as_ref()) - } - } + pub fn set_accelerator(&self, accelerator: Option) -> crate::Result<()> { + todo!() } } -/// Submenu methods impl MenuChild { - pub fn add_menu_item(&mut self, item: &dyn crate::IsMenuItem, op: AddOp) -> crate::Result<()> { - if is_item_supported!(item) { - for menus in self.gtk_menus.as_ref().unwrap().values() { - for (menu_id, menu) in menus { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - match op { - AddOp::Append => menu.append(>k_item), - AddOp::Insert(position) => menu.insert(>k_item, position as i32), - } - gtk_item.show(); - } - } - - { - if let (menu_id, Some(menu)) = self.gtk_menu.as_ref().unwrap() { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - match op { - AddOp::Append => menu.append(>k_item), - AddOp::Insert(position) => menu.insert(>k_item, position as i32), - } - gtk_item.show(); - } - } - } - - match op { - AddOp::Append => self.children.as_mut().unwrap().push(item.child()), - AddOp::Insert(position) => self - .children - .as_mut() - .unwrap() - .insert(position, item.child()), - } - - Ok(()) - } - - fn add_menu_item_with_id(&self, item: &dyn crate::IsMenuItem, id: u32) -> crate::Result<()> { - return_if_item_not_supported!(item); - - for menus in self.gtk_menus.as_ref().unwrap().values() { - for (menu_id, menu) in menus.iter().filter(|m| m.0 == id) { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - menu.append(>k_item); - gtk_item.show(); - } - } - - Ok(()) + pub fn new_submenu(text: &str, enabled: bool, id: Option) -> Self { + Self {} } - fn add_menu_item_to_context_menu(&self, item: &dyn crate::IsMenuItem) -> crate::Result<()> { - return_if_item_not_supported!(item); - - let (menu_id, menu) = self.gtk_menu.as_ref().unwrap(); - if let Some(menu) = menu { - let gtk_item = - item.make_gtk_menu_item(*menu_id, self.accel_group.as_ref(), true, false)?; - menu.append(>k_item); - gtk_item.show(); - } - - Ok(()) + pub fn add_menu_item(&self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { + todo!() } - pub fn remove(&mut self, item: &dyn crate::IsMenuItem) -> crate::Result<()> { - self.remove_inner(item, true, None) - } - - fn remove_inner( - &mut self, - item: &dyn crate::IsMenuItem, - remove_from_cache: bool, - id: Option, - ) -> crate::Result<()> { - // get child - let child = { - let index = self - .children - .as_ref() - .unwrap() - .iter() - .position(|e| e.borrow().id == item.id()) - .ok_or(crate::Error::NotAChildOfThisMenu)?; - if remove_from_cache { - self.children.as_mut().unwrap().remove(index) - } else { - self.children.as_ref().unwrap().get(index).cloned().unwrap() - } - }; - - for menus in self.gtk_menus.as_ref().unwrap().values() { - for (menu_id, menu) in menus { - // check if we are removing this item from all gtk_menus - // which is usually when this is the item the user is actaully removing - // or if we are removing from a specific menu (id) - // which is when the actual item being removed is a submenu - // and we are iterating through its children and removing - // each child gtk items that are related to this submenu. - if id.map(|i| i == *menu_id).unwrap_or(true) { - // bail this is not a supported item like a close_window predefined menu item - if is_item_supported!(item) { - let mut child_ = child.borrow_mut(); - - if child_.item_type == MenuItemType::Submenu { - let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned(); - if let Some(menus) = menus { - for (id, menu) in menus { - // iterate through children and only remove the gtk items - // related to this submenu - for item in child_.items() { - child_.remove_inner(item.as_ref(), false, Some(id))?; - } - unsafe { menu.destroy() }; - } - } - child_.gtk_menus.as_mut().unwrap().remove(menu_id); - } - - // remove all the gtk items that are related to this gtk menu and destroy it - if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(menu_id) { - for item in items { - menu.remove(&item); - if let Some(accel_group) = &child_.accel_group { - if let Some((mods, key)) = child_.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() }; - } - }; - } - } - } - } - - // remove from the gtk menu assigned to the context menu - if remove_from_cache { - if let (id, Some(menu)) = self.gtk_menu.as_ref().unwrap() { - let child_ = child.borrow_mut(); - if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) { - for item in items { - menu.remove(&item); - if let Some(accel_group) = &child_.accel_group { - if let Some((mods, key)) = child_.gtk_accelerator { - item.remove_accelerator(accel_group, key, mods); - } - } - unsafe { item.destroy() }; - } - }; - } - } - - Ok(()) + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + todo!() } pub fn items(&self) -> Vec { - self.children - .as_ref() - .unwrap() - .iter() - .map(|c| c.borrow().kind(c.clone())) - .collect() + todo!() } pub fn show_context_menu_for_gtk_window( - &mut self, - widget: &impl IsA, + &self, + w: >k4::Window, position: Option, ) -> bool { - show_context_menu(self.gtk_context_menu(), widget, position) - } - - pub fn gtk_context_menu(&mut self) -> gtk::Menu { - let mut add_items = false; - { - let gtk_menu = self.gtk_menu.as_mut().unwrap(); - if gtk_menu.1.is_none() { - gtk_menu.1 = Some(gtk::Menu::new()); - add_items = true; - } - } - - if add_items { - for item in self.items() { - self.add_menu_item_to_context_menu(item.as_ref()).unwrap(); - } - } - - self.gtk_menu.as_ref().unwrap().1.as_ref().unwrap().clone() + todo!() } } -macro_rules! register_accel { - ($self:ident, $item:ident, $accel_group:ident) => { - $self.gtk_accelerator = $self - .accelerator - .as_ref() - .map(parse_accelerator) - .transpose()?; - - if let Some((mods, key)) = &$self.gtk_accelerator { - if let Some(accel_group) = $accel_group { - $item.add_accelerator( - "activate", - accel_group, - *key, - *mods, - gtk::AccelFlags::VISIBLE, - ) - } - } - }; -} - -/// Gtk menu item creation methods impl MenuChild { - fn create_gtk_item_for_submenu( - &mut self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - ) -> crate::Result { - let submenu = gtk::Menu::new(); - let item = gtk::MenuItem::builder() - .label(to_gtk_mnemonic(&self.text)) - .use_underline(true) - .submenu(&submenu) - .sensitive(self.enabled) - .build(); - - item.show(); - item.set_submenu(Some(&submenu)); - - self.accel_group = accel_group.cloned(); - - let mut id = 0; - if add_to_cache { - id = COUNTER.next(); - - self.gtk_menu_items - .borrow_mut() - .entry(menu_id) - .or_default() - .push(item.clone()); - self.gtk_menus - .as_mut() - .unwrap() - .entry(menu_id) - .or_default() - .push((id, submenu.clone())); - } - - for item in self.items() { - if add_to_cache { - self.add_menu_item_with_id(item.as_ref(), id)?; - } else { - let gtk_item = item.make_gtk_menu_item(0, None, false, false)?; - submenu.append(>k_item); - } - } - - Ok(item) - } - - fn create_gtk_item_for_menu_item( - &mut self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - ) -> crate::Result { - let item = gtk::MenuItem::builder() - .label(to_gtk_mnemonic(&self.text)) - .use_underline(true) - .sensitive(self.enabled) - .build(); - - self.accel_group = accel_group.cloned(); - - register_accel!(self, item, accel_group); - - let id = self.id.clone(); - item.connect_activate(move |_| { - MenuEvent::send(crate::MenuEvent { id: id.clone() }); - }); - - if add_to_cache { - self.gtk_menu_items - .borrow_mut() - .entry(menu_id) - .or_default() - .push(item.clone()); - } - - Ok(item) + pub fn new_predefined(item: PredefinedMenuItemType, text: Option) -> Self { + Self {} } +} - fn create_gtk_item_for_predefined_menu_item( - &mut self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - ) -> crate::Result { - let text = self.text.clone(); - self.gtk_accelerator = self - .accelerator - .as_ref() - .map(parse_accelerator) - .transpose()?; - let predefined_item_type = self.predefined_item_type.clone().unwrap(); - - let make_item = || { - gtk::MenuItem::builder() - .label(to_gtk_mnemonic(&text)) - .use_underline(true) - .sensitive(true) - .build() - }; - let register_accel = |item: >k::MenuItem| { - if let Some((mods, key)) = &self.gtk_accelerator { - if let Some(accel_group) = accel_group { - item.add_accelerator( - "activate", - accel_group, - *key, - *mods, - gtk::AccelFlags::VISIBLE, - ) - } - } - }; - - let item = match predefined_item_type { - PredefinedMenuItemType::Separator => { - gtk::SeparatorMenuItem::new().upcast::() - } - PredefinedMenuItemType::Copy - | PredefinedMenuItemType::Cut - | PredefinedMenuItemType::Paste - | PredefinedMenuItemType::SelectAll => { - let item = make_item(); - let (mods, key) = - parse_accelerator(&predefined_item_type.accelerator().unwrap()).unwrap(); - item.child() - .unwrap() - .downcast::() - .unwrap() - .set_accel(key, mods); - item.connect_activate(move |_| { - // TODO: wayland - #[cfg(feature = "libxdo")] - if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence(predefined_item_type.xdo_keys(), 0); - } - }); - item - } - PredefinedMenuItemType::About(metadata) => { - let item = make_item(); - register_accel(&item); - item.connect_activate(move |_| { - if let Some(metadata) = &metadata { - let mut builder = AboutDialog::builder().modal(true).resizable(false); - - if let Some(name) = &metadata.name { - builder = builder.program_name(name); - } - if let Some(version) = &metadata.full_version() { - builder = builder.version(version); - } - if let Some(authors) = &metadata.authors { - builder = builder.authors(authors.clone()); - } - if let Some(comments) = &metadata.comments { - builder = builder.comments(comments); - } - if let Some(copyright) = &metadata.copyright { - builder = builder.copyright(copyright); - } - if let Some(license) = &metadata.license { - builder = builder.license(license); - } - if let Some(website) = &metadata.website { - builder = builder.website(website); - } - if let Some(website_label) = &metadata.website_label { - builder = builder.website_label(website_label); - } - if let Some(icon) = &metadata.icon { - builder = builder.logo(&icon.inner.to_pixbuf()); - } - - let about = builder.build(); - about.run(); - unsafe { - about.destroy(); - } - } - }); - item - } - _ => unreachable!(), - }; - - if add_to_cache { - self.gtk_menu_items - .borrow_mut() - .entry(menu_id) - .or_default() - .push(item.clone()); - } - Ok(item) - } - - fn create_gtk_item_for_check_menu_item( - &mut self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - ) -> crate::Result { - let item = gtk::CheckMenuItem::builder() - .label(to_gtk_mnemonic(&self.text)) - .use_underline(true) - .sensitive(self.enabled) - .active(self.checked.as_ref().unwrap().load(Ordering::Relaxed)) - .build(); - - self.accel_group = accel_group.cloned(); - - register_accel!(self, item, accel_group); - - let id = self.id.clone(); - let is_syncing_checked_state = self.is_syncing_checked_state.clone().unwrap(); - let checked = self.checked.clone().unwrap(); - let store = self.gtk_menu_items.clone(); - item.connect_toggled(move |i| { - let should_dispatch = is_syncing_checked_state - .compare_exchange(false, true, Ordering::Release, Ordering::Relaxed) - .is_ok(); - - if should_dispatch { - let c = i.is_active(); - checked.store(c, Ordering::Release); - - for items in store.borrow().values() { - for i in items { - i.downcast_ref::() - .unwrap() - .set_active(c); - } - } - - is_syncing_checked_state.store(false, Ordering::Release); - - MenuEvent::send(crate::MenuEvent { id: id.clone() }); - } - }); - - let item = item.upcast::(); - - if add_to_cache { - self.gtk_menu_items - .borrow_mut() - .entry(menu_id) - .or_default() - .push(item.clone()); - } - - Ok(item) +impl MenuChild { + pub fn new_check( + text: &str, + enabled: bool, + checked: bool, + accelerator: Option, + id: Option, + ) -> Self { + Self {} } - fn create_gtk_item_for_icon_menu_item( - &mut self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - for_menu_bar: bool, - ) -> crate::Result { - let image = self - .icon - .as_ref() - .map(|i| gtk::Image::from_pixbuf(Some(&i.inner.to_pixbuf_scale(16, 16)))) - .unwrap_or_default(); - - self.accel_group = accel_group.cloned(); - - let label = gtk::AccelLabel::builder() - .label(to_gtk_mnemonic(&self.text)) - .use_underline(true) - .xalign(0.0) - .build(); - - let box_container = gtk::Box::new(Orientation::Horizontal, 6); - if !for_menu_bar { - let style_context = box_container.style_context(); - let css_provider = gtk::CssProvider::new(); - let theme = r#" - box { - margin-left: -22px; - } - "#; - let _ = css_provider.load_from_data(theme.as_bytes()); - style_context.add_provider(&css_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION); - } - box_container.pack_start(&image, false, false, 0); - box_container.pack_start(&label, true, true, 0); - box_container.show_all(); - - let item = gtk::MenuItem::builder() - .child(&box_container) - .sensitive(self.enabled) - .build(); - - register_accel!(self, item, accel_group); - - let id = self.id.clone(); - item.connect_activate(move |_| { - MenuEvent::send(crate::MenuEvent { id: id.clone() }); - }); - - if add_to_cache { - self.gtk_menu_items - .borrow_mut() - .entry(menu_id) - .or_default() - .push(item.clone()); - } - - Ok(item) + pub fn is_checked(&self) -> bool { + todo!() } -} -impl MenuItemKind { - fn make_gtk_menu_item( - &self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - for_menu_bar: bool, - ) -> crate::Result { - let mut child = self.child_mut(); - match child.item_type() { - MenuItemType::Submenu => { - child.create_gtk_item_for_submenu(menu_id, accel_group, add_to_cache) - } - MenuItemType::MenuItem => { - child.create_gtk_item_for_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Predefined => { - child.create_gtk_item_for_predefined_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Check => { - child.create_gtk_item_for_check_menu_item(menu_id, accel_group, add_to_cache) - } - MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item( - menu_id, - accel_group, - add_to_cache, - for_menu_bar, - ), - } + pub fn set_checked(&self, checked: bool) { + todo!() } } -impl dyn IsMenuItem + '_ { - fn make_gtk_menu_item( - &self, - menu_id: u32, - accel_group: Option<>k::AccelGroup>, - add_to_cache: bool, - for_menu_bar: bool, - ) -> crate::Result { - self.kind() - .make_gtk_menu_item(menu_id, accel_group, add_to_cache, for_menu_bar) +impl MenuChild { + pub fn new_icon( + text: &str, + enabled: bool, + icon: Option, + accelerator: Option, + id: Option, + ) -> Self { + Self {} } -} -fn show_context_menu( - gtk_menu: gtk::Menu, - widget: &impl IsA, - position: Option, -) -> bool { - let (pos, window) = if let Some(pos) = position { - let window = widget.window(); - ( - pos.to_logical::(window.as_ref().map(|w| w.scale_factor()).unwrap_or(1) as _) - .into(), - window, - ) - } else { - let window = widget.screen().and_then(|s| s.root_window()); - ( - window - .as_ref() - .and_then(|w| { - w.display() - .default_seat() - .and_then(|s| s.pointer()) - .map(|s| { - let p = s.position(); - (p.1, p.2) - }) - }) - .unwrap_or_default(), - window, - ) - }; - - if let Some(window) = window { - let mut event = gdk::Event::new(gdk::EventType::ButtonPress); - event.set_device( - window - .display() - .default_seat() - .and_then(|d| d.pointer()) - .as_ref(), - ); - - // Set the time of the event otherwise GTK will close the menu - // when right click is released - let event_ffi: *mut gdk::ffi::GdkEvent = event.to_glib_none().0; - if !event_ffi.is_null() { - let time = glib::monotonic_time() / 1000; - unsafe { - (*event_ffi).button.time = time as _; - } - } - - let (tx, rx) = crossbeam_channel::unbounded(); - let tx_clone = tx.clone(); - let id = gtk_menu.connect_cancel(move |_| tx_clone.send(false).unwrap_or(())); - let id2 = gtk_menu.connect_selection_done(move |_| tx.send(true).unwrap_or(())); - - gtk_menu.popup_at_rect( - &window, - &gdk::Rectangle::new(pos.0, pos.1, 0, 0), - gdk::Gravity::NorthWest, - gdk::Gravity::NorthWest, - Some(&event), - ); - - loop { - gtk::main_iteration(); - - match rx.try_recv() { - Ok(result) => { - gtk_menu.disconnect(id); - gtk_menu.disconnect(id2); - return result; - } - Err(err) => { - if err.is_disconnected() { - gtk_menu.disconnect(id); - gtk_menu.disconnect(id2); - return false; - } - } - } - } + pub fn new_native_icon( + text: &str, + enabled: bool, + icon: Option, + accelerator: Option, + id: Option, + ) -> Self { + Self {} } - false -} - -impl PredefinedMenuItemType { - #[cfg(feature = "libxdo")] - fn xdo_keys(&self) -> &str { - match self { - PredefinedMenuItemType::Copy => "ctrl+c", - PredefinedMenuItemType::Cut => "ctrl+X", - PredefinedMenuItemType::Paste => "ctrl+v", - PredefinedMenuItemType::SelectAll => "ctrl+a", - _ => unreachable!(), - } - } + pub fn set_icon(&self, icon: Option) {} } From b5ae7513ae168d54865c3c2e4c1b5c948489d622 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Sun, 2 Feb 2025 01:29:14 +0200 Subject: [PATCH 02/21] example --- examples/gtk.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/gtk.rs b/examples/gtk.rs index 0b807d7d..64b07ea9 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -36,22 +36,22 @@ fn on_activate(application: >k4::Application) { window.present(); let menubar = { - // let file_menu = { - // let about_menu_item = muda::MenuItem::new("About", true, None); - // let quit_menu_item = muda::MenuItem::new( - // "About", - // true, - // Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), - // ); + let file_menu = { + let about_menu_item = muda::MenuItem::new("About", true, None); + let quit_menu_item = muda::MenuItem::new( + "About", + true, + Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + ); - // let file_menu = muda::Submenu::new("File", true); - // file_menu.append(&about_menu_item).unwrap(); - // file_menu.append(&quit_menu_item).unwrap(); - // file_menu - // }; + let file_menu = muda::Submenu::new("File", true); + file_menu.append(&about_menu_item).unwrap(); + file_menu.append(&quit_menu_item).unwrap(); + file_menu + }; let menubar = muda::Menu::new(); - // menubar.append(&file_menu).unwrap(); + menubar.append(&file_menu).unwrap(); menubar }; From 9c2f2b2ee8c41b06d6c1a28a7341fc57f660614b Mon Sep 17 00:00:00 2001 From: amrbashir Date: Mon, 3 Feb 2025 04:37:35 +0200 Subject: [PATCH 03/21] basic submenu and menu working --- examples/gtk.rs | 12 +- src/menu_id.rs | 7 +- src/platform_impl/gtk/mod.rs | 364 +++++++++++++++++++++++++++++------ 3 files changed, 313 insertions(+), 70 deletions(-) diff --git a/examples/gtk.rs b/examples/gtk.rs index 64b07ea9..1a6dfe9d 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -38,8 +38,9 @@ fn on_activate(application: >k4::Application) { let menubar = { let file_menu = { let about_menu_item = muda::MenuItem::new("About", true, None); - let quit_menu_item = muda::MenuItem::new( - "About", + let quit_menu_item = muda::MenuItem::with_id( + "quit", + "Quit", true, Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), ); @@ -56,9 +57,10 @@ fn on_activate(application: >k4::Application) { menubar }; - menubar - .init_for_gtk_window(&window, None::<>k4::Window>) - .unwrap(); + let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + menubar.init_for_gtk_window(&window, Some(&vbox)).unwrap(); + + window.set_child(Some(&vbox)); } #[cfg(not(target_os = "linux"))] diff --git a/src/menu_id.rs b/src/menu_id.rs index 5702eeaf..6c98f5c0 100644 --- a/src/menu_id.rs +++ b/src/menu_id.rs @@ -10,11 +10,6 @@ impl MenuId { pub fn new>(id: S) -> Self { Self(id.as_ref().to_string()) } - - /// Create a new menu id. - pub fn new_owned(id: String) -> Self { - Self(id) - } } impl AsRef for MenuId { @@ -25,7 +20,7 @@ impl AsRef for MenuId { impl From for MenuId { fn from(value: T) -> Self { - Self::new(value.to_string()) + Self(value.to_string()) } } diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index b9388661..8d64a623 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -5,16 +5,21 @@ mod accelerator; mod icon; -use std::collections::{hash_map::Entry, HashMap}; +use std::{ + cell::RefCell, + collections::{hash_map::Entry, HashMap}, + rc::Rc, +}; use dpi::Position; -use gtk4::{gio::SimpleActionGroup, prelude::*}; +use gtk4::{gio::SimpleActionGroup, glib::VariantTy, prelude::*}; pub(crate) use icon::PlatformIcon; use crate::{ accelerator::Accelerator, util::{AddOp, Counter}, - Icon, IsMenuItem, MenuId, MenuItemKind, MenuItemType, NativeIcon, PredefinedMenuItemType, + Icon, IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType, NativeIcon, + PredefinedMenuItemType, }; static COUNTER: Counter = Counter::new(); @@ -30,20 +35,26 @@ impl GtkMenuBar { fn widget(&self) -> >k4::PopoverMenuBar { &self.0 } + + fn menu(&self) -> >k4::gio::Menu { + &self.1 + } } pub struct Menu { id: MenuId, - gtk_menubars: HashMap, + instances: HashMap, action_group: Option, + children: Vec>>, } impl Menu { pub fn new(id: Option) -> Self { Self { - id: id.unwrap_or_else(|| MenuId::new_owned(COUNTER.next().to_string())), - gtk_menubars: HashMap::new(), + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + instances: HashMap::new(), action_group: None, + children: Vec::new(), } } @@ -51,8 +62,30 @@ impl Menu { &self.id } - pub fn add_menu_item(&self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { - todo!() + pub fn add_menu_item(&mut self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { + match op { + AddOp::Append => self.children.push(item.child()), + AddOp::Insert(i) => self.children.insert(i, item.child()), + } + + for (menu_id, menu_bar) in &self.instances { + let gtk_item = item.make_gtk_menu_item(*menu_id, self.action_group.as_ref())?; + match op { + AddOp::Append => menu_bar.menu().append_item(>k_item), + AddOp::Insert(position) => menu_bar.menu().insert_item(position as i32, >k_item), + } + } + + Ok(()) + } + + 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_id, self.action_group.as_ref())?; + menu_bar.menu().append_item(>k_item); + } + + Ok(()) } pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { @@ -60,7 +93,10 @@ impl Menu { } pub fn items(&self) -> Vec { - todo!() + self.children + .iter() + .map(|c| c.borrow().kind(c.clone())) + .collect() } pub fn init_for_gtk_window( @@ -76,45 +112,68 @@ impl Menu { let id = window.as_ptr() as u32; if self.action_group.is_none() { - self.action_group = Some(gtk4::gio::SimpleActionGroup::new()); + let action_group = gtk4::gio::SimpleActionGroup::new(); + + let action = gtk4::gio::SimpleAction::new_stateful( + "sendEvent", + Some(&VariantTy::STRING), + &"".to_variant(), + ); + action_group.add_action(&action); + action.connect_activate(|_, v| { + if let Some(v) = v { + MenuEvent::send(MenuEvent { + id: MenuId(v.as_ref().to_string()), + }); + } + }); + + let action = + gtk4::gio::SimpleAction::new_stateful("sendCheckEvent", None, &"".to_variant()); + action_group.add_action(&action); + action.connect_activate(|_, _| { + MenuEvent::send(MenuEvent { + id: MenuId("0".to_string()), + }); + }); + + self.action_group = Some(action_group); } // This is the first time this method has been called on this window - // so we need to create the menubar and its parent box - if let Entry::Vacant(e) = self.gtk_menubars.entry(id) { + // so we need to create the menubar + if let Entry::Vacant(e) = self.instances.entry(id) { e.insert(GtkMenuBar::new()); } else { return Err(crate::Error::AlreadyInitialized); } - // Construct the entries of the menubar - let menu_bar = &self.gtk_menubars[&id]; + window.insert_action_group("muda", self.action_group.as_ref()); - window.insert_action_group(self.id().as_ref(), self.action_group.as_ref()); + for item in self.items() { + self.add_menu_item_with_id(item.as_ref(), id)?; + } - // TODO: - // for item in self.items() { - // self.add_menu_item_with_id(item.as_ref(), id)?; - // } + let menu_bar = self.instances[&id].widget(); // add the menubar to the specified widget, otherwise to the window if let Some(container) = container { if container.type_().name() == "GtkBox" { let gtk_box = container.dynamic_cast_ref::().unwrap(); - gtk_box.prepend(menu_bar.widget()); + gtk_box.prepend(menu_bar); } else if container.type_().name() == "GtkFixed" { let gtk_box = container.dynamic_cast_ref::().unwrap(); - gtk_box.put(menu_bar.widget(), 0., 0.); + gtk_box.put(menu_bar, 0., 0.); } else if container.type_().name() == "GtkStack" { let gtk_box = container.dynamic_cast_ref::().unwrap(); - gtk_box.add_child(menu_bar.widget()); + gtk_box.add_child(menu_bar); } } else { - window.set_child(Some(menu_bar.widget())); + window.set_child(Some(menu_bar)); } // show the menu bar - menu_bar.widget().set_visible(true); + menu_bar.set_visible(true); Ok(()) } @@ -164,7 +223,138 @@ impl Menu { } } -pub struct MenuChild {} +#[derive(Clone)] +enum GtkChild { + Item(gtk4::gio::MenuItem), + Submenu { + id: u32, + item: gtk4::gio::MenuItem, + menu: gtk4::gio::Menu, + }, +} + +impl GtkChild { + fn id(&self) -> u32 { + match self { + GtkChild::Item(_) => { + unreachable!("This is a bug report to https://github.com/tauri-apps/muda") + } + GtkChild::Submenu { id, .. } => *id, + } + } + + fn menu(&self) -> >k4::gio::Menu { + match self { + GtkChild::Item(_) => { + unreachable!("This is a bug report to https://github.com/tauri-apps/muda") + } + GtkChild::Submenu { menu, .. } => menu, + } + } +} + +pub struct MenuChild { + id: MenuId, + text: String, + enabled: bool, + accelerator: Option, + + type_: MenuItemType, + + instances: HashMap>, + children: Vec>>, +} + +impl MenuChild { + pub fn new_submenu(text: &str, enabled: bool, id: Option) -> Self { + Self { + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + text: text.to_string(), + enabled, + accelerator: None, + type_: MenuItemType::Submenu, + instances: HashMap::new(), + children: Vec::new(), + } + } + + fn create_gtk_item_for_submenu( + &mut self, + menu_id: u32, + action_group: Option<>k4::gio::SimpleActionGroup>, + ) -> crate::Result { + let menu = gtk4::gio::Menu::new(); + let item = gtk4::gio::MenuItem::new_submenu(Some(&self.text), &menu); + + let id = COUNTER.next(); + + let child = GtkChild::Submenu { + item: item.clone(), + menu, + id, + }; + + self.instances.entry(menu_id).or_default().push(child); + + for item in self.items() { + self.add_menu_item_with_id(item.as_ref(), id)?; + } + + Ok(item) + } + + pub fn add_menu_item(&mut self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { + match op { + AddOp::Append => self.children.push(item.child()), + AddOp::Insert(i) => self.children.insert(i, item.child()), + } + + for menus in self.instances.values() { + for gtk_child in menus { + let gtk_item = item.make_gtk_menu_item(gtk_child.id(), None /* TODO */)?; + + match op { + AddOp::Append => gtk_child.menu().append_item(>k_item), + AddOp::Insert(position) => { + gtk_child.menu().insert_item(position as i32, >k_item) + } + } + } + } + + Ok(()) + } + + 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.id(), None /* TODO */)?; + gtk_child.menu().append_item(>k_item); + } + } + + Ok(()) + } + + pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { + todo!() + } + + pub fn items(&self) -> Vec { + self.children + .iter() + .map(|c| c.borrow().kind(c.clone())) + .collect() + } + + pub fn show_context_menu_for_gtk_window( + &self, + w: >k4::Window, + position: Option, + ) -> bool { + todo!() + } +} impl MenuChild { pub fn new( @@ -173,15 +363,39 @@ impl MenuChild { accelerator: Option, id: Option, ) -> Self { - Self {} + Self { + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + text: text.to_string(), + enabled, + accelerator, + type_: MenuItemType::MenuItem, + instances: HashMap::new(), + children: Vec::new(), + } + } + + fn create_gtk_item_for_menu_item( + &mut self, + menu_id: u32, + action_group: Option<>k4::gio::SimpleActionGroup>, + ) -> crate::Result { + let item = gtk4::gio::MenuItem::new( + Some(&self.text), + Some(&format!("muda.sendEvent::{}", self.id.as_ref())), + ); + + let child = GtkChild::Item(item.clone()); + self.instances.entry(menu_id).or_default().push(child); + + Ok(item) } pub fn id(&self) -> &MenuId { - todo!() + &self.id } pub fn item_type(&self) -> &MenuItemType { - todo!() + &self.type_ } pub fn text(&self) -> String { @@ -206,34 +420,16 @@ impl MenuChild { } impl MenuChild { - pub fn new_submenu(text: &str, enabled: bool, id: Option) -> Self { - Self {} - } - - pub fn add_menu_item(&self, item: &dyn IsMenuItem, op: AddOp) -> crate::Result<()> { - todo!() - } - - pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - todo!() - } - - pub fn items(&self) -> Vec { - todo!() - } - - pub fn show_context_menu_for_gtk_window( - &self, - w: >k4::Window, - position: Option, - ) -> bool { - todo!() - } -} - -impl MenuChild { - pub fn new_predefined(item: PredefinedMenuItemType, text: Option) -> Self { - Self {} + pub fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + Self { + id: MenuId(COUNTER.next().to_string()), + text: text.unwrap_or_else(|| item_type.text().to_string()), + enabled: true, + accelerator: None, + type_: MenuItemType::Predefined, + instances: HashMap::new(), + children: Vec::new(), + } } } @@ -245,7 +441,15 @@ impl MenuChild { accelerator: Option, id: Option, ) -> Self { - Self {} + Self { + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + text: text.to_string(), + enabled, + accelerator, + type_: MenuItemType::Check, + instances: HashMap::new(), + children: Vec::new(), + } } pub fn is_checked(&self) -> bool { @@ -265,7 +469,15 @@ impl MenuChild { accelerator: Option, id: Option, ) -> Self { - Self {} + Self { + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + text: text.to_string(), + enabled, + accelerator, + type_: MenuItemType::Icon, + instances: HashMap::new(), + children: Vec::new(), + } } pub fn new_native_icon( @@ -275,8 +487,42 @@ impl MenuChild { accelerator: Option, id: Option, ) -> Self { - Self {} + Self { + id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), + text: text.to_string(), + enabled, + accelerator, + type_: MenuItemType::Submenu, + instances: HashMap::new(), + children: Vec::new(), + } } pub fn set_icon(&self, icon: Option) {} } + +impl dyn IsMenuItem + '_ { + fn make_gtk_menu_item( + &self, + menu_id: u32, + action_group: Option<>k4::gio::SimpleActionGroup>, + ) -> crate::Result { + let kind = self.kind(); + let mut child = kind.child_mut(); + match child.item_type() { + MenuItemType::Submenu => child.create_gtk_item_for_submenu(menu_id, action_group), + MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(menu_id, action_group), + _ => todo!(), + // MenuItemType::Predefined => { + // child.create_gtk_item_for_predefined_menu_item(menu_id, action_group, add_to_store) + // } + // MenuItemType::Check => { + // child.create_gtk_item_for_check_menu_item(menu_id, action_group, add_to_store) + // } + // MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item( + // menu_id, + // action_group, + // ), + } + } +} From 1f1ddafc7612d7162944c8bb64f54595b4338dc2 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Mon, 3 Feb 2025 04:41:22 +0200 Subject: [PATCH 04/21] fix items showing as radio buttons --- src/platform_impl/gtk/mod.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 8d64a623..1436ba6f 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -114,11 +114,7 @@ impl Menu { if self.action_group.is_none() { let action_group = gtk4::gio::SimpleActionGroup::new(); - let action = gtk4::gio::SimpleAction::new_stateful( - "sendEvent", - Some(&VariantTy::STRING), - &"".to_variant(), - ); + let action = gtk4::gio::SimpleAction::new("sendEvent", Some(&VariantTy::STRING)); action_group.add_action(&action); action.connect_activate(|_, v| { if let Some(v) = v { From 0e08f4e6e79efdbb2096b8f306c7521d75a8cb02 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Mon, 3 Feb 2025 22:51:51 +0200 Subject: [PATCH 05/21] use a separate action for check menu items --- examples/gtk.rs | 11 +++- src/platform_impl/gtk/mod.rs | 111 +++++++++++++++++++++++------------ 2 files changed, 83 insertions(+), 39 deletions(-) diff --git a/examples/gtk.rs b/examples/gtk.rs index 1a6dfe9d..a72523c1 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -38,15 +38,22 @@ fn on_activate(application: >k4::Application) { let menubar = { let file_menu = { let about_menu_item = muda::MenuItem::new("About", true, None); + let check = muda::CheckMenuItem::new( + "Check", + true, + true, + Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + ); let quit_menu_item = muda::MenuItem::with_id( "quit", - "Quit", + "&Quit", true, Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), ); - let file_menu = muda::Submenu::new("File", true); + let file_menu = muda::Submenu::new("&File", true); file_menu.append(&about_menu_item).unwrap(); + file_menu.append(&check).unwrap(); file_menu.append(&quit_menu_item).unwrap(); file_menu }; diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 1436ba6f..b00c3dde 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -11,8 +11,9 @@ use std::{ rc::Rc, }; +use accelerator::to_gtk_mnemonic; use dpi::Position; -use gtk4::{gio::SimpleActionGroup, glib::VariantTy, prelude::*}; +use gtk4::{gio, glib::VariantTy, prelude::*}; pub(crate) use icon::PlatformIcon; use crate::{ @@ -24,11 +25,15 @@ use crate::{ static COUNTER: Counter = Counter::new(); -struct GtkMenuBar(gtk4::PopoverMenuBar, gtk4::gio::Menu); +const DEFAULT_ACTION: &str = "_internal_sendEvent"; +const DEFAULT_ACTION_GROUP: &str = "muda"; +const DEFAULT_DETAILED_ACTION: &str = "muda._internal_sendEvent"; + +struct GtkMenuBar(gtk4::PopoverMenuBar, gio::Menu); impl GtkMenuBar { fn new() -> Self { - let menu = gtk4::gio::Menu::new(); + let menu = gio::Menu::new(); Self(gtk4::PopoverMenuBar::from_model(Some(&menu)), menu) } @@ -36,7 +41,7 @@ impl GtkMenuBar { &self.0 } - fn menu(&self) -> >k4::gio::Menu { + fn menu(&self) -> &gio::Menu { &self.1 } } @@ -44,7 +49,7 @@ impl GtkMenuBar { pub struct Menu { id: MenuId, instances: HashMap, - action_group: Option, + action_group: Option, children: Vec>>, } @@ -114,8 +119,7 @@ impl Menu { if self.action_group.is_none() { let action_group = gtk4::gio::SimpleActionGroup::new(); - let action = gtk4::gio::SimpleAction::new("sendEvent", Some(&VariantTy::STRING)); - action_group.add_action(&action); + let action = gtk4::gio::SimpleAction::new(DEFAULT_ACTION, Some(&VariantTy::STRING)); action.connect_activate(|_, v| { if let Some(v) = v { MenuEvent::send(MenuEvent { @@ -123,15 +127,7 @@ impl Menu { }); } }); - - let action = - gtk4::gio::SimpleAction::new_stateful("sendCheckEvent", None, &"".to_variant()); action_group.add_action(&action); - action.connect_activate(|_, _| { - MenuEvent::send(MenuEvent { - id: MenuId("0".to_string()), - }); - }); self.action_group = Some(action_group); } @@ -144,7 +140,7 @@ impl Menu { return Err(crate::Error::AlreadyInitialized); } - window.insert_action_group("muda", self.action_group.as_ref()); + window.insert_action_group(DEFAULT_ACTION_GROUP, self.action_group.as_ref()); for item in self.items() { self.add_menu_item_with_id(item.as_ref(), id)?; @@ -221,11 +217,12 @@ impl Menu { #[derive(Clone)] enum GtkChild { - Item(gtk4::gio::MenuItem), + Item(gio::MenuItem), Submenu { id: u32, - item: gtk4::gio::MenuItem, - menu: gtk4::gio::Menu, + item: gio::MenuItem, + menu: gio::Menu, + action_group: Option, }, } @@ -239,7 +236,7 @@ impl GtkChild { } } - fn menu(&self) -> >k4::gio::Menu { + fn menu(&self) -> &gio::Menu { match self { GtkChild::Item(_) => { unreachable!("This is a bug report to https://github.com/tauri-apps/muda") @@ -247,6 +244,13 @@ impl GtkChild { GtkChild::Submenu { menu, .. } => menu, } } + + fn action_group(&self) -> Option<&gio::SimpleActionGroup> { + match self { + GtkChild::Item(_) => None, + GtkChild::Submenu { action_group, .. } => action_group.as_ref(), + } + } } pub struct MenuChild { @@ -255,6 +259,8 @@ pub struct MenuChild { enabled: bool, accelerator: Option, + checked: bool, + type_: MenuItemType, instances: HashMap>, @@ -267,6 +273,7 @@ impl MenuChild { id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), text: text.to_string(), enabled, + checked: false, accelerator: None, type_: MenuItemType::Submenu, instances: HashMap::new(), @@ -277,10 +284,10 @@ impl MenuChild { fn create_gtk_item_for_submenu( &mut self, menu_id: u32, - action_group: Option<>k4::gio::SimpleActionGroup>, - ) -> crate::Result { - let menu = gtk4::gio::Menu::new(); - let item = gtk4::gio::MenuItem::new_submenu(Some(&self.text), &menu); + action_group: Option<&gio::SimpleActionGroup>, + ) -> crate::Result { + let menu = gio::Menu::new(); + let item = gio::MenuItem::new_submenu(Some(&to_gtk_mnemonic(&self.text)), &menu); let id = COUNTER.next(); @@ -288,6 +295,7 @@ impl MenuChild { item: item.clone(), menu, id, + action_group: action_group.cloned(), }; self.instances.entry(menu_id).or_default().push(child); @@ -307,7 +315,7 @@ impl MenuChild { for menus in self.instances.values() { for gtk_child in menus { - let gtk_item = item.make_gtk_menu_item(gtk_child.id(), None /* TODO */)?; + let gtk_item = item.make_gtk_menu_item(gtk_child.id(), gtk_child.action_group())?; match op { AddOp::Append => gtk_child.menu().append_item(>k_item), @@ -324,7 +332,7 @@ 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.id(), None /* TODO */)?; + let gtk_item = item.make_gtk_menu_item(gtk_child.id(), gtk_child.action_group())?; gtk_child.menu().append_item(>k_item); } } @@ -364,6 +372,7 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + checked: false, type_: MenuItemType::MenuItem, instances: HashMap::new(), children: Vec::new(), @@ -373,11 +382,11 @@ impl MenuChild { fn create_gtk_item_for_menu_item( &mut self, menu_id: u32, - action_group: Option<>k4::gio::SimpleActionGroup>, - ) -> crate::Result { - let item = gtk4::gio::MenuItem::new( - Some(&self.text), - Some(&format!("muda.sendEvent::{}", self.id.as_ref())), + _action_group: Option<&gio::SimpleActionGroup>, + ) -> crate::Result { + let item = gio::MenuItem::new( + Some(&to_gtk_mnemonic(&self.text)), + Some(&format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref())), ); let child = GtkChild::Item(item.clone()); @@ -422,6 +431,7 @@ impl MenuChild { text: text.unwrap_or_else(|| item_type.text().to_string()), enabled: true, accelerator: None, + checked: false, type_: MenuItemType::Predefined, instances: HashMap::new(), children: Vec::new(), @@ -442,12 +452,39 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + checked, type_: MenuItemType::Check, instances: HashMap::new(), children: Vec::new(), } } + fn create_gtk_item_for_check_menu_item( + &mut self, + menu_id: u32, + action_group: Option<&gio::SimpleActionGroup>, + ) -> crate::Result { + let item = gio::MenuItem::new( + Some(&to_gtk_mnemonic(&self.text)), + Some(&format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref())), + ); + + if let Some(action_group) = action_group { + let state = &self.checked.to_variant(); + let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); + let id = self.id.clone(); + action.connect_state_notify(move |_| { + MenuEvent::send(MenuEvent { id: id.clone() }); + }); + action_group.add_action(&action); + } + + let child = GtkChild::Item(item.clone()); + self.instances.entry(menu_id).or_default().push(child); + + Ok(item) + } + pub fn is_checked(&self) -> bool { todo!() } @@ -470,6 +507,7 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + checked: false, type_: MenuItemType::Icon, instances: HashMap::new(), children: Vec::new(), @@ -488,6 +526,7 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + checked: false, type_: MenuItemType::Submenu, instances: HashMap::new(), children: Vec::new(), @@ -501,19 +540,17 @@ impl dyn IsMenuItem + '_ { fn make_gtk_menu_item( &self, menu_id: u32, - action_group: Option<>k4::gio::SimpleActionGroup>, - ) -> crate::Result { + action_group: Option<&gio::SimpleActionGroup>, + ) -> crate::Result { let kind = self.kind(); let mut child = kind.child_mut(); match child.item_type() { MenuItemType::Submenu => child.create_gtk_item_for_submenu(menu_id, action_group), MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(menu_id, action_group), + MenuItemType::Check => child.create_gtk_item_for_check_menu_item(menu_id, action_group), _ => todo!(), // MenuItemType::Predefined => { - // child.create_gtk_item_for_predefined_menu_item(menu_id, action_group, add_to_store) - // } - // MenuItemType::Check => { - // child.create_gtk_item_for_check_menu_item(menu_id, action_group, add_to_store) + // child.create_gtk_item_for_predefined_menu_item(menu_id, action_group) // } // MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item( // menu_id, From b21c46cc8011c3d06f8acb83f433e648e0a24f90 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Sun, 9 Feb 2025 01:25:39 +0200 Subject: [PATCH 06/21] context menu and is_checked --- examples/gtk.rs | 32 ++- src/error.rs | 2 + src/platform_impl/gtk/mod.rs | 374 +++++++++++++++++++++++++++-------- 3 files changed, 324 insertions(+), 84 deletions(-) diff --git a/examples/gtk.rs b/examples/gtk.rs index a72523c1..6da1ba9c 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -25,6 +25,8 @@ fn on_startup(_: >k4::Application) { #[cfg(target_os = "linux")] fn on_activate(application: >k4::Application) { + use muda::ContextMenu; + let window = gtk4::ApplicationWindow::builder() .application(application) .title("Menubar Example") @@ -35,15 +37,21 @@ fn on_activate(application: >k4::Application) { window.present(); - let menubar = { + let (menubar, file_menu) = { let file_menu = { let about_menu_item = muda::MenuItem::new("About", true, None); + let check = muda::CheckMenuItem::new( "Check", true, true, Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), ); + + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); + let icon = load_icon(std::path::Path::new(path)); + let icon_menu_item = muda::IconMenuItem::new("Icon", true, Some(icon), None); + let quit_menu_item = muda::MenuItem::with_id( "quit", "&Quit", @@ -54,6 +62,7 @@ fn on_activate(application: >k4::Application) { let file_menu = muda::Submenu::new("&File", true); file_menu.append(&about_menu_item).unwrap(); file_menu.append(&check).unwrap(); + file_menu.append(&icon_menu_item).unwrap(); file_menu.append(&quit_menu_item).unwrap(); file_menu }; @@ -61,14 +70,33 @@ fn on_activate(application: >k4::Application) { let menubar = muda::Menu::new(); menubar.append(&file_menu).unwrap(); - menubar + (menubar, file_menu) }; let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); menubar.init_for_gtk_window(&window, Some(&vbox)).unwrap(); + let btn = gtk4::Button::with_label("ASdasd"); + let w = window.clone(); + btn.connect_clicked(move |_| { + file_menu.show_context_menu_for_gtk_window(w.dynamic_cast_ref().unwrap(), None); + }); + vbox.append(&btn); + window.set_child(Some(&vbox)); } #[cfg(not(target_os = "linux"))] fn main() {} + +fn load_icon(path: &std::path::Path) -> muda::Icon { + let (icon_rgba, icon_width, icon_height) = { + let image = image::open(path) + .expect("Failed to open icon path") + .into_rgba8(); + let (width, height) = image.dimensions(); + let rgba = image.into_raw(); + (rgba, width, height) + }; + muda::Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon") +} diff --git a/src/error.rs b/src/error.rs index 7ad6527c..1f2b2e7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,8 @@ pub enum Error { AlreadyInitialized, #[error(transparent)] AcceleratorParseError(#[from] AcceleratorParseError), + #[error("Gtk Window doesn't have an application")] + GtkWindowWithoutApplication, } /// Convenient type alias of Result type for muda. diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index b00c3dde..b739442a 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -13,7 +13,7 @@ use std::{ use accelerator::to_gtk_mnemonic; use dpi::Position; -use gtk4::{gio, glib::VariantTy, prelude::*}; +use gtk4::{gdk::Rectangle, gio, glib::VariantTy, prelude::*}; pub(crate) use icon::PlatformIcon; use crate::{ @@ -28,28 +28,68 @@ static COUNTER: Counter = Counter::new(); const DEFAULT_ACTION: &str = "_internal_sendEvent"; const DEFAULT_ACTION_GROUP: &str = "muda"; const DEFAULT_DETAILED_ACTION: &str = "muda._internal_sendEvent"; +const ACTION_GROUP_DATA_KEY: &str = "mudaActionGroup"; -struct GtkMenuBar(gtk4::PopoverMenuBar, gio::Menu); +enum GtkMenuBar { + MenuBar { + widget: gtk4::PopoverMenuBar, + menu: gio::Menu, + app: gtk4::Application, + }, + ContextMenu { + widget: gtk4::PopoverMenu, + menu: gio::Menu, + app: gtk4::Application, + }, +} impl GtkMenuBar { - fn new() -> Self { + fn new(app: gtk4::Application) -> Self { + let menu = gio::Menu::new(); + let widget = gtk4::PopoverMenuBar::from_model(Some(&menu)); + Self::MenuBar { widget, menu, app } + } + + fn new_context(app: gtk4::Application) -> Self { let menu = gio::Menu::new(); - Self(gtk4::PopoverMenuBar::from_model(Some(&menu)), menu) + let widget = gtk4::PopoverMenu::from_model(Some(&menu)); + Self::ContextMenu { widget, menu, app } + } + + fn applicaiton(&self) -> >k4::Application { + match self { + GtkMenuBar::MenuBar { app, .. } => app, + GtkMenuBar::ContextMenu { app, .. } => app, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } + } + + fn menu_bar(&self) -> >k4::PopoverMenuBar { + match self { + GtkMenuBar::MenuBar { widget, .. } => widget, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } } - fn widget(&self) -> >k4::PopoverMenuBar { - &self.0 + fn context_menu(&self) -> >k4::PopoverMenu { + match self { + GtkMenuBar::ContextMenu { widget, .. } => widget, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } } fn menu(&self) -> &gio::Menu { - &self.1 + match self { + GtkMenuBar::MenuBar { menu, .. } => menu, + GtkMenuBar::ContextMenu { menu, .. } => menu, + } } } pub struct Menu { id: MenuId, instances: HashMap, - action_group: Option, + ctx_menu_id: u32, children: Vec>>, } @@ -58,7 +98,7 @@ impl Menu { Self { id: id.unwrap_or_else(|| MenuId(COUNTER.next().to_string())), instances: HashMap::new(), - action_group: None, + ctx_menu_id: COUNTER.next(), children: Vec::new(), } } @@ -74,7 +114,7 @@ impl Menu { } for (menu_id, menu_bar) in &self.instances { - let gtk_item = item.make_gtk_menu_item(*menu_id, self.action_group.as_ref())?; + let gtk_item = item.make_gtk_menu_item(menu_bar.applicaiton(), *menu_id)?; match op { AddOp::Append => menu_bar.menu().append_item(>k_item), AddOp::Insert(position) => menu_bar.menu().insert_item(position as i32, >k_item), @@ -86,7 +126,7 @@ 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_id, self.action_group.as_ref())?; + let gtk_item = item.make_gtk_menu_item(menu_bar.applicaiton(), *menu_id)?; menu_bar.menu().append_item(>k_item); } @@ -116,37 +156,26 @@ impl Menu { { let id = window.as_ptr() as u32; - if self.action_group.is_none() { - let action_group = gtk4::gio::SimpleActionGroup::new(); - - let action = gtk4::gio::SimpleAction::new(DEFAULT_ACTION, Some(&VariantTy::STRING)); - action.connect_activate(|_, v| { - if let Some(v) = v { - MenuEvent::send(MenuEvent { - id: MenuId(v.as_ref().to_string()), - }); - } - }); - action_group.add_action(&action); - - self.action_group = Some(action_group); - } + let Some(app) = window.application() else { + return Err(crate::Error::GtkWindowWithoutApplication); + }; // This is the first time this method has been called on this window // so we need to create the menubar if let Entry::Vacant(e) = self.instances.entry(id) { - e.insert(GtkMenuBar::new()); + e.insert(GtkMenuBar::new(app.clone())); } else { return Err(crate::Error::AlreadyInitialized); } - window.insert_action_group(DEFAULT_ACTION_GROUP, self.action_group.as_ref()); + let action_group = action_group_from_app(&app); + window.insert_action_group(DEFAULT_ACTION_GROUP, Some(&action_group)); for item in self.items() { self.add_menu_item_with_id(item.as_ref(), id)?; } - let menu_bar = self.instances[&id].widget(); + let menu_bar = self.instances[&id].menu_bar(); // add the menubar to the specified widget, otherwise to the window if let Some(container) = container { @@ -207,48 +236,121 @@ impl Menu { } pub fn show_context_menu_for_gtk_window( - &self, + &mut self, window: >k4::Window, position: Option, ) -> bool { - todo!() + let Some(app) = window.application() else { + return false; // TODO: better error + }; + + if self.instances.get(&self.ctx_menu_id).is_none() { + let action_group = action_group_from_app(&app); + window.insert_action_group(DEFAULT_ACTION_GROUP, Some(&action_group)); + + let menu = GtkMenuBar::new_context(app); + + menu.context_menu().connect_closed(|m| { + m.unparent(); + }); + + self.instances.insert(self.ctx_menu_id, menu); + + for item in self.items() { + let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); + } + } + + let (x, y) = match position { + Some(p) => p.to_logical::(window.scale_factor() as _).into(), + None => WidgetExt::display(window) + .default_seat() + .and_then(|s| s.pointer()) + .map(|p| { + let (_, x, y) = p.surface_at_position(); + (x as _, y as _) + }) + .unwrap_or_default(), + }; + + // SAFETY: it is guaranteed to exist due to the check above + let menu = self.instances.get(&self.ctx_menu_id).unwrap(); + let context_menu = menu.context_menu(); + context_menu.set_parent(window); + context_menu.popup(); + + context_menu.set_pointing_to(Some(&Rectangle::new(x, y, 0, 0))); + + true } } #[derive(Clone)] -enum GtkChild { +enum GtkMenuChild { Item(gio::MenuItem), + CheckItem { + item: gio::MenuItem, + action: gio::SimpleAction, + }, Submenu { id: u32, item: gio::MenuItem, menu: gio::Menu, - action_group: Option, + app: gtk4::Application, + }, + ContextMenu { + id: u32, + widget: gtk4::PopoverMenu, + menu: gio::Menu, + app: gtk4::Application, }, } -impl GtkChild { +impl GtkMenuChild { fn id(&self) -> u32 { match self { - GtkChild::Item(_) => { - unreachable!("This is a bug report to https://github.com/tauri-apps/muda") - } - GtkChild::Submenu { id, .. } => *id, + GtkMenuChild::Submenu { id, .. } => *id, + GtkMenuChild::ContextMenu { id, .. } => *id, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } + } + + fn application(&self) -> >k4::Application { + match self { + GtkMenuChild::Submenu { app, .. } => app, + GtkMenuChild::ContextMenu { app, .. } => app, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } + } + + fn item(&self) -> &gio::MenuItem { + match self { + GtkMenuChild::Submenu { item, .. } => item, + GtkMenuChild::Item(item) => item, + GtkMenuChild::CheckItem { item, .. } => item, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + } + } + + fn action(&self) -> &gio::SimpleAction { + match self { + GtkMenuChild::CheckItem { action, .. } => action, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } fn menu(&self) -> &gio::Menu { match self { - GtkChild::Item(_) => { - unreachable!("This is a bug report to https://github.com/tauri-apps/muda") - } - GtkChild::Submenu { menu, .. } => menu, + GtkMenuChild::Submenu { menu, .. } => menu, + GtkMenuChild::ContextMenu { menu, .. } => menu, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } - fn action_group(&self) -> Option<&gio::SimpleActionGroup> { + fn context_menu(&self) -> >k4::PopoverMenu { match self { - GtkChild::Item(_) => None, - GtkChild::Submenu { action_group, .. } => action_group.as_ref(), + GtkMenuChild::ContextMenu { widget, .. } => widget, + _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } } @@ -261,9 +363,12 @@ pub struct MenuChild { checked: bool, + icon: Option, + type_: MenuItemType, - instances: HashMap>, + instances: HashMap>, + ctx_menu_id: u32, children: Vec>>, } @@ -274,8 +379,10 @@ impl MenuChild { text: text.to_string(), enabled, checked: false, + icon: None, accelerator: None, type_: MenuItemType::Submenu, + ctx_menu_id: COUNTER.next(), instances: HashMap::new(), children: Vec::new(), } @@ -283,19 +390,19 @@ impl MenuChild { fn create_gtk_item_for_submenu( &mut self, + app: >k4::Application, menu_id: u32, - action_group: Option<&gio::SimpleActionGroup>, ) -> crate::Result { let menu = gio::Menu::new(); let item = gio::MenuItem::new_submenu(Some(&to_gtk_mnemonic(&self.text)), &menu); let id = COUNTER.next(); - let child = GtkChild::Submenu { + let child = GtkMenuChild::Submenu { item: item.clone(), menu, id, - action_group: action_group.cloned(), + app: app.clone(), }; self.instances.entry(menu_id).or_default().push(child); @@ -315,7 +422,7 @@ impl MenuChild { for menus in self.instances.values() { for gtk_child in menus { - let gtk_item = item.make_gtk_menu_item(gtk_child.id(), gtk_child.action_group())?; + let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id())?; match op { AddOp::Append => gtk_child.menu().append_item(>k_item), @@ -332,7 +439,7 @@ 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.id(), gtk_child.action_group())?; + let gtk_item = item.make_gtk_menu_item(gtk_child.application(), gtk_child.id())?; gtk_child.menu().append_item(>k_item); } } @@ -352,11 +459,61 @@ impl MenuChild { } pub fn show_context_menu_for_gtk_window( - &self, - w: >k4::Window, + &mut self, + window: >k4::Window, position: Option, ) -> bool { - todo!() + let Some(app) = window.application() else { + return false; // TODO: better error + }; + + if self.instances.get(&self.ctx_menu_id).is_none() { + let menu = gio::Menu::new(); + let widget = gtk4::PopoverMenu::from_model(Some(&menu)); + + let action_group = action_group_from_app(&app); + window.insert_action_group(DEFAULT_ACTION_GROUP, Some(&action_group)); + + let menu = GtkMenuChild::ContextMenu { + id: self.ctx_menu_id, + widget, + menu, + app, + }; + + menu.context_menu().connect_closed(|m| { + m.unparent(); + }); + + self.instances.insert(self.ctx_menu_id, vec![menu]); + + for item in self.items() { + let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); + } + } + + // SAFETY: it is guaranteed to exist due to the check above + let menus = self.instances.get(&self.ctx_menu_id).unwrap(); + let menu = menus.first().unwrap(); + + let (x, y) = match position { + Some(p) => p.to_logical::(window.scale_factor() as _).into(), + None => WidgetExt::display(window) + .default_seat() + .and_then(|s| s.pointer()) + .map(|p| { + let (_, x, y) = p.surface_at_position(); + (x as _, y as _) + }) + .unwrap_or_default(), + }; + + let context_menu = menu.context_menu(); + context_menu.set_parent(window); + context_menu.popup(); + context_menu.set_pointing_to(Some(&Rectangle::new(x, y, 0, 0))); + + true } } @@ -372,24 +529,22 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + icon: None, checked: false, type_: MenuItemType::MenuItem, + ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), } } - fn create_gtk_item_for_menu_item( - &mut self, - menu_id: u32, - _action_group: Option<&gio::SimpleActionGroup>, - ) -> crate::Result { + fn create_gtk_item_for_menu_item(&mut self, menu_id: u32) -> crate::Result { let item = gio::MenuItem::new( Some(&to_gtk_mnemonic(&self.text)), Some(&format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref())), ); - let child = GtkChild::Item(item.clone()); + let child = GtkMenuChild::Item(item.clone()); self.instances.entry(menu_id).or_default().push(child); Ok(item) @@ -404,7 +559,7 @@ impl MenuChild { } pub fn text(&self) -> String { - todo!() + self.text.clone() } pub fn set_text(&self, text: &str) { @@ -412,7 +567,7 @@ impl MenuChild { } pub fn is_enabled(&self) -> bool { - todo!() + self.enabled } pub fn set_enabled(&self, enabled: bool) { @@ -431,8 +586,10 @@ impl MenuChild { text: text.unwrap_or_else(|| item_type.text().to_string()), enabled: true, accelerator: None, + icon: None, checked: false, type_: MenuItemType::Predefined, + ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), } @@ -452,8 +609,10 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + icon: None, checked, type_: MenuItemType::Check, + ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), } @@ -461,32 +620,40 @@ impl MenuChild { fn create_gtk_item_for_check_menu_item( &mut self, + app: >k4::Application, menu_id: u32, - action_group: Option<&gio::SimpleActionGroup>, ) -> crate::Result { let item = gio::MenuItem::new( Some(&to_gtk_mnemonic(&self.text)), Some(&format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref())), ); - if let Some(action_group) = action_group { - let state = &self.checked.to_variant(); - let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); - let id = self.id.clone(); - action.connect_state_notify(move |_| { - MenuEvent::send(MenuEvent { id: id.clone() }); - }); - action_group.add_action(&action); - } + let action_group = action_group_from_app(&app); + + let state = &self.checked.to_variant(); + let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); + let id = self.id.clone(); + action.connect_state_notify(move |_| { + MenuEvent::send(MenuEvent { id: id.clone() }); + }); + action_group.add_action(&action); - let child = GtkChild::Item(item.clone()); + let child = GtkMenuChild::CheckItem { + item: item.clone(), + action, + }; self.instances.entry(menu_id).or_default().push(child); Ok(item) } pub fn is_checked(&self) -> bool { - todo!() + self.instances + .values() + .find_map(|i| i.first()) + .and_then(|i| i.action().state()) + .and_then(|s| s.get()) + .unwrap_or(self.checked) } pub fn set_checked(&self, checked: bool) { @@ -507,8 +674,10 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + icon, checked: false, type_: MenuItemType::Icon, + ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), } @@ -526,36 +695,77 @@ impl MenuChild { text: text.to_string(), enabled, accelerator, + icon: None, checked: false, type_: MenuItemType::Submenu, + ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), } } + fn create_gtk_item_for_icon_menu_item(&mut self, menu_id: u32) -> crate::Result { + let item = gio::MenuItem::new( + Some(&to_gtk_mnemonic(&self.text)), + Some(&format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref())), + ); + + if let Some(icon) = &self.icon { + item.set_icon(icon.inner.bytes_icon()); + } + + let child = GtkMenuChild::Item(item.clone()); + self.instances.entry(menu_id).or_default().push(child); + + Ok(item) + } + pub fn set_icon(&self, icon: Option) {} } impl dyn IsMenuItem + '_ { fn make_gtk_menu_item( &self, + app: >k4::Application, menu_id: u32, - action_group: Option<&gio::SimpleActionGroup>, ) -> crate::Result { let kind = self.kind(); let mut child = kind.child_mut(); match child.item_type() { - MenuItemType::Submenu => child.create_gtk_item_for_submenu(menu_id, action_group), - MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(menu_id, action_group), - MenuItemType::Check => child.create_gtk_item_for_check_menu_item(menu_id, action_group), + MenuItemType::Submenu => child.create_gtk_item_for_submenu(app, menu_id), + MenuItemType::MenuItem => child.create_gtk_item_for_menu_item(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(menu_id), _ => todo!(), // MenuItemType::Predefined => { // child.create_gtk_item_for_predefined_menu_item(menu_id, action_group) // } - // MenuItemType::Icon => child.create_gtk_item_for_icon_menu_item( - // menu_id, - // action_group, - // ), } } } + +/// Returns and creates the action group on this applicaiton if necessary. +fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { + let action_group = unsafe { app.data::(ACTION_GROUP_DATA_KEY) }; + + let action_group = if let Some(action_group) = action_group { + unsafe { action_group.as_ref() }.clone() + } else { + let action_group = gio::SimpleActionGroup::new(); + + let action = gtk4::gio::SimpleAction::new(DEFAULT_ACTION, Some(&VariantTy::STRING)); + action.connect_activate(|_, v| { + if let Some(v) = v { + MenuEvent::send(MenuEvent { + id: MenuId(v.as_ref().to_string()), + }); + } + }); + action_group.add_action(&action); + + unsafe { app.set_data(ACTION_GROUP_DATA_KEY, action_group.clone()) }; + action_group + }; + + action_group +} From 32f25a61e55dfe11e2ead9e87366c55c2d24a7b1 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 12 Mar 2025 03:38:27 +0200 Subject: [PATCH 07/21] fix context menu items not firing off --- src/platform_impl/gtk/mod.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index b739442a..d78ad659 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -481,10 +481,6 @@ impl MenuChild { app, }; - menu.context_menu().connect_closed(|m| { - m.unparent(); - }); - self.instances.insert(self.ctx_menu_id, vec![menu]); for item in self.items() { @@ -509,7 +505,12 @@ impl MenuChild { }; let context_menu = menu.context_menu(); + + if context_menu.parent().is_some() { + context_menu.unparent(); + } context_menu.set_parent(window); + context_menu.popup(); context_menu.set_pointing_to(Some(&Rectangle::new(x, y, 0, 0))); From 1e50683da7504e41cb7a6b5bcdb60156835df935 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Wed, 12 Mar 2025 09:35:50 +0200 Subject: [PATCH 08/21] fix context menu for Menu type as well --- src/items/normal.rs | 21 ++++++++++++------ src/platform_impl/gtk/mod.rs | 41 +++++++++++++++++------------------- 2 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/items/normal.rs b/src/items/normal.rs index 7935e7f6..8e1499d5 100644 --- a/src/items/normal.rs +++ b/src/items/normal.rs @@ -33,7 +33,12 @@ 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(text.as_ref(), enabled, accelerator, None); + let item = crate::platform_impl::MenuChild::new_menu_item( + text.as_ref(), + enabled, + accelerator, + None, + ); Self { id: Rc::new(item.id().clone()), inner: Rc::new(RefCell::new(item)), @@ -53,12 +58,14 @@ impl MenuItem { let id = id.into(); Self { id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new( - text.as_ref(), - enabled, - accelerator, - Some(id), - ))), + inner: Rc::new(RefCell::new( + crate::platform_impl::MenuChild::new_menu_item( + text.as_ref(), + enabled, + accelerator, + Some(id), + ), + )), } } diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index d78ad659..18d6298d 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -250,10 +250,6 @@ impl Menu { let menu = GtkMenuBar::new_context(app); - menu.context_menu().connect_closed(|m| { - m.unparent(); - }); - self.instances.insert(self.ctx_menu_id, menu); for item in self.items() { @@ -263,22 +259,19 @@ impl Menu { let (x, y) = match position { Some(p) => p.to_logical::(window.scale_factor() as _).into(), - None => WidgetExt::display(window) - .default_seat() - .and_then(|s| s.pointer()) - .map(|p| { - let (_, x, y) = p.surface_at_position(); - (x as _, y as _) - }) - .unwrap_or_default(), + None => get_cursor_pos(window), }; // SAFETY: it is guaranteed to exist due to the check above let menu = self.instances.get(&self.ctx_menu_id).unwrap(); let context_menu = menu.context_menu(); + + if context_menu.parent().is_some() { + context_menu.unparent(); + } context_menu.set_parent(window); - context_menu.popup(); + context_menu.popup(); context_menu.set_pointing_to(Some(&Rectangle::new(x, y, 0, 0))); true @@ -494,14 +487,7 @@ impl MenuChild { let (x, y) = match position { Some(p) => p.to_logical::(window.scale_factor() as _).into(), - None => WidgetExt::display(window) - .default_seat() - .and_then(|s| s.pointer()) - .map(|p| { - let (_, x, y) = p.surface_at_position(); - (x as _, y as _) - }) - .unwrap_or_default(), + None => get_cursor_pos(window), }; let context_menu = menu.context_menu(); @@ -519,7 +505,7 @@ impl MenuChild { } impl MenuChild { - pub fn new( + pub fn new_menu_item( text: &str, enabled: bool, accelerator: Option, @@ -770,3 +756,14 @@ fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { action_group } + +fn get_cursor_pos(window: >k4::Window) -> (i32, i32) { + WidgetExt::display(window) + .default_seat() + .and_then(|s| s.pointer()) + .map(|p| { + let (_, x, y) = p.surface_at_position(); + (x as _, y as _) + }) + .unwrap_or_default() +} From 9fc08a5586b7e9fc28bf84267fc84cae0d713893 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 06:15:36 +0200 Subject: [PATCH 09/21] rename file --- src/platform_impl/gtk/{accelerator.rs => mnemonic.rs} | 2 +- src/platform_impl/gtk/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/platform_impl/gtk/{accelerator.rs => mnemonic.rs} (95%) diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/mnemonic.rs similarity index 95% rename from src/platform_impl/gtk/accelerator.rs rename to src/platform_impl/gtk/mnemonic.rs index a682c923..dba54ca1 100644 --- a/src/platform_impl/gtk/accelerator.rs +++ b/src/platform_impl/gtk/mnemonic.rs @@ -40,7 +40,7 @@ pub fn from_gtk_mnemonic>(string: S) -> String { #[cfg(test)] mod tests { - use crate::platform_impl::platform::accelerator::{from_gtk_mnemonic, to_gtk_mnemonic}; + use crate::platform_impl::platform::mnemonic::{from_gtk_mnemonic, to_gtk_mnemonic}; #[test] fn it_converts() { diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 18d6298d..de9de577 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -mod accelerator; mod icon; +mod mnemonic; use std::{ cell::RefCell, @@ -11,10 +11,10 @@ use std::{ rc::Rc, }; -use accelerator::to_gtk_mnemonic; use dpi::Position; use gtk4::{gdk::Rectangle, gio, glib::VariantTy, prelude::*}; pub(crate) use icon::PlatformIcon; +use mnemonic::to_gtk_mnemonic; use crate::{ accelerator::Accelerator, From e9bbefbdbd83d11fce42e6818269e773f3b214ba Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 06:48:50 +0200 Subject: [PATCH 10/21] implement accelerators --- examples/gtk.rs | 2 +- src/platform_impl/gtk/accelerator.rs | 207 +++++++++++++++++++++++++++ src/platform_impl/gtk/mod.rs | 47 +++--- 3 files changed, 239 insertions(+), 17 deletions(-) create mode 100644 src/platform_impl/gtk/accelerator.rs diff --git a/examples/gtk.rs b/examples/gtk.rs index 6da1ba9c..26ed802f 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -45,7 +45,7 @@ fn on_activate(application: >k4::Application) { "Check", true, true, - Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + Some(Accelerator::new(Modifiers::empty(), Code::KeyQ)), ); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/accelerator.rs new file mode 100644 index 00000000..d1b32795 --- /dev/null +++ b/src/platform_impl/gtk/accelerator.rs @@ -0,0 +1,207 @@ +use keyboard_types::{Code, Modifiers}; + +use crate::accelerator::Accelerator; + +impl Accelerator { + pub fn to_gtk(&self) -> String { + let mut gtk = modifiers_to_gtk(self.mods); + gtk.push_str(code_to_gtk(self.key)); + gtk + } +} + +fn modifiers_to_gtk(mods: Modifiers) -> String { + let mut gtk = String::new(); + + if mods.shift() { + gtk.push_str(""); + } + if mods.ctrl() { + gtk.push_str(""); + } + if mods.alt() { + gtk.push_str(""); + } + if mods.meta() { + gtk.push_str(""); + } + gtk +} + +fn code_to_gtk(code: Code) -> &'static str { + match code { + Code::Backquote => "grave", + Code::Backslash => "backslash", + Code::BracketLeft => "bracketleft", + Code::BracketRight => "bracketright", + Code::Comma => "comma", + Code::Digit0 => "1", + Code::Digit1 => "1", + Code::Digit2 => "2", + Code::Digit3 => "3", + Code::Digit4 => "4", + Code::Digit5 => "5", + Code::Digit6 => "6", + Code::Digit7 => "7", + Code::Digit8 => "8", + Code::Digit9 => "9", + Code::Equal => "equal", + Code::KeyA => "A", + Code::KeyB => "B", + Code::KeyC => "C", + Code::KeyD => "D", + Code::KeyE => "E", + Code::KeyF => "F", + Code::KeyG => "G", + Code::KeyH => "H", + Code::KeyI => "I", + Code::KeyJ => "J", + Code::KeyK => "K", + Code::KeyL => "L", + Code::KeyM => "M", + Code::KeyN => "N", + Code::KeyO => "O", + Code::KeyP => "P", + Code::KeyQ => "Q", + Code::KeyR => "R", + Code::KeyS => "S", + Code::KeyT => "T", + Code::KeyU => "U", + Code::KeyV => "V", + Code::KeyW => "W", + Code::KeyX => "X", + Code::KeyY => "Y", + Code::KeyZ => "Z", + Code::Minus => "minus", + Code::Period => "period", + Code::Quote => "quotedbl", + Code::Semicolon => "semicolon", + Code::Slash => "slash", + Code::AltLeft => "Alt_L", + Code::AltRight => "Alt_R", + Code::Backspace => "BackSpace", + Code::CapsLock => "Caps_Lock", + Code::ContextMenu => "Menu", + Code::ControlLeft => "Control_L", + Code::ControlRight => "Control_R", + Code::Enter => "Return", + Code::MetaLeft => "Meta_L", + Code::MetaRight => "Meta_R", + Code::ShiftLeft => "Shift_L", + Code::ShiftRight => "Shift_R", + Code::Space => "space", + Code::Tab => "Tab", + Code::Delete => "Delete", + Code::End => "End", + Code::Help => "Help", + Code::Home => "Home", + Code::Insert => "Insert", + Code::PageDown => "Page_Down", + Code::PageUp => "Page_Up", + Code::ArrowDown => "downarrow", + Code::ArrowLeft => "leftarrow", + Code::ArrowRight => "rightarrow", + Code::ArrowUp => "uparrow", + Code::NumLock => "Num_Lock", + Code::Numpad0 => "KP_0", + Code::Numpad1 => "KP_1", + Code::Numpad2 => "KP_2", + Code::Numpad3 => "KP_3", + Code::Numpad4 => "KP_4", + Code::Numpad5 => "KP_5", + Code::Numpad6 => "KP_6", + Code::Numpad7 => "KP_7", + Code::Numpad8 => "KP_8", + Code::Numpad9 => "KP_9", + Code::NumpadAdd => "KP_Add", + Code::NumpadClear => "Clear", + Code::NumpadComma => "KP_Separator", + Code::NumpadDecimal => "KP_Decimal", + Code::NumpadDivide => "KP_Divide", + Code::NumpadEnter => "KP_Enter", + Code::NumpadEqual => "KP_Equal", + Code::NumpadHash => "numbersign", + Code::NumpadMemoryAdd => "KP_Add", + Code::NumpadMemoryClear => "Clear", + Code::NumpadMemorySubtract => "KP_Subtract", + Code::NumpadMultiply => "KP_Multiply", + Code::NumpadParenLeft => "parenleft", + Code::NumpadParenRight => "parenright", + Code::NumpadStar => "asterisk", + Code::NumpadSubtract => "KP_Subtract", + Code::Escape => "Escape", + Code::Fn => "F", + Code::FnLock => "F", + Code::PrintScreen => "Print", + Code::ScrollLock => "Scroll_Lock", + Code::Pause => "Pause", + Code::BrowserBack => "Back", + Code::BrowserFavorites => "Favorites", + Code::BrowserForward => "Forward", + Code::BrowserHome => "HomePage", + Code::BrowserRefresh => "Reload", + Code::BrowserSearch => "Search", + Code::BrowserStop => "Stop", + Code::Eject => "Eject", + Code::LaunchApp1 => "Launch0", + Code::LaunchApp2 => "Launch1", + Code::LaunchMail => "Mail", + Code::MediaPlayPause => "AudioPlay", + Code::MediaSelect => "AudioMedia", + Code::MediaStop => "AudioStop", + Code::MediaTrackNext => "AudioNext", + Code::MediaTrackPrevious => "AudioPrev", + Code::Power => "PowerOff", + Code::Sleep => "Sleep", + Code::AudioVolumeDown => "AudioLowerVolume", + Code::AudioVolumeMute => "AudioMute", + Code::AudioVolumeUp => "AudioRaiseVolume", + Code::WakeUp => "WakeUp", + Code::Hyper => "Hyper_L", + Code::Super => "Super_L", + Code::Suspend => "Suspend", + Code::Copy => "Copy", + Code::Cut => "Cut", + Code::Find => "Find", + Code::Open => "Open", + Code::Paste => "Paste", + Code::Select => "Select", + Code::Undo => "Undo", + Code::F1 => "F1", + Code::F2 => "F2", + Code::F3 => "F3", + Code::F4 => "F4", + Code::F5 => "F5", + Code::F6 => "F6", + Code::F7 => "F7", + Code::F8 => "F8", + Code::F9 => "F9", + Code::F10 => "F10", + Code::F11 => "F11", + Code::F12 => "F12", + Code::F13 => "F13", + Code::F14 => "F14", + Code::F15 => "F15", + Code::F16 => "F16", + Code::F17 => "F17", + Code::F18 => "F18", + Code::F19 => "F19", + Code::F20 => "F20", + Code::F21 => "F21", + Code::F22 => "F22", + Code::F23 => "F23", + Code::F24 => "F24", + Code::F25 => "F25", + Code::F26 => "F26", + Code::F27 => "F27", + Code::F28 => "F28", + Code::F29 => "F29", + Code::F30 => "F30", + Code::F31 => "F31", + Code::F32 => "F32", + Code::F33 => "F33", + Code::F34 => "F34", + Code::F35 => "F35", + _ => return "", + } +} diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index de9de577..5a6b388e 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +mod accelerator; mod icon; mod mnemonic; @@ -525,11 +526,17 @@ impl MenuChild { } } - fn create_gtk_item_for_menu_item(&mut self, menu_id: u32) -> crate::Result { - let item = gio::MenuItem::new( - Some(&to_gtk_mnemonic(&self.text)), - Some(&format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref())), - ); + fn create_gtk_item_for_menu_item( + &mut self, + app: >k4::Application, + menu_id: u32, + ) -> crate::Result { + let detailed_action = format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref()); + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); + + if let Some(accelerator) = &self.accelerator { + app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); + } let child = GtkMenuChild::Item(item.clone()); self.instances.entry(menu_id).or_default().push(child); @@ -610,10 +617,12 @@ impl MenuChild { app: >k4::Application, menu_id: u32, ) -> crate::Result { - let item = gio::MenuItem::new( - Some(&to_gtk_mnemonic(&self.text)), - Some(&format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref())), - ); + let detailed_action = format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref()); + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); + + if let Some(accelerator) = &self.accelerator { + app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); + } let action_group = action_group_from_app(&app); @@ -691,11 +700,17 @@ impl MenuChild { } } - fn create_gtk_item_for_icon_menu_item(&mut self, menu_id: u32) -> crate::Result { - let item = gio::MenuItem::new( - Some(&to_gtk_mnemonic(&self.text)), - Some(&format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref())), - ); + fn create_gtk_item_for_icon_menu_item( + &mut self, + app: >k4::Application, + menu_id: u32, + ) -> crate::Result { + let detailed_action = format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref()); + let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); + + if let Some(accelerator) = &self.accelerator { + app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); + } if let Some(icon) = &self.icon { item.set_icon(icon.inner.bytes_icon()); @@ -720,9 +735,9 @@ impl dyn IsMenuItem + '_ { 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(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(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) From 3af8f9da329a0cac6b10c1348bf633995fb12cd2 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 06:55:27 +0200 Subject: [PATCH 11/21] fix icon menu item not active --- src/platform_impl/gtk/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 5a6b388e..c3871935 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -61,7 +61,6 @@ impl GtkMenuBar { match self { GtkMenuBar::MenuBar { app, .. } => app, GtkMenuBar::ContextMenu { app, .. } => app, - _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } @@ -705,7 +704,7 @@ impl MenuChild { app: >k4::Application, menu_id: u32, ) -> crate::Result { - let detailed_action = format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref()); + let detailed_action = format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref()); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); if let Some(accelerator) = &self.accelerator { From 0d433c2cd73108eae86cd0f49b060300d889c326 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 07:17:46 +0200 Subject: [PATCH 12/21] implement initial enabled state and set_enabled for items --- src/platform_impl/gtk/mod.rs | 66 +++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index c3871935..1cc5dfd4 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -363,6 +363,8 @@ pub struct MenuChild { instances: HashMap>, ctx_menu_id: u32, children: Vec>>, + + action: Option, } impl MenuChild { @@ -378,6 +380,7 @@ impl MenuChild { ctx_menu_id: COUNTER.next(), instances: HashMap::new(), children: Vec::new(), + action: None, } } @@ -522,6 +525,7 @@ impl MenuChild { ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), + action: None, } } @@ -530,13 +534,25 @@ impl MenuChild { app: >k4::Application, menu_id: u32, ) -> crate::Result { - let detailed_action = format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref()); + let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); if let Some(accelerator) = &self.accelerator { app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); } + if self.action.is_none() { + let action_group = action_group_from_app(&app); + + let action = gio::SimpleAction::new(self.id.as_ref(), None); + let id = self.id.clone(); + action.connect_activate(move |_, _| MenuEvent::send(MenuEvent { id: id.clone() })); + action.set_enabled(self.enabled); + action_group.add_action(&action); + + self.action = Some(action); + } + let child = GtkMenuChild::Item(item.clone()); self.instances.entry(menu_id).or_default().push(child); @@ -547,6 +563,10 @@ impl MenuChild { &self.id } + fn detailed_action(&self) -> String { + format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref()) + } + pub fn item_type(&self) -> &MenuItemType { &self.type_ } @@ -563,8 +583,12 @@ impl MenuChild { self.enabled } - pub fn set_enabled(&self, enabled: bool) { - todo!() + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + + if let Some(action) = self.action.as_ref() { + action.set_enabled(enabled); + } } pub fn set_accelerator(&self, accelerator: Option) -> crate::Result<()> { @@ -585,6 +609,7 @@ impl MenuChild { ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), + action: None, } } } @@ -608,6 +633,7 @@ impl MenuChild { ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), + action: None, } } @@ -616,7 +642,7 @@ impl MenuChild { app: >k4::Application, menu_id: u32, ) -> crate::Result { - let detailed_action = format!("{DEFAULT_ACTION_GROUP}.{}", self.id.as_ref()); + let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); if let Some(accelerator) = &self.accelerator { @@ -628,9 +654,8 @@ impl MenuChild { let state = &self.checked.to_variant(); let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); let id = self.id.clone(); - action.connect_state_notify(move |_| { - MenuEvent::send(MenuEvent { id: id.clone() }); - }); + action.connect_state_notify(move |_| MenuEvent::send(MenuEvent { id: id.clone() })); + action.set_enabled(self.enabled); action_group.add_action(&action); let child = GtkMenuChild::CheckItem { @@ -675,6 +700,7 @@ impl MenuChild { ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), + action: None, } } @@ -696,6 +722,7 @@ impl MenuChild { ctx_menu_id: 0, instances: HashMap::new(), children: Vec::new(), + action: None, } } @@ -704,7 +731,7 @@ impl MenuChild { app: >k4::Application, menu_id: u32, ) -> crate::Result { - let detailed_action = format!("{DEFAULT_DETAILED_ACTION}::{}", self.id.as_ref()); + let detailed_action = self.detailed_action(); let item = gio::MenuItem::new(Some(&to_gtk_mnemonic(&self.text)), Some(&detailed_action)); if let Some(accelerator) = &self.accelerator { @@ -715,6 +742,18 @@ impl MenuChild { item.set_icon(icon.inner.bytes_icon()); } + if self.action.is_none() { + let action_group = action_group_from_app(&app); + + let action = gio::SimpleAction::new(self.id.as_ref(), None); + let id = self.id.clone(); + action.connect_activate(move |_, _| MenuEvent::send(MenuEvent { id: id.clone() })); + action.set_enabled(self.enabled); + action_group.add_action(&action); + + self.action = Some(action); + } + let child = GtkMenuChild::Item(item.clone()); self.instances.entry(menu_id).or_default().push(child); @@ -753,17 +792,6 @@ fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { unsafe { action_group.as_ref() }.clone() } else { let action_group = gio::SimpleActionGroup::new(); - - let action = gtk4::gio::SimpleAction::new(DEFAULT_ACTION, Some(&VariantTy::STRING)); - action.connect_activate(|_, v| { - if let Some(v) = v { - MenuEvent::send(MenuEvent { - id: MenuId(v.as_ref().to_string()), - }); - } - }); - action_group.add_action(&action); - unsafe { app.set_data(ACTION_GROUP_DATA_KEY, action_group.clone()) }; action_group }; From 0c06482c0572340b4a384e3174c671bdbfe9695f Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 07:20:37 +0200 Subject: [PATCH 13/21] implement set_checked --- src/platform_impl/gtk/mod.rs | 50 +++++++++++++++--------------------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 1cc5dfd4..20bbd9d1 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -281,10 +281,6 @@ impl Menu { #[derive(Clone)] enum GtkMenuChild { Item(gio::MenuItem), - CheckItem { - item: gio::MenuItem, - action: gio::SimpleAction, - }, Submenu { id: u32, item: gio::MenuItem, @@ -320,14 +316,6 @@ impl GtkMenuChild { match self { GtkMenuChild::Submenu { item, .. } => item, GtkMenuChild::Item(item) => item, - GtkMenuChild::CheckItem { item, .. } => item, - _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), - } - } - - fn action(&self) -> &gio::SimpleAction { - match self { - GtkMenuChild::CheckItem { action, .. } => action, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } @@ -649,35 +637,39 @@ impl MenuChild { app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); } - let action_group = action_group_from_app(&app); + if self.action.is_none() { + let action_group = action_group_from_app(&app); - let state = &self.checked.to_variant(); - let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); - let id = self.id.clone(); - action.connect_state_notify(move |_| MenuEvent::send(MenuEvent { id: id.clone() })); - action.set_enabled(self.enabled); - action_group.add_action(&action); + let state = &self.checked.to_variant(); + let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); + let id = self.id.clone(); + action.connect_state_notify(move |_| MenuEvent::send(MenuEvent { id: id.clone() })); + action.set_enabled(self.enabled); + action_group.add_action(&action); - let child = GtkMenuChild::CheckItem { - item: item.clone(), - action, - }; + self.action = Some(action); + } + + let child = GtkMenuChild::Item(item.clone()); self.instances.entry(menu_id).or_default().push(child); Ok(item) } pub fn is_checked(&self) -> bool { - self.instances - .values() - .find_map(|i| i.first()) - .and_then(|i| i.action().state()) + self.action + .as_ref() + .and_then(|action| action.state()) .and_then(|s| s.get()) .unwrap_or(self.checked) } - pub fn set_checked(&self, checked: bool) { - todo!() + pub fn set_checked(&mut self, checked: bool) { + self.checked = checked; + + if let Some(action) = self.action.as_ref() { + action.set_state(&checked.to_variant()); + } } } From dc77079cbf81e7bf43598bdaf0a61be8193d6b86 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 13 Mar 2025 07:24:19 +0200 Subject: [PATCH 14/21] update example --- examples/gtk.rs | 64 ++++++++++++++++++++++--------------------------- 1 file changed, 29 insertions(+), 35 deletions(-) diff --git a/examples/gtk.rs b/examples/gtk.rs index 26ed802f..248bd990 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -37,47 +37,41 @@ fn on_activate(application: >k4::Application) { window.present(); - let (menubar, file_menu) = { - let file_menu = { - let about_menu_item = muda::MenuItem::new("About", true, None); - - let check = muda::CheckMenuItem::new( - "Check", - true, - true, - Some(Accelerator::new(Modifiers::empty(), Code::KeyQ)), - ); - - let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); - let icon = load_icon(std::path::Path::new(path)); - let icon_menu_item = muda::IconMenuItem::new("Icon", true, Some(icon), None); - - let quit_menu_item = muda::MenuItem::with_id( - "quit", - "&Quit", - true, - Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), - ); - - let file_menu = muda::Submenu::new("&File", true); - file_menu.append(&about_menu_item).unwrap(); - file_menu.append(&check).unwrap(); - file_menu.append(&icon_menu_item).unwrap(); - file_menu.append(&quit_menu_item).unwrap(); - file_menu - }; - - let menubar = muda::Menu::new(); - menubar.append(&file_menu).unwrap(); - - (menubar, file_menu) - }; + let about_menu_item = muda::MenuItem::new("About", true, None); + + let check = muda::CheckMenuItem::new( + "Check", + true, + true, + Some(Accelerator::new(Modifiers::empty(), Code::KeyQ)), + ); + + let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); + let icon = load_icon(std::path::Path::new(path)); + let icon_menu_item = muda::IconMenuItem::new("Icon", true, Some(icon), None); + + let quit_menu_item = muda::MenuItem::with_id( + "quit", + "&Quit", + true, + Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + ); + + let file_menu = muda::Submenu::new("&File", true); + file_menu.append(&about_menu_item).unwrap(); + file_menu.append(&check).unwrap(); + file_menu.append(&icon_menu_item).unwrap(); + file_menu.append(&quit_menu_item).unwrap(); + + let menubar = muda::Menu::new(); + menubar.append(&file_menu).unwrap(); let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); menubar.init_for_gtk_window(&window, Some(&vbox)).unwrap(); let btn = gtk4::Button::with_label("ASdasd"); let w = window.clone(); + btn.connect_clicked(move |_| { file_menu.show_context_menu_for_gtk_window(w.dynamic_cast_ref().unwrap(), None); }); From adcc6b6c53c5050d03b8b9af932518562eaf4881 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Fri, 14 Mar 2025 07:05:29 +0200 Subject: [PATCH 15/21] fix set_enabled for submenus --- src/platform_impl/gtk/mod.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 20bbd9d1..e7816d91 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -13,7 +13,7 @@ use std::{ }; use dpi::Position; -use gtk4::{gdk::Rectangle, gio, glib::VariantTy, prelude::*}; +use gtk4::{gdk::Rectangle, gio, prelude::*}; pub(crate) use icon::PlatformIcon; use mnemonic::to_gtk_mnemonic; @@ -26,9 +26,7 @@ use crate::{ static COUNTER: Counter = Counter::new(); -const DEFAULT_ACTION: &str = "_internal_sendEvent"; const DEFAULT_ACTION_GROUP: &str = "muda"; -const DEFAULT_DETAILED_ACTION: &str = "muda._internal_sendEvent"; const ACTION_GROUP_DATA_KEY: &str = "mudaActionGroup"; enum GtkMenuBar { @@ -379,9 +377,20 @@ impl MenuChild { ) -> crate::Result { let menu = gio::Menu::new(); let item = gio::MenuItem::new_submenu(Some(&to_gtk_mnemonic(&self.text)), &menu); + item.set_detailed_action(&self.detailed_action()); - let id = COUNTER.next(); + if self.action.is_none() { + let action_group = action_group_from_app(&app); + let action = gio::SimpleAction::new(self.id.as_ref(), None); + action.connect_activate(|_, _| ()); + action.set_enabled(self.enabled); + action_group.add_action(&action); + + self.action = Some(action); + } + + let id = COUNTER.next(); let child = GtkMenuChild::Submenu { item: item.clone(), menu, From e018a51c9424b73644611bec7bd12dde8ffed4cb Mon Sep 17 00:00:00 2001 From: amrbashir Date: Fri, 14 Mar 2025 07:37:26 +0200 Subject: [PATCH 16/21] implement a few more methods on Menu type --- src/menu.rs | 1 + src/platform_impl/gtk/mod.rs | 38 ++++++++++++++++++++++++++++++------ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/menu.rs b/src/menu.rs index d53e5d0b..023d47c1 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -276,6 +276,7 @@ impl Menu { pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, + W: gtk4::prelude::IsA, { self.inner.borrow_mut().remove_for_gtk_window(window) } diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index e7816d91..7a6aabb5 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -197,25 +197,46 @@ impl Menu { Ok(()) } - pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> + pub fn remove_for_gtk_window(&mut self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, + W: gtk4::prelude::IsA, { - todo!() + let id = window.as_ptr() as u32; + + 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 + + Ok(()) } pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, { - todo!() + let id = window.as_ptr() as u32; + let Some(menu_bar) = self.instances.get(&id) else { + return Err(crate::Error::NotInitialized); + }; + menu_bar.menu_bar().set_visible(false); + Ok(()) } pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, { - todo!() + let id = window.as_ptr() as u32; + let Some(menu_bar) = self.instances.get(&id) else { + return Err(crate::Error::NotInitialized); + }; + menu_bar.menu_bar().set_visible(true); + Ok(()) } #[cfg(target_os = "linux")] @@ -223,14 +244,19 @@ impl Menu { where W: gtk4::prelude::IsA, { - todo!() + let id = window.as_ptr() as u32; + self.instances + .get(&id) + .map(|m| m.menu_bar().is_visible()) + .unwrap_or(false) } pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option where W: gtk4::prelude::IsA, { - todo!() + let id = window.as_ptr() as u32; + self.instances.get(&id).map(|m| m.menu_bar().clone()) } pub fn show_context_menu_for_gtk_window( From 3a29ee8418909e6af829c2f84c8005578340c48d Mon Sep 17 00:00:00 2001 From: amrbashir Date: Fri, 14 Mar 2025 08:27:36 +0200 Subject: [PATCH 17/21] implemenet set_accelerator --- src/platform_impl/gtk/mod.rs | 38 ++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 7a6aabb5..06e4e93d 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -304,7 +304,10 @@ impl Menu { #[derive(Clone)] enum GtkMenuChild { - Item(gio::MenuItem), + Item { + item: gio::MenuItem, + app: gtk4::Application, + }, Submenu { id: u32, item: gio::MenuItem, @@ -332,14 +335,14 @@ impl GtkMenuChild { match self { GtkMenuChild::Submenu { app, .. } => app, GtkMenuChild::ContextMenu { app, .. } => app, - _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), + GtkMenuChild::Item { app, .. } => app, } } fn item(&self) -> &gio::MenuItem { match self { GtkMenuChild::Submenu { item, .. } => item, - GtkMenuChild::Item(item) => item, + GtkMenuChild::Item { item, .. } => item, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } @@ -576,7 +579,10 @@ impl MenuChild { self.action = Some(action); } - let child = GtkMenuChild::Item(item.clone()); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + }; self.instances.entry(menu_id).or_default().push(child); Ok(item) @@ -614,8 +620,18 @@ impl MenuChild { } } - pub fn set_accelerator(&self, accelerator: Option) -> crate::Result<()> { - todo!() + pub fn set_accelerator(&mut self, accelerator: Option) -> crate::Result<()> { + self.accelerator = accelerator; + + let detailed_action = self.detailed_action(); + let accelerator = accelerator.map(|a| a.to_gtk()); + let accelerator = accelerator.as_deref().map(|a| [a]).unwrap_or_default(); + for item in self.instances.values().flat_map(|v| v.iter()) { + let app = item.application(); + app.set_accels_for_action(&detailed_action, accelerator.as_slice()); + } + + Ok(()) } } @@ -685,7 +701,10 @@ impl MenuChild { self.action = Some(action); } - let child = GtkMenuChild::Item(item.clone()); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + }; self.instances.entry(menu_id).or_default().push(child); Ok(item) @@ -781,7 +800,10 @@ impl MenuChild { self.action = Some(action); } - let child = GtkMenuChild::Item(item.clone()); + let child = GtkMenuChild::Item { + item: item.clone(), + app: app.clone(), + }; self.instances.entry(menu_id).or_default().push(child); Ok(item) From dcb6b31f2cba14dbe7ec8cd1ae22986f72021458 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Tue, 7 Jul 2026 23:07:06 +0300 Subject: [PATCH 18/21] restore gtk feature flag --- Cargo.toml | 9 +++-- README.md | 2 - examples/gtk.rs | 56 ++++++++++++++++++++++---- src/error.rs | 22 ++++++++++- src/icon.rs | 13 +++++- src/items/submenu.rs | 11 +++++- src/lib.rs | 21 +++++++--- src/menu.rs | 77 ++++++++++++++++++++++++++++++++---- src/platform_impl/gtk/mod.rs | 8 +++- src/platform_impl/mod.rs | 11 +++++- 10 files changed, 197 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a879cc54..ccc34c6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ rust-version = "1.73" include = ["README.md", "src/**/*.rs", "Cargo.toml", "LICENSE-APACHE", "LICENSE-MIT", "LICENSE.spdx"] [features] -default = [] +default = ["gtk"] +gtk = ["dep:gtk4", "dep:png"] common-controls-v6 = [] serde = ["dep:serde", "dpi/serde", "keyboard-types/serde"] @@ -41,9 +42,9 @@ features = [ "Win32_UI_Controls", ] -[target.'cfg(target_os = "linux")'.dependencies] -gtk4 = "0.9" -png = "0.17" +[target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] +gtk4 = { version = "0.9", optional = true } +png = { version = "0.17", optional = true } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.1" diff --git a/README.md b/README.md index a72c5cb9..5f44809f 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ Menu Utilities library for Desktop Applications. ### Cargo Features - `common-controls-v6`: Use `TaskDialogIndirect` API from `ComCtl32.dll` v6 on Windows for showing the predefined `About` menu item dialog. -- `libxdo`: Enables linking to `libxdo` on Linux or FreeBSD which is used for the predefined `Copy`, `Cut`, `Paste` and `SelectAll` menu item. -- `serde`: Enables de/serializing the dpi types. - `gtk`: Enables the `gtk` crate dependency on Linux or FreeBSD. This is required for `muda` to function properly on Linux or FreeBSD. ## Dependencies (Linux Only) diff --git a/examples/gtk.rs b/examples/gtk.rs index 248bd990..dfe7dbf7 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -1,11 +1,35 @@ -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] use gtk4::prelude::*; -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] use keyboard_types::{Code, Modifiers}; -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] use muda::{accelerator::Accelerator, MenuEvent}; -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] fn main() { // Create a new application let application = gtk4::Application::builder() @@ -16,14 +40,26 @@ fn main() { application.run(); } -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] fn on_startup(_: >k4::Application) { MenuEvent::set_event_handler(Some(|event| { println!("{event:?}"); })); } -#[cfg(target_os = "linux")] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] fn on_activate(application: >k4::Application) { use muda::ContextMenu; @@ -80,7 +116,13 @@ fn on_activate(application: >k4::Application) { window.set_child(Some(&vbox)); } -#[cfg(not(target_os = "linux"))] +#[cfg(not(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +)))] fn main() {} fn load_icon(path: &std::path::Path) -> muda::Icon { diff --git a/src/error.rs b/src/error.rs index 1f2b2e7b..56d006ab 100644 --- a/src/error.rs +++ b/src/error.rs @@ -15,13 +15,31 @@ pub enum Error { #[cfg(windows)] #[error("This menu has not been initialized for this hwnd`")] NotInitialized, - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] #[error("This menu has not been initialized for this gtk window`")] NotInitialized, #[cfg(windows)] #[error("This menu has already been initialized for this hwnd`")] AlreadyInitialized, - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] #[error("This menu has already been initialized for this gtk window`")] AlreadyInitialized, #[error(transparent)] diff --git a/src/icon.rs b/src/icon.rs index 20d59d62..a5d88850 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -35,7 +35,16 @@ pub enum BadIcon { /// Produced when underlying OS functionality failed to create the icon OsError(io::Error), /// Produced when encoding provided RGBA into png - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] PngEncodingError(png::EncodingError), } @@ -56,7 +65,7 @@ impl fmt::Display for BadIcon { width, height, pixel_count, width_x_height, ), BadIcon::OsError(e) => write!(f, "OS error when instantiating the icon: {:?}", e), - #[cfg(target_os = "linux")] + #[cfg(all(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"), feature = "gtk"))] BadIcon::PngEncodingError(e) => write!(f, "PNG encoding error when instantiating the icon: {:?}", e), } } diff --git a/src/items/submenu.rs b/src/items/submenu.rs index 36e11381..3cd26843 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -267,7 +267,16 @@ impl ContextMenu for Submenu { self.inner.borrow().detach_menu_subclass_from_hwnd(hwnd) } - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] fn show_context_menu_for_gtk_window( &self, w: >k4::Window, diff --git a/src/lib.rs b/src/lib.rs index 83290a22..e45f0a71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,14 +77,14 @@ //! ```no_run //! # let menu = muda::Menu::new(); //! # let window_hwnd = 0; -//! # #[cfg(target_os = "linux")] +//! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] //! # let gtk_window = gtk4::Window::builder().build(); -//! # #[cfg(target_os = "linux")] +//! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] //! # let vertical_gtk_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); //! // --snip-- //! #[cfg(target_os = "windows")] //! unsafe { menu.init_for_hwnd(window_hwnd) }; -//! #[cfg(target_os = "linux")] +//! #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] //! menu.init_for_gtk_window(>k_window, Some(&vertical_gtk_box)); //! #[cfg(target_os = "macos")] //! menu.init_for_nsapp(); @@ -98,7 +98,7 @@ //! use muda::ContextMenu; //! # let menu = muda::Menu::new(); //! # let window_hwnd = 0; -//! # #[cfg(target_os = "linux")] +//! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] //! # let gtk_window = gtk4::Window::builder().build(); //! # #[cfg(target_os = "macos")] //! # let nsview = std::ptr::null(); @@ -106,7 +106,7 @@ //! let position = muda::dpi::PhysicalPosition { x: 100., y: 120. }; //! #[cfg(target_os = "windows")] //! unsafe { menu.show_context_menu_for_hwnd(window_hwnd, Some(position.into())) }; -//! #[cfg(target_os = "linux")] +//! #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] //! menu.show_context_menu_for_gtk_window(>k_window, Some(position.into())); //! #[cfg(target_os = "macos")] //! unsafe { menu.show_context_menu_for_nsview(nsview, Some(position.into())) }; @@ -370,7 +370,16 @@ pub trait ContextMenu { /// Returns `true` if menu tracking ended because an item was selected or clicked outside the menu to dismiss it. /// /// Returns `false` if menu tracking was cancelled for any reason. - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] fn show_context_menu_for_gtk_window( &self, w: >k4::Window, diff --git a/src/menu.rs b/src/menu.rs index e4e0f1f7..0d2570de 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -184,7 +184,16 @@ impl Menu { /// ## Panics: /// /// Panics if the gtk event loop hasn't been initialized on the thread. - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] pub fn init_for_gtk_window(&self, window: &W, container: Option<&C>) -> crate::Result<()> where W: gtk4::prelude::IsA, @@ -272,7 +281,16 @@ impl Menu { } /// Removes this menu from a [`gtk4::Window`] - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, @@ -292,7 +310,16 @@ impl Menu { } /// Hides this menu from a [`gtk4::Window`] - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, @@ -311,7 +338,16 @@ impl Menu { } /// Shows this menu on a [`gtk4::Window`] - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where W: gtk4::prelude::IsA, @@ -330,7 +366,16 @@ impl Menu { } /// Returns whether this menu visible on a [`gtk4::Window`] - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where W: gtk4::prelude::IsA, @@ -338,7 +383,16 @@ impl Menu { self.inner.borrow().is_visible_on_gtk_window(window) } - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] /// Returns the [`gtk4::MenuBar`] that is associated with this window if it exists. /// This is useful to get information about the menubar for example its height. pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option @@ -394,7 +448,16 @@ impl ContextMenu for Menu { self.inner.borrow().detach_menu_subclass_from_hwnd(hwnd) } - #[cfg(target_os = "linux")] + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] fn show_context_menu_for_gtk_window( &self, window: >k4::Window, diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 671c4574..d25559ab 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -239,7 +239,13 @@ impl Menu { Ok(()) } - #[cfg(target_os = "linux")] + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where W: gtk4::prelude::IsA, diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index d5cb07e9..2288fcbc 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -5,7 +5,16 @@ #[cfg(target_os = "windows")] #[path = "windows/mod.rs"] mod platform; -#[cfg(target_os = "linux")] +#[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" +))] #[path = "gtk/mod.rs"] mod platform; #[cfg(target_os = "macos")] From 07f66849d83d295ce54a0ef50af6cd9ef7e255fe Mon Sep 17 00:00:00 2001 From: John Carmack Date: Tue, 7 Jul 2026 13:26:30 -0700 Subject: [PATCH 19/21] feat(gtk): port the backend to gtk4 Builds on the feat/gtk4 backend: brings in the extension work from rmakestrash's fork and adapts everything to dev's current API, so the public surface stays what dev ships today and call sites compile unchanged. - gtk4 0.11 and png 0.18, MSRV 1.92 to match gtk4's floor - the gtk dependency is optional behind a `gtk` feature (part of default) with dev's BSD-wide target cfg; `libxdo` stays as a no-op feature name for compatibility - shared modules follow dev's current API (the KeyAccelerator line); the only changes there are the feature and target cfg gates - backend methods keep their gtk3 names and shapes; only widget types move with the toolkit (Menu to PopoverMenu, MenuBar to PopoverMenuBar, Container bounds to Widget) - predefined item accelerators are wired on gtk, matching macOS - KeyAccelerator::to_gtk covers the Key-based accelerator path Co-authored-by: amrbashir Co-authored-by: rmakestrash-jpg --- .changes/gtk4.md | 5 + Cargo.lock | 674 ++++++++++-------- Cargo.toml | 12 +- examples/gtk.rs | 25 +- examples/tao.rs | 38 +- .../windows-common-controls-v6/src/main.rs | 4 +- examples/winit.rs | 4 +- examples/wry.rs | 46 +- src/accelerator.rs | 26 +- src/icon.rs | 2 +- src/items/predefined.rs | 37 +- src/items/submenu.rs | 16 +- src/lib.rs | 27 +- src/menu.rs | 65 +- src/platform_impl/gtk/accelerator.rs | 92 +-- src/platform_impl/gtk/icon.rs | 28 +- src/platform_impl/gtk/mnemonic.rs | 1 + src/platform_impl/gtk/mod.rs | 576 ++++++++++++--- 18 files changed, 1114 insertions(+), 564 deletions(-) create mode 100644 .changes/gtk4.md diff --git a/.changes/gtk4.md b/.changes/gtk4.md new file mode 100644 index 00000000..cc149bb8 --- /dev/null +++ b/.changes/gtk4.md @@ -0,0 +1,5 @@ +--- +"muda": minor +--- + +Port the Linux backend from GTK3 to GTK4 (`gtk4` 0.11, `glib` 0.22). Every public method keeps its name and signature shape; the gtk types in returns and bounds follow the toolkit (`PopoverMenu`/`PopoverMenuBar`, `Widget` bounds), and `ContextMenu::gtk_context_menu` is retained on GTK4. Raises MSRV to 1.92. diff --git a/Cargo.lock b/Cargo.lock index 6a3c102f..5bb45c8c 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" @@ -44,7 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" dependencies = [ "android-properties", - "bitflags 2.8.0", + "bitflags 2.13.0", "cc", "cesu8", "jni", @@ -131,11 +131,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.8.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -188,9 +188,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" [[package]] name = "bytes" -version = "1.9.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cairo-rs" @@ -198,7 +198,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "cairo-sys-rs 0.18.2", "glib 0.18.5", "libc", @@ -208,13 +208,13 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.20.7" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50b5510d86cf96ac2370e66d8dc960882f3df179d6a5a1e52bd94a1416c0f7" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" dependencies = [ - "bitflags 2.8.0", - "cairo-sys-rs 0.20.7", - "glib 0.20.7", + "bitflags 2.13.0", + "cairo-sys-rs 0.22.0", + "glib 0.22.8", "libc", ] @@ -231,13 +231,13 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.7" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18b6bb8e43c7eb0f2aac7976afe0c61b6f5fc2ab7bc4c139537ea56c92290df" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.22.8", "libc", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] @@ -246,7 +246,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "log", "polling", "rustix", @@ -290,17 +290,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.12.16", ] [[package]] name = "cfg-expr" -version = "0.17.2" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" dependencies = [ "smallvec", - "target-lexicon", + "target-lexicon 0.13.5", ] [[package]] @@ -321,7 +321,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79398230a6e2c08f5c9760610eb6924b52aa9e7950a619602baba59dcbbdbb2" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block", "cocoa-foundation", "core-foundation 0.10.0", @@ -337,7 +337,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14045fb83be07b5acf1c0884b2180461635b433455fa35d1cd6f17f1450679d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block", "core-foundation 0.10.0", "core-graphics-types 0.2.0", @@ -415,7 +415,7 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-graphics-types 0.2.0", "foreign-types", @@ -439,7 +439,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "core-foundation 0.10.0", "libc", ] @@ -464,9 +464,9 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -511,7 +511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -530,7 +530,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -549,6 +549,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -557,7 +567,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -589,7 +599,7 @@ checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -697,7 +707,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -765,7 +775,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -827,13 +837,13 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.7" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6efc7705f7863d37b12ad6974cbb310d35d054f5108cdc1e69037742f573c4c" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" dependencies = [ - "gdk-pixbuf-sys 0.20.7", - "gio 0.20.7", - "glib 0.20.7", + "gdk-pixbuf-sys 0.22.0", + "gio 0.22.8", + "glib 0.22.8", "libc", ] @@ -852,15 +862,15 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.7" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67f2587c9202bf997476bbba6aaed4f78a11538a2567df002a5f57f5331d0b5c" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" dependencies = [ - "gio-sys 0.20.8", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "gio-sys 0.22.8", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "libc", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] @@ -882,34 +892,34 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0196720118f880f71fe7da971eff58cc43a89c9cf73f46076b7cb1e60889b15" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" dependencies = [ - "cairo-rs 0.20.7", - "gdk-pixbuf 0.20.7", + "cairo-rs 0.22.0", + "gdk-pixbuf 0.22.0", "gdk4-sys", - "gio 0.20.7", - "glib 0.20.7", + "gio 0.22.8", + "glib 0.22.8", "libc", - "pango 0.20.7", + "pango 0.22.8", ] [[package]] name = "gdk4-sys" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b0e1340bd15e7a78810cf39fed9e5d85f0a8f80b1d999d384ca17dcc452b60" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" 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.22.0", + "gdk-pixbuf-sys 0.22.0", + "gio-sys 0.22.8", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "libc", - "pango-sys 0.20.7", + "pango-sys 0.22.0", "pkg-config", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] @@ -1016,16 +1026,16 @@ dependencies = [ [[package]] name = "gio" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a517657589a174be9f60c667f1fec8b7ac82ed5db4ebf56cf073a3b5955d8e2e" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" dependencies = [ "futures-channel", "futures-core", "futures-io", "futures-util", - "gio-sys 0.20.8", - "glib 0.20.7", + "gio-sys 0.22.8", + "glib 0.22.8", "libc", "pin-project-lite", "smallvec", @@ -1046,15 +1056,15 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.8" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" dependencies = [ - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "libc", - "system-deps 7.0.3", - "windows-sys 0.59.0", + "system-deps 7.0.8", + "windows-sys 0.61.2", ] [[package]] @@ -1063,7 +1073,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -1082,20 +1092,20 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.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.22.8", + "glib-macros 0.22.6", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "libc", "memchr", "smallvec", @@ -1108,24 +1118,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "glib-macros" -version = "0.20.7" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" dependencies = [ "heck 0.5.0", - "proc-macro-crate 3.2.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -1140,12 +1149,12 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" dependencies = [ "libc", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] @@ -1161,67 +1170,65 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.7" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.22.8", "libc", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] name = "graphene-rs" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f39d3bcd2e24fd9c2874a56f277b72c03e728de9bdc95a8d4ef4c962f10ced98" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" dependencies = [ - "glib 0.20.7", + "glib 0.22.8", "graphene-sys", - "libc", ] [[package]] name = "graphene-sys" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a68d39515bf340e879b72cecd4a25c1332557757ada6e8aba8654b4b81d23a" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" dependencies = [ - "glib-sys 0.20.7", + "glib-sys 0.22.8", "libc", - "pkg-config", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] name = "gsk4" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b9188db0a6219e708b6b6e7225718e459def664023dbddb8395ca1486d8102" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" dependencies = [ - "cairo-rs 0.20.7", + "cairo-rs 0.22.0", "gdk4", - "glib 0.20.7", + "glib 0.22.8", "graphene-rs", "gsk4-sys", "libc", - "pango 0.20.7", + "pango 0.22.8", ] [[package]] name = "gsk4-sys" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca10fc65d68528a548efa3d8747934adcbe7058b73695c9a7f43a25352fce14" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" dependencies = [ - "cairo-sys-rs 0.20.7", + "cairo-sys-rs 0.22.0", "gdk4-sys", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "graphene-sys", "libc", - "pango-sys 0.20.7", - "system-deps 7.0.3", + "pango-sys 0.22.0", + "system-deps 7.0.8", ] [[package]] @@ -1273,59 +1280,59 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "gtk4" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b697ff938136625f6acf75f01951220f47a45adcf0060ee55b4671cf734dac44" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" dependencies = [ - "cairo-rs 0.20.7", + "cairo-rs 0.22.0", "field-offset", "futures-channel", - "gdk-pixbuf 0.20.7", + "gdk-pixbuf 0.22.0", "gdk4", - "gio 0.20.7", - "glib 0.20.7", + "gio 0.22.8", + "glib 0.22.8", "graphene-rs", "gsk4", "gtk4-macros", "gtk4-sys", "libc", - "pango 0.20.7", + "pango 0.22.8", ] [[package]] name = "gtk4-macros" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "gtk4-sys" -version = "0.9.5" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af4b680cee5d2f786a2f91f1c77e95ecf2254522f0ca4edf3a2dce6cb35cecf" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" dependencies = [ - "cairo-sys-rs 0.20.7", - "gdk-pixbuf-sys 0.20.7", + "cairo-sys-rs 0.22.0", + "gdk-pixbuf-sys 0.22.0", "gdk4-sys", - "gio-sys 0.20.8", - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "gio-sys 0.22.8", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "graphene-sys", "gsk4-sys", "libc", - "pango-sys 0.20.7", - "system-deps 7.0.3", + "pango-sys 0.22.0", + "system-deps 7.0.8", ] [[package]] @@ -1336,9 +1343,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" [[package]] name = "heck" @@ -1498,7 +1505,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -1524,9 +1531,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.5" +version = "0.25.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd6f44aed642f18953a158afeb30206f4d50da59fbc66ecb53c66488de73563b" +checksum = "db35664ce6b9810857a38a906215e75a9c879f0696556a39f59c62829710251a" dependencies = [ "bytemuck", "byteorder-lite", @@ -1546,12 +1553,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.17.1", ] [[package]] @@ -1636,7 +1643,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] @@ -1682,7 +1689,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "libc", "redox_syscall 0.5.8", ] @@ -1752,9 +1759,9 @@ checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" [[package]] name = "memchr" -version = "2.7.4" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "memmap2" @@ -1794,15 +1801,14 @@ dependencies = [ "image", "keyboard-types", "objc2 0.6.4", - "objc2-app-kit 0.3.0", + "objc2-app-kit 0.3.1", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "once_cell", - "png 0.17.16", "png 0.18.1", "serde", "tao", - "thiserror 2.0.11", + "thiserror 2.0.18", "windows-sys 0.61.2", "winit", "wry", @@ -1814,7 +1820,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "jni-sys", "log", "ndk-sys", @@ -1874,10 +1880,10 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 3.2.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -1921,7 +1927,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "libc", "objc2 0.5.2", @@ -1933,14 +1939,14 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" +checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -1949,7 +1955,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -1973,7 +1979,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -1981,11 +1987,12 @@ dependencies = [ [[package]] name = "objc2-core-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", + "dispatch2", "objc2 0.6.4", ] @@ -2025,7 +2032,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "dispatch", "libc", @@ -2034,11 +2041,11 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "objc2 0.6.4", "objc2-core-foundation", ] @@ -2061,7 +2068,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2073,7 +2080,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -2096,7 +2103,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-cloud-kit", @@ -2128,7 +2135,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "objc2 0.5.2", "objc2-core-location", @@ -2155,9 +2162,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "orbclient" @@ -2192,14 +2199,13 @@ dependencies = [ [[package]] name = "pango" -version = "0.20.7" +version = "0.22.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e89bd74250a03a05cec047b43465469102af803be2bf5e5a1088f8b8455e087" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" dependencies = [ - "gio 0.20.7", - "glib 0.20.7", - "libc", - "pango-sys 0.20.7", + "gio 0.22.8", + "glib 0.22.8", + "pango-sys 0.22.0", ] [[package]] @@ -2216,14 +2222,14 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.7" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71787e0019b499a5eda889279e4adb455a4f3fdd6870cd5ab7f4a5aa25df6699" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" dependencies = [ - "glib-sys 0.20.7", - "gobject-sys 0.20.7", + "glib-sys 0.22.8", + "gobject-sys 0.22.6", "libc", - "system-deps 7.0.3", + "system-deps 7.0.8", ] [[package]] @@ -2364,7 +2370,7 @@ checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -2381,9 +2387,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -2404,7 +2410,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", @@ -2453,20 +2459,21 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ + "toml_datetime 0.6.3", "toml_edit 0.20.2", ] [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.22.23", + "toml_edit 0.25.12+spec-1.1.0", ] [[package]] @@ -2510,9 +2517,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" dependencies = [ "memchr", ] @@ -2628,7 +2635,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", ] [[package]] @@ -2646,7 +2653,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "errno", "libc", "linux-raw-sys", @@ -2721,22 +2728,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 = "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 = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +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.118", ] [[package]] @@ -2748,6 +2765,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -2798,9 +2824,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smithay-client-toolkit" @@ -2808,7 +2834,7 @@ version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "calloop", "calloop-wayland-source", "cursor-icon", @@ -2913,9 +2939,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.96" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -2930,7 +2956,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -2942,20 +2968,20 @@ dependencies = [ "cfg-expr 0.15.8", "heck 0.5.0", "pkg-config", - "toml", + "toml 0.8.2", "version-compare", ] [[package]] name = "system-deps" -version = "7.0.3" +version = "7.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" dependencies = [ - "cfg-expr 0.17.2", + "cfg-expr 0.20.8", "heck 0.5.0", "pkg-config", - "toml", + "toml 1.1.2+spec-1.1.0", "version-compare", ] @@ -2965,7 +2991,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "core-foundation 0.10.0", "core-graphics 0.24.0", "crossbeam-channel", @@ -2983,8 +3009,8 @@ dependencies = [ "ndk-context", "ndk-sys", "objc2 0.6.4", - "objc2-app-kit 0.3.0", - "objc2-foundation 0.3.0", + "objc2-app-kit 0.3.1", + "objc2-foundation 0.3.2", "once_cell", "parking_lot", "raw-window-handle", @@ -2992,8 +3018,8 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows 0.61.3", - "windows-core 0.61.2", + "windows 0.61.1", + "windows-core 0.61.0", "windows-version", "x11-dl", ] @@ -3006,7 +3032,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -3015,6 +3041,12 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + [[package]] name = "tendril" version = "0.4.3" @@ -3043,11 +3075,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.18", ] [[package]] @@ -3058,18 +3090,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -3114,28 +3146,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.8", + "toml_datetime 0.6.3", "toml_edit 0.20.2", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +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.14.0", + "toml_datetime 0.6.3", "winnow 0.5.40", ] @@ -3145,24 +3201,40 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.7.1", + "indexmap 2.14.0", "serde", - "serde_spanned", - "toml_datetime", + "serde_spanned 0.6.8", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.23" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" dependencies = [ - "indexmap 2.7.1", - "toml_datetime", - "winnow 0.7.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", ] +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tracing" version = "0.1.41" @@ -3288,7 +3360,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", "wasm-bindgen-shared", ] @@ -3323,7 +3395,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3339,9 +3411,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.7" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" +checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121" dependencies = [ "cc", "downcast-rs", @@ -3353,11 +3425,11 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.7" +version = "0.31.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" +checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "rustix", "wayland-backend", "wayland-scanner", @@ -3369,7 +3441,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "cursor-icon", "wayland-backend", ] @@ -3387,11 +3459,11 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.5" +version = "0.32.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" +checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -3399,11 +3471,11 @@ dependencies = [ [[package]] name = "wayland-protocols-plasma" -version = "0.3.5" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" +checksum = "4fd38cdad69b56ace413c6bcc1fbf5acc5e2ef4af9d5f8f1f9570c0c83eae175" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3416,7 +3488,7 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -3425,9 +3497,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" +checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484" dependencies = [ "proc-macro2", "quick-xml", @@ -3436,9 +3508,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.5" +version = "0.31.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" +checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615" dependencies = [ "dlib", "log", @@ -3532,7 +3604,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -3589,12 +3661,12 @@ dependencies = [ [[package]] name = "windows" -version = "0.61.3" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ "windows-collections", - "windows-core 0.61.2", + "windows-core 0.61.0", "windows-future", "windows-link 0.1.3", "windows-numerics", @@ -3606,7 +3678,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-core 0.61.2", + "windows-core 0.61.0", ] [[package]] @@ -3624,26 +3696,25 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.61.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement 0.60.0", + "windows-interface 0.59.1", "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-result 0.3.2", + "windows-strings 0.4.0", ] [[package]] name = "windows-future" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ - "windows-core 0.61.2", + "windows-core 0.61.0", "windows-link 0.1.3", - "windows-threading", ] [[package]] @@ -3654,18 +3725,18 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -3676,18 +3747,18 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -3708,7 +3779,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-core 0.61.2", + "windows-core 0.61.0", "windows-link 0.1.3", ] @@ -3723,9 +3794,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.4" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" dependencies = [ "windows-link 0.1.3", ] @@ -3742,9 +3813,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97" dependencies = [ "windows-link 0.1.3", ] @@ -3831,15 +3902,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-version" version = "0.1.1" @@ -3983,14 +4045,14 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winit" -version = "0.30.8" +version = "0.30.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" +checksum = "a4409c10174df8779dc29a4788cac85ed84024ccbc1743b776b21a520ee1aaf4" dependencies = [ "ahash", "android-activity", "atomic-waker", - "bitflags 2.8.0", + "bitflags 2.13.0", "block2", "bytemuck", "calloop", @@ -4044,9 +4106,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e49d2d35d3fad69b39b94139037ecfb4f359f08958b9c11e7315ce770462419" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" dependencies = [ "memchr", ] @@ -4157,7 +4219,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" dependencies = [ - "bitflags 2.8.0", + "bitflags 2.13.0", "dlib", "log", "once_cell", @@ -4190,7 +4252,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", "synstructure", ] @@ -4212,7 +4274,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] [[package]] @@ -4232,7 +4294,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", "synstructure", ] @@ -4255,5 +4317,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn 2.0.118", ] diff --git a/Cargo.toml b/Cargo.toml index ccc34c6b..75969a9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,12 +9,14 @@ readme = "README.md" repository = "https://github.com/tauri-apps/muda" documentation = "https://docs.rs/muda" categories = ["gui"] -rust-version = "1.73" +rust-version = "1.92" include = ["README.md", "src/**/*.rs", "Cargo.toml", "LICENSE-APACHE", "LICENSE-MIT", "LICENSE.spdx"] [features] -default = ["gtk"] -gtk = ["dep:gtk4", "dep:png"] +default = ["libxdo", "gtk"] +# No-op on GTK4: libxdo is X11-only. The feature name stays for compatibility. +libxdo = [] +gtk = ["dep:gtk", "dep:png"] common-controls-v6 = [] serde = ["dep:serde", "dpi/serde", "keyboard-types/serde"] @@ -43,8 +45,8 @@ features = [ ] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))'.dependencies] -gtk4 = { version = "0.9", optional = true } -png = { version = "0.17", optional = true } +gtk = { package = "gtk4", version = "0.11", optional = true } +png = { version = "0.18", optional = true } [target.'cfg(target_os = "macos")'.dependencies] objc2 = "0.6.1" diff --git a/examples/gtk.rs b/examples/gtk.rs index dfe7dbf7..9b48a8e9 100644 --- a/examples/gtk.rs +++ b/examples/gtk.rs @@ -5,7 +5,7 @@ target_os = "netbsd", target_os = "openbsd" ))] -use gtk4::prelude::*; +use gtk::prelude::*; #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -32,7 +32,7 @@ use muda::{accelerator::Accelerator, MenuEvent}; ))] fn main() { // Create a new application - let application = gtk4::Application::builder() + let application = gtk::Application::builder() .application_id("com.github.gtk4-rs.examples.menubar") .build(); application.connect_startup(on_startup); @@ -47,7 +47,7 @@ fn main() { target_os = "netbsd", target_os = "openbsd" ))] -fn on_startup(_: >k4::Application) { +fn on_startup(_: >k::Application) { MenuEvent::set_event_handler(Some(|event| { println!("{event:?}"); })); @@ -60,10 +60,10 @@ fn on_startup(_: >k4::Application) { target_os = "netbsd", target_os = "openbsd" ))] -fn on_activate(application: >k4::Application) { +fn on_activate(application: >k::Application) { use muda::ContextMenu; - let window = gtk4::ApplicationWindow::builder() + let window = gtk::ApplicationWindow::builder() .application(application) .title("Menubar Example") .default_width(350) @@ -79,7 +79,7 @@ fn on_activate(application: >k4::Application) { "Check", true, true, - Some(Accelerator::new(Modifiers::empty(), Code::KeyQ)), + Some(Accelerator::new(Some(Modifiers::empty()), Code::KeyQ)), ); let path = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/icon.png"); @@ -90,7 +90,7 @@ fn on_activate(application: >k4::Application) { "quit", "&Quit", true, - Some(Accelerator::new(Modifiers::CONTROL, Code::KeyQ)), + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyQ)), ); let file_menu = muda::Submenu::new("&File", true); @@ -102,10 +102,10 @@ fn on_activate(application: >k4::Application) { let menubar = muda::Menu::new(); menubar.append(&file_menu).unwrap(); - let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); menubar.init_for_gtk_window(&window, Some(&vbox)).unwrap(); - let btn = gtk4::Button::with_label("ASdasd"); + let btn = gtk::Button::with_label("ASdasd"); let w = window.clone(); btn.connect_clicked(move |_| { @@ -125,6 +125,13 @@ fn on_activate(application: >k4::Application) { )))] fn main() {} +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] fn load_icon(path: &std::path::Path) -> muda::Icon { let (icon_rgba, icon_width, icon_height) = { let image = image::open(path) diff --git a/examples/tao.rs b/examples/tao.rs index 9cc94214..4da1cb49 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -100,7 +100,7 @@ fn main() { "custom-i-1", "C&ustom 1", true, - Some(Accelerator::new(Modifiers::ALT, Code::KeyC)), + Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyC)), ); let image_item = IconMenuItem::with_id( @@ -108,7 +108,7 @@ fn main() { "Image custom 1", true, Some(icon), - Some(Accelerator::new(Modifiers::CONTROL, Code::KeyC)), + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)), ); let check_custom_i_1 = @@ -120,7 +120,7 @@ fn main() { "Check Custom 3", true, true, - Some(Accelerator::new(Modifiers::SHIFT, Code::KeyD)), + Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), ); let copy_i = PredefinedMenuItem::copy(None); @@ -165,11 +165,19 @@ fn main() { menu_bar.init_for_hwnd(window.hwnd() as _); menu_bar.init_for_hwnd(window2.hwnd() as _); } - // #[cfg(target_os = "linux")] - // { - // menu_bar.init_for_gtk_window(window.gtk_window(), window.default_vbox()); - // menu_bar.init_for_gtk_window(window2.gtk_window(), window2.default_vbox()); - // } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + // Pending a tao release on gtk4; muda's gtk API now takes gtk4 types. + // menu_bar.init_for_gtk_window(window.gtk_window(), window.default_vbox()); + // menu_bar.init_for_gtk_window(window2.gtk_window(), window2.default_vbox()); + let _ = (&menu_bar, &window, &window2); + } #[cfg(target_os = "macos")] { menu_bar.init_for_nsapp(); @@ -243,8 +251,18 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option

wry::Result<()> { "Image custom 1", true, Some(icon), - Some(Accelerator::new(Modifiers::CONTROL, Code::KeyC)), + Some(Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC)), ); let check_custom_i_1 = CheckMenuItem::new("Check Custom 1", true, true, None); @@ -130,7 +130,7 @@ fn main() -> wry::Result<()> { "Check Custom 3", true, true, - Some(Accelerator::new(Modifiers::SHIFT, Code::KeyD)), + Some(Accelerator::new(Some(Modifiers::SHIFT), Code::KeyD)), ); let copy_i = PredefinedMenuItem::copy(None); @@ -193,12 +193,14 @@ fn main() -> wry::Result<()> { target_os = "openbsd" ))] { + // Pending a tao release on gtk4; muda's gtk API now takes gtk4 types. // menu_bar // .init_for_gtk_window(window.gtk_window(), window.default_vbox()) // .unwrap(); // menu_bar // .init_for_gtk_window(window2.gtk_window(), window2.default_vbox()) // .unwrap(); + let _ = (&menu_bar, &window, &window2); } #[cfg(target_os = "macos")] { @@ -278,14 +280,24 @@ fn main() -> wry::Result<()> { .map(|(x, y)| (x.parse::().unwrap(), y.parse::().unwrap())) .unwrap(); - // #[cfg(target_os = "linux")] - // if let Some(menu_bar) = menu_bar - // .clone() - // .gtk_menubar_for_gtk_window(window.gtk_window()) - // { - // use gtk::prelude::*; - // y += menu_bar.allocated_height(); - // } + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + { + // Pending a tao release on gtk4; muda's gtk API now takes gtk4 types. + // if let Some(menu_bar) = menu_bar + // .clone() + // .gtk_menubar_for_gtk_window(window.gtk_window()) + // { + // use gtk::prelude::*; + // y += menu_bar.allocated_height(); + // } + let _ = &mut y; + } show_context_menu(&window, &file_m_c, Some(Position::Logical((x, y).into()))) } @@ -348,8 +360,18 @@ fn show_context_menu(window: &Window, menu: &dyn ContextMenu, position: Option

Self { + /// Only [`Modifiers::ALT`], [`Modifiers::SHIFT`], [`Modifiers::CONTROL`], and [`Modifiers::SUPER`] + pub fn new(mods: Option, key: Code) -> Self { + let mut mods = mods.unwrap_or_else(Modifiers::empty); if mods.contains(Modifiers::META) { mods.remove(Modifiers::META); mods.insert(Modifiers::SUPER); @@ -86,11 +87,6 @@ impl Accelerator { Self { mods, key, id } } - /// Same as [`Accelerator::new`] but consists of key without a modifier. - pub fn key_only(key: Code) -> Self { - Self::new(Modifiers::empty(), key) - } - fn generate_hash(mods: Modifiers, key: Code) -> u32 { KeyAccelerator::generate_hash(mods, &code_to_key(key)) } @@ -149,7 +145,7 @@ fn parse_accelerator(accelerator: &str) -> Result Result { @@ -422,7 +418,7 @@ fn code_to_key(code: Code) -> Key { /// or converted from an existing [`Accelerator`] /// ```no_run /// # use muda::accelerator::{Accelerator, KeyAccelerator, Code, Modifiers}; -/// let old_accel = Accelerator::new(Modifiers::CONTROL, Code::KeyC); +/// let old_accel = Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC); /// let new_accel: KeyAccelerator = old_accel.into(); /// ``` /// or parsed from a string, which supports literal character keys @@ -769,7 +765,7 @@ fn test_parse_key_accelerator_error() { fn test_equality() { let h1 = parse_accelerator("Shift+KeyR").unwrap(); let h2 = parse_accelerator("Shift+KeyR").unwrap(); - let h3 = Accelerator::new(Modifiers::SHIFT, Code::KeyR); + let h3 = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyR); let h4 = parse_accelerator("Alt+KeyR").unwrap(); let h5 = parse_accelerator("Alt+KeyR").unwrap(); let h6 = parse_accelerator("KeyR").unwrap(); @@ -835,11 +831,11 @@ fn test_parse_key_accelerator() { fn test_key_accelerator_from_accelerator() { let cases = [ ( - Accelerator::new(Modifiers::CONTROL, Code::KeyC), + Accelerator::new(Some(Modifiers::CONTROL), Code::KeyC), Key::Character("c".into()), ), ( - Accelerator::new(Modifiers::SHIFT, Code::ArrowUp), + Accelerator::new(Some(Modifiers::SHIFT), Code::ArrowUp), Key::ArrowUp, ), ]; @@ -868,7 +864,7 @@ fn test_key_accelerator_equality() { assert!(h1.id() == h2.id() && h2.id() == h3.id() && h3.id() != h4.id()); // Converted from Accelerator should match parsed KeyAccelerator - let from_code = Accelerator::new(Modifiers::SHIFT, Code::KeyR); + let from_code = Accelerator::new(Some(Modifiers::SHIFT), Code::KeyR); let from_code: KeyAccelerator = from_code.into(); assert_eq!(from_code, h1); assert_eq!(from_code.id(), h1.id()); diff --git a/src/icon.rs b/src/icon.rs index a5d88850..97d1ff88 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -94,7 +94,7 @@ mod constructors { impl RgbaIcon { pub fn from_rgba(rgba: Vec, width: u32, height: u32) -> Result { - if rgba.len() % PIXEL_SIZE != 0 { + if !rgba.len().is_multiple_of(PIXEL_SIZE) { return Err(BadIcon::ByteCountNotDivisibleBy4 { byte_count: rgba.len(), }); diff --git a/src/items/predefined.rs b/src/items/predefined.rs index 7ab6add4..acb02831 100644 --- a/src/items/predefined.rs +++ b/src/items/predefined.rs @@ -300,34 +300,43 @@ impl PredefinedMenuItemType { 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)), + PredefinedMenuItemType::Copy => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)), + PredefinedMenuItemType::Cut => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)), + PredefinedMenuItemType::Paste => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)), + PredefinedMenuItemType::Undo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)), #[cfg(target_os = "macos")] PredefinedMenuItemType::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)), + PredefinedMenuItemType::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)), + PredefinedMenuItemType::SelectAll => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA)) + } + PredefinedMenuItemType::Minimize => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyM)) + } #[cfg(target_os = "macos")] PredefinedMenuItemType::Fullscreen => Some(Accelerator::new( Some(Modifiers::META | Modifiers::CONTROL), Code::KeyF, )), - PredefinedMenuItemType::Hide => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyH)), - PredefinedMenuItemType::HideOthers => { - Some(Accelerator::new(CMD_OR_CTRL | Modifiers::ALT, Code::KeyH)) - } + PredefinedMenuItemType::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)), + PredefinedMenuItemType::HideOthers => Some(Accelerator::new( + Some(CMD_OR_CTRL | Modifiers::ALT), + Code::KeyH, + )), #[cfg(target_os = "macos")] - PredefinedMenuItemType::CloseWindow => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyW)), + PredefinedMenuItemType::CloseWindow => { + Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyW)) + } #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::CloseWindow => Some(Accelerator::new(Modifiers::ALT, Code::F4)), + PredefinedMenuItemType::CloseWindow => { + Some(Accelerator::new(Some(Modifiers::ALT), Code::F4)) + } #[cfg(target_os = "macos")] - PredefinedMenuItemType::Quit => Some(Accelerator::new(CMD_OR_CTRL, Code::KeyQ)), + PredefinedMenuItemType::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)), _ => None, } } diff --git a/src/items/submenu.rs b/src/items/submenu.rs index 3cd26843..1c98acec 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -279,7 +279,7 @@ impl ContextMenu for Submenu { ))] fn show_context_menu_for_gtk_window( &self, - w: >k4::Window, + w: >k::Window, position: Option, ) -> bool { self.inner @@ -287,6 +287,20 @@ impl ContextMenu for Submenu { .show_context_menu_for_gtk_window(w, position) } + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] + fn gtk_context_menu(&self) -> gtk::PopoverMenu { + self.inner.borrow_mut().gtk_context_menu() + } + #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/lib.rs b/src/lib.rs index e45f0a71..fbb77e4f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,7 +52,7 @@ //! &MenuItem::new( //! "Menu item #1", //! true, -//! Some(Accelerator::new(Modifiers::ALT, Code::KeyD)), +//! Some(Accelerator::new(Some(Modifiers::ALT), Code::KeyD)), //! ), //! &PredefinedMenuItem::separator(), //! &menu_item2, @@ -78,9 +78,9 @@ //! # let menu = muda::Menu::new(); //! # let window_hwnd = 0; //! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -//! # let gtk_window = gtk4::Window::builder().build(); +//! # let gtk_window = gtk::Window::builder().build(); //! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -//! # let vertical_gtk_box = gtk4::Box::new(gtk4::Orientation::Vertical, 0); +//! # let vertical_gtk_box = gtk::Box::new(gtk::Orientation::Vertical, 0); //! // --snip-- //! #[cfg(target_os = "windows")] //! unsafe { menu.init_for_hwnd(window_hwnd) }; @@ -99,7 +99,7 @@ //! # let menu = muda::Menu::new(); //! # let window_hwnd = 0; //! # #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -//! # let gtk_window = gtk4::Window::builder().build(); +//! # let gtk_window = gtk::Window::builder().build(); //! # #[cfg(target_os = "macos")] //! # let nsview = std::ptr::null(); //! // --snip-- @@ -363,7 +363,7 @@ pub trait ContextMenu { #[cfg(target_os = "windows")] unsafe fn detach_menu_subclass_from_hwnd(&self, hwnd: isize); - /// Shows this menu as a context menu inside a [`gtk4::Window`] + /// Shows this menu as a context menu inside a [`gtk::Window`] /// /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. /// @@ -382,10 +382,25 @@ pub trait ContextMenu { ))] fn show_context_menu_for_gtk_window( &self, - w: >k4::Window, + w: >k::Window, position: Option, ) -> bool; + /// Get the underlying gtk menu reserved for context menus. + /// + /// The returned [`gtk::PopoverMenu`] is valid as long as the `ContextMenu` is. + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] + fn gtk_context_menu(&self) -> gtk::PopoverMenu; + /// Shows this menu as a context menu for the specified `NSView`. /// /// - `position` is relative to the window top-left corner, if `None`, the cursor position is used. diff --git a/src/menu.rs b/src/menu.rs index 0d2570de..8ee971e2 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -161,20 +161,20 @@ impl Menu { self.inner.borrow().items() } - /// Adds this menu to a [`gtk4::Window`] + /// Adds this menu to a [`gtk::Window`] /// - /// - `container`: this is an optional parameter to specify a container for the [`gtk4::PopoverMenuBar`], + /// - `container`: this is an optional parameter to specify a container for the [`gtk::PopoverMenuBar`], /// it is highly recommended to pass a container, otherwise the menubar will be added directly to the window, /// which is usually not the desired behavior. - /// Supported types of containers are [`gtk4::Box`], [`gtk4::Stack`] and [`gtk4::Fixed`]: - /// - [`gtk4::Box`], menu bar is add at the beginning using [`gtk4::prelude::BoxExt::prepend`] - /// - [`gtk4::Stack`], menu bar is added using [`gtk4::Stack::add_child`]. - /// - [`gtk4::Fixed`], menu bar is put at 0,0 using [`gtk4::prelude::FixedExt::put`]. + /// Supported types of containers are [`gtk::Box`], [`gtk::Stack`] and [`gtk::Fixed`]: + /// - [`gtk::Box`], menu bar is add at the beginning using [`gtk::prelude::BoxExt::prepend`] + /// - [`gtk::Stack`], menu bar is added using [`gtk::Stack::add_child`]. + /// - [`gtk::Fixed`], menu bar is put at 0,0 using [`gtk::prelude::FixedExt::put`]. /// /// ## Example: /// ```no_run - /// let window = gtk4::Window::builder().build(); - /// let vbox = gtk4::Box::new(gtk4::Orientation::Vertical, 0); + /// let window = gtk::Window::builder().build(); + /// let vbox = gtk::Box::new(gtk::Orientation::Vertical, 0); /// let menu = muda::Menu::new(); /// // -- snip, add your menu items -- /// menu.init_for_gtk_window(&window, Some(&vbox)); @@ -196,9 +196,9 @@ impl Menu { ))] pub fn init_for_gtk_window(&self, window: &W, container: Option<&C>) -> crate::Result<()> where - W: gtk4::prelude::IsA, - W: gtk4::prelude::IsA, - C: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, + C: gtk::prelude::IsA, { self.inner .borrow_mut() @@ -280,7 +280,7 @@ impl Menu { self.inner.borrow_mut().haccel() } - /// Removes this menu from a [`gtk4::Window`] + /// Removes this menu from a [`gtk::Window`] #[cfg(all( any( target_os = "linux", @@ -293,8 +293,8 @@ impl Menu { ))] pub fn remove_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.inner.borrow_mut().remove_for_gtk_window(window) } @@ -309,7 +309,7 @@ impl Menu { self.inner.borrow_mut().remove_for_hwnd(hwnd) } - /// Hides this menu from a [`gtk4::Window`] + /// Hides this menu from a [`gtk::Window`] #[cfg(all( any( target_os = "linux", @@ -322,7 +322,8 @@ impl Menu { ))] pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.inner.borrow_mut().hide_for_gtk_window(window) } @@ -337,7 +338,7 @@ impl Menu { self.inner.borrow().hide_for_hwnd(hwnd) } - /// Shows this menu on a [`gtk4::Window`] + /// Shows this menu on a [`gtk::Window`] #[cfg(all( any( target_os = "linux", @@ -350,7 +351,8 @@ impl Menu { ))] pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.inner.borrow_mut().show_for_gtk_window(window) } @@ -365,7 +367,7 @@ impl Menu { self.inner.borrow().show_for_hwnd(hwnd) } - /// Returns whether this menu visible on a [`gtk4::Window`] + /// Returns whether this menu visible on a [`gtk::Window`] #[cfg(all( any( target_os = "linux", @@ -378,7 +380,8 @@ impl Menu { ))] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { self.inner.borrow().is_visible_on_gtk_window(window) } @@ -393,11 +396,11 @@ impl Menu { ), feature = "gtk" ))] - /// Returns the [`gtk4::MenuBar`] that is associated with this window if it exists. + /// Returns the [`gtk::PopoverMenuBar`] that is associated with this window if it exists. /// This is useful to get information about the menubar for example its height. - pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option + pub fn gtk_menubar_for_gtk_window(self, window: &W) -> Option where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, { self.inner.borrow().gtk_menubar_for_gtk_window(window) } @@ -460,7 +463,7 @@ impl ContextMenu for Menu { ))] fn show_context_menu_for_gtk_window( &self, - window: >k4::Window, + window: >k::Window, position: Option, ) -> bool { self.inner @@ -468,6 +471,20 @@ impl ContextMenu for Menu { .show_context_menu_for_gtk_window(window, position) } + #[cfg(all( + any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ), + feature = "gtk" + ))] + fn gtk_context_menu(&self) -> gtk::PopoverMenu { + self.inner.borrow_mut().gtk_context_menu() + } + #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/platform_impl/gtk/accelerator.rs b/src/platform_impl/gtk/accelerator.rs index 7e608fe8..f4f9f2d2 100644 --- a/src/platform_impl/gtk/accelerator.rs +++ b/src/platform_impl/gtk/accelerator.rs @@ -7,11 +7,13 @@ use keyboard_types::{Key, Modifiers}; use crate::accelerator::KeyAccelerator; impl KeyAccelerator { - /// Builds a GTK accelerator string (e.g. `a`) that can be - /// passed to [`gtk4::Application::set_accels_for_action`]. + /// Render as a GTK accelerator string, e.g. `c`. + /// + /// The result is parseable by `gtk::accelerator_parse` and accepted by + /// `gio::prelude::ActionMapExt::set_accels_for_action`. pub fn to_gtk(&self) -> String { - let mut gtk = modifiers_to_gtk(self.mods); - gtk.push_str(&key_to_gtk(&self.key)); + let mut gtk = modifiers_to_gtk(self.modifiers()); + gtk.push_str(&key_to_gtk(self.key())); gtk } } @@ -19,74 +21,38 @@ impl KeyAccelerator { fn modifiers_to_gtk(mods: Modifiers) -> String { let mut gtk = String::new(); - if mods.shift() { + if mods.contains(Modifiers::SHIFT) { gtk.push_str(""); } - if mods.ctrl() { + if mods.contains(Modifiers::CONTROL) { gtk.push_str(""); } - if mods.alt() { + if mods.contains(Modifiers::ALT) { gtk.push_str(""); } - if mods.contains(Modifiers::SUPER) || mods.meta() { - gtk.push_str(""); + if mods.contains(Modifiers::META) { + gtk.push_str(""); } - gtk } -/// Maps a logical [`Key`] to the matching GDK keysym name understood by -/// `gtk_accelerator_parse`. Printable characters are used verbatim. fn key_to_gtk(key: &Key) -> String { - let name = match key { - Key::Character(c) if c == " " => "space", - Key::Character(c) => return c.clone(), - Key::Escape => "Escape", - Key::Backspace => "BackSpace", - Key::Tab => "Tab", - Key::Enter => "Return", - Key::CapsLock => "Caps_Lock", - Key::F1 => "F1", - Key::F2 => "F2", - Key::F3 => "F3", - Key::F4 => "F4", - Key::F5 => "F5", - Key::F6 => "F6", - Key::F7 => "F7", - Key::F8 => "F8", - Key::F9 => "F9", - Key::F10 => "F10", - Key::F11 => "F11", - Key::F12 => "F12", - Key::F13 => "F13", - Key::F14 => "F14", - Key::F15 => "F15", - Key::F16 => "F16", - Key::F17 => "F17", - Key::F18 => "F18", - Key::F19 => "F19", - Key::F20 => "F20", - Key::F21 => "F21", - Key::F22 => "F22", - Key::F23 => "F23", - Key::F24 => "F24", - Key::PrintScreen => "Print", - Key::ScrollLock => "Scroll_Lock", - Key::Pause => "Pause", - Key::Insert => "Insert", - Key::Delete => "Delete", - Key::Home => "Home", - Key::End => "End", - Key::PageUp => "Page_Up", - Key::PageDown => "Page_Down", - Key::NumLock => "Num_Lock", - Key::ArrowUp => "Up", - Key::ArrowDown => "Down", - Key::ArrowLeft => "Left", - Key::ArrowRight => "Right", - Key::ContextMenu => "Menu", - Key::WakeUp => "WakeUp", - _ => return String::new(), - }; - name.to_string() + match key { + // A single character maps straight to its GDK key name, except + // space, which GDK names rather than prints. + Key::Character(c) if c == " " => "space".to_string(), + Key::Character(c) => c.to_lowercase(), + // Named keys whose GDK name differs from the W3C name. + Key::Enter => "Return".to_string(), + Key::ArrowUp => "Up".to_string(), + Key::ArrowDown => "Down".to_string(), + Key::ArrowLeft => "Left".to_string(), + Key::ArrowRight => "Right".to_string(), + Key::Backspace => "BackSpace".to_string(), + Key::PageUp => "Page_Up".to_string(), + Key::PageDown => "Page_Down".to_string(), + // Escape, Delete, Tab, Home, End, Insert, F-keys, and space + // already match the GDK name that `accelerator_parse` understands. + other => other.to_string(), + } } diff --git a/src/platform_impl/gtk/icon.rs b/src/platform_impl/gtk/icon.rs index 825b8a45..5cdd28d5 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,14 @@ impl PlatformIcon { .map_err(BadIcon::PngEncodingError)?; writer.finish().map_err(BadIcon::PngEncodingError)?; - let bytes = gtk4::glib::Bytes::from_owned(w); - - Ok(Self(gtk4::gio::BytesIcon::new(&bytes))) + Ok(Self { png_data }) } - pub fn bytes_icon(&self) -> >k4::gio::BytesIcon { - &self.0 + /// 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) -> gtk::gio::BytesIcon { + let bytes = gtk::glib::Bytes::from(&self.png_data); + gtk::gio::BytesIcon::new(&bytes) } } 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 d25559ab..050ab016 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -13,7 +13,7 @@ use std::{ }; use dpi::Position; -use gtk4::{gdk::Rectangle, gio, prelude::*}; +use gtk::{gdk::Rectangle, gio, prelude::*}; pub(crate) use icon::PlatformIcon; use mnemonic::to_gtk_mnemonic; @@ -31,45 +31,45 @@ const ACTION_GROUP_DATA_KEY: &str = "mudaActionGroup"; enum GtkMenuBar { MenuBar { - widget: gtk4::PopoverMenuBar, + widget: gtk::PopoverMenuBar, menu: gio::Menu, - app: gtk4::Application, + app: gtk::Application, }, ContextMenu { - widget: gtk4::PopoverMenu, + widget: gtk::PopoverMenu, menu: gio::Menu, - app: gtk4::Application, + app: gtk::Application, }, } impl GtkMenuBar { - fn new(app: gtk4::Application) -> Self { + fn new(app: gtk::Application) -> Self { let menu = gio::Menu::new(); - let widget = gtk4::PopoverMenuBar::from_model(Some(&menu)); + let widget = gtk::PopoverMenuBar::from_model(Some(&menu)); Self::MenuBar { widget, menu, app } } - fn new_context(app: gtk4::Application) -> Self { + fn new_context(app: gtk::Application) -> Self { let menu = gio::Menu::new(); - let widget = gtk4::PopoverMenu::from_model(Some(&menu)); + let widget = gtk::PopoverMenu::from_model(Some(&menu)); Self::ContextMenu { widget, menu, app } } - fn applicaiton(&self) -> >k4::Application { + fn application(&self) -> >k::Application { match self { GtkMenuBar::MenuBar { app, .. } => app, GtkMenuBar::ContextMenu { app, .. } => app, } } - fn menu_bar(&self) -> >k4::PopoverMenuBar { + fn menu_bar(&self) -> >k::PopoverMenuBar { match self { GtkMenuBar::MenuBar { widget, .. } => widget, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), } } - fn context_menu(&self) -> >k4::PopoverMenu { + fn context_menu(&self) -> >k::PopoverMenu { match self { GtkMenuBar::ContextMenu { widget, .. } => widget, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), @@ -112,10 +112,12 @@ 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 +126,40 @@ 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 { @@ -148,9 +175,9 @@ impl Menu { container: Option<&C>, ) -> crate::Result<()> where - W: gtk4::prelude::IsA, - W: gtk4::prelude::IsA, - C: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, + C: gtk::prelude::IsA, { let id = window.as_ptr() as u32; @@ -178,13 +205,13 @@ impl Menu { // add the menubar to the specified widget, otherwise to the window if let Some(container) = container { if container.type_().name() == "GtkBox" { - let gtk_box = container.dynamic_cast_ref::().unwrap(); + let gtk_box = container.dynamic_cast_ref::().unwrap(); gtk_box.prepend(menu_bar); } else if container.type_().name() == "GtkFixed" { - let gtk_box = container.dynamic_cast_ref::().unwrap(); + let gtk_box = container.dynamic_cast_ref::().unwrap(); gtk_box.put(menu_bar, 0., 0.); } else if container.type_().name() == "GtkStack" { - let gtk_box = container.dynamic_cast_ref::().unwrap(); + let gtk_box = container.dynamic_cast_ref::().unwrap(); gtk_box.add_child(menu_bar); } } else { @@ -199,25 +226,31 @@ impl Menu { pub fn remove_for_gtk_window(&mut self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, + W: gtk::prelude::IsA, { 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(()) } pub fn hide_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, { let id = window.as_ptr() as u32; let Some(menu_bar) = self.instances.get(&id) else { @@ -229,7 +262,7 @@ impl Menu { pub fn show_for_gtk_window(&self, window: &W) -> crate::Result<()> where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, { let id = window.as_ptr() as u32; let Some(menu_bar) = self.instances.get(&id) else { @@ -248,7 +281,7 @@ impl Menu { ))] pub fn is_visible_on_gtk_window(&self, window: &W) -> bool where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, { let id = window.as_ptr() as u32; self.instances @@ -257,17 +290,35 @@ impl Menu { .unwrap_or(false) } - pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option + pub fn gtk_menubar_for_gtk_window(&self, window: &W) -> Option where - W: gtk4::prelude::IsA, + W: gtk::prelude::IsA, { let id = window.as_ptr() as u32; self.instances.get(&id).map(|m| m.menu_bar().clone()) } + pub fn gtk_context_menu(&mut self) -> gtk::PopoverMenu { + if self.instances.get(&self.ctx_menu_id).is_none() { + let app = gio::Application::default() + .and_downcast::() + .expect("`gtk_context_menu` requires a running `gtk::Application`"); + let menu = GtkMenuBar::new_context(app); + self.instances.insert(self.ctx_menu_id, menu); + for item in self.items() { + let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); + } + } + self.instances + .get(&self.ctx_menu_id) + .unwrap() + .context_menu() + .clone() + } + pub fn show_context_menu_for_gtk_window( &mut self, - window: >k4::Window, + window: >k::Window, position: Option, ) -> bool { let Some(app) = window.application() else { @@ -312,19 +363,21 @@ impl Menu { enum GtkMenuChild { Item { item: gio::MenuItem, - app: gtk4::Application, + app: gtk::Application, + parent_menu: gio::Menu, }, Submenu { id: u32, item: gio::MenuItem, menu: gio::Menu, - app: gtk4::Application, + app: gtk::Application, + parent_menu: gio::Menu, }, ContextMenu { id: u32, - widget: gtk4::PopoverMenu, + widget: gtk::PopoverMenu, menu: gio::Menu, - app: gtk4::Application, + app: gtk::Application, }, } @@ -337,7 +390,7 @@ impl GtkMenuChild { } } - fn application(&self) -> >k4::Application { + fn application(&self) -> >k::Application { match self { GtkMenuChild::Submenu { app, .. } => app, GtkMenuChild::ContextMenu { app, .. } => app, @@ -345,10 +398,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"), } } @@ -361,7 +414,7 @@ impl GtkMenuChild { } } - fn context_menu(&self) -> >k4::PopoverMenu { + fn context_menu(&self) -> >k::PopoverMenu { match self { GtkMenuChild::ContextMenu { widget, .. } => widget, _ => unreachable!("This is a bug report to https://github.com/tauri-apps/muda"), @@ -379,6 +432,8 @@ pub struct MenuChild { icon: Option, + predefined_item_type: Option, + type_: MenuItemType, instances: HashMap>, @@ -397,6 +452,7 @@ impl MenuChild { checked: false, icon: None, key_accelerator: None, + predefined_item_type: None, type_: MenuItemType::Submenu, ctx_menu_id: COUNTER.next(), instances: HashMap::new(), @@ -407,15 +463,16 @@ impl MenuChild { fn create_gtk_item_for_submenu( &mut self, - app: >k4::Application, + app: >k::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); item.set_detailed_action(&self.detailed_action()); if self.action.is_none() { - let action_group = action_group_from_app(&app); + let action_group = action_group_from_app(app); let action = gio::SimpleAction::new(self.id.as_ref(), None); action.connect_activate(|_, _| ()); @@ -431,6 +488,7 @@ impl MenuChild { menu, id, app: app.clone(), + parent_menu: parent_menu.clone(), }; self.instances.entry(menu_id).or_default().push(child); @@ -450,13 +508,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::Insert(position) => { - gtk_child.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), } } } @@ -467,16 +525,52 @@ 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 { @@ -486,9 +580,36 @@ impl MenuChild { .collect() } + pub fn gtk_context_menu(&mut self) -> gtk::PopoverMenu { + if self.instances.get(&self.ctx_menu_id).is_none() { + let app = gio::Application::default() + .and_downcast::() + .expect("`gtk_context_menu` requires a running `gtk::Application`"); + let menu = gio::Menu::new(); + let widget = gtk::PopoverMenu::from_model(Some(&menu)); + let menu = GtkMenuChild::ContextMenu { + id: self.ctx_menu_id, + widget, + menu, + app, + }; + self.instances.insert(self.ctx_menu_id, vec![menu]); + for item in self.items() { + let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); + } + } + self.instances + .get(&self.ctx_menu_id) + .unwrap() + .first() + .unwrap() + .context_menu() + .clone() + } + pub fn show_context_menu_for_gtk_window( &mut self, - window: >k4::Window, + window: >k::Window, position: Option, ) -> bool { let Some(app) = window.application() else { @@ -497,7 +618,7 @@ impl MenuChild { if self.instances.get(&self.ctx_menu_id).is_none() { let menu = gio::Menu::new(); - let widget = gtk4::PopoverMenu::from_model(Some(&menu)); + let widget = gtk::PopoverMenu::from_model(Some(&menu)); let action_group = action_group_from_app(&app); window.insert_action_group(DEFAULT_ACTION_GROUP, Some(&action_group)); @@ -553,6 +674,7 @@ impl MenuChild { key_accelerator, icon: None, checked: false, + predefined_item_type: None, type_: MenuItemType::MenuItem, ctx_menu_id: 0, instances: HashMap::new(), @@ -563,8 +685,9 @@ impl MenuChild { fn create_gtk_item_for_menu_item( &mut self, - app: >k4::Application, + app: >k::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)); @@ -574,7 +697,7 @@ impl MenuChild { } if self.action.is_none() { - let action_group = action_group_from_app(&app); + let action_group = action_group_from_app(app); let action = gio::SimpleAction::new(self.id.as_ref(), None); let id = self.id.clone(); @@ -588,6 +711,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); @@ -610,8 +734,56 @@ impl MenuChild { self.text.clone() } - pub fn set_text(&self, text: &str) { - todo!() + 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, "action", None) { + if let Some(action_str) = action.str() { + if action_str == detailed_action { + position = Some(i); + 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 { @@ -628,18 +800,18 @@ impl MenuChild { pub fn set_key_accelerator( &mut self, - key_accelerator: Option, + accelerator: Option, ) -> crate::Result<()> { + self.key_accelerator = accelerator.clone(); + let detailed_action = self.detailed_action(); - let accelerator = key_accelerator.as_ref().map(|a| a.to_gtk()); + let accelerator = accelerator.map(|a| a.to_gtk()); let accelerator = accelerator.as_deref().map(|a| [a]).unwrap_or_default(); for item in self.instances.values().flat_map(|v| v.iter()) { let app = item.application(); app.set_accels_for_action(&detailed_action, accelerator.as_slice()); } - self.key_accelerator = key_accelerator; - Ok(()) } } @@ -650,9 +822,10 @@ impl MenuChild { id: MenuId(COUNTER.next().to_string()), text: text.unwrap_or_else(|| item_type.text().to_string()), enabled: true, - key_accelerator: None, + key_accelerator: item_type.accelerator().map(KeyAccelerator::from), icon: None, checked: false, + predefined_item_type: Some(item_type), type_: MenuItemType::Predefined, ctx_menu_id: 0, instances: HashMap::new(), @@ -677,6 +850,7 @@ impl MenuChild { key_accelerator, icon: None, checked, + predefined_item_type: None, type_: MenuItemType::Check, ctx_menu_id: 0, instances: HashMap::new(), @@ -687,8 +861,9 @@ impl MenuChild { fn create_gtk_item_for_check_menu_item( &mut self, - app: >k4::Application, + app: >k::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)); @@ -698,7 +873,7 @@ impl MenuChild { } if self.action.is_none() { - let action_group = action_group_from_app(&app); + let action_group = action_group_from_app(app); let state = &self.checked.to_variant(); let action = gio::SimpleAction::new_stateful(self.id.as_ref(), None, state); @@ -713,6 +888,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); @@ -751,6 +927,7 @@ impl MenuChild { key_accelerator, icon, checked: false, + predefined_item_type: None, type_: MenuItemType::Icon, ctx_menu_id: 0, instances: HashMap::new(), @@ -762,7 +939,7 @@ impl MenuChild { pub fn new_native_icon( text: &str, enabled: bool, - icon: Option, + _icon: Option, key_accelerator: Option, id: Option, ) -> Self { @@ -773,7 +950,8 @@ impl MenuChild { key_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(), @@ -783,8 +961,9 @@ impl MenuChild { fn create_gtk_item_for_icon_menu_item( &mut self, - app: >k4::Application, + app: >k::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)); @@ -794,11 +973,11 @@ 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() { - let action_group = action_group_from_app(&app); + let action_group = action_group_from_app(app); let action = gio::SimpleAction::new(self.id.as_ref(), None); let id = self.id.clone(); @@ -812,38 +991,263 @@ 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) + } + + fn create_gtk_item_for_predefined_menu_item( + &mut self, + app: >k::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) + PredefinedMenuItemType::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) + PredefinedMenuItemType::Copy => (self.text.clone(), "clipboard.copy"), + PredefinedMenuItemType::Cut => (self.text.clone(), "clipboard.cut"), + PredefinedMenuItemType::Paste => (self.text.clone(), "clipboard.paste"), + PredefinedMenuItemType::SelectAll => (self.text.clone(), "selection.select-all"), + + // Text actions (widget-scoped, work on focused text widgets) + PredefinedMenuItemType::Undo => (self.text.clone(), "text.undo"), + PredefinedMenuItemType::Redo => (self.text.clone(), "text.redo"), + + // Window actions (built-in on GtkWindow) + PredefinedMenuItemType::Minimize => (self.text.clone(), "window.minimize"), + PredefinedMenuItemType::Maximize => (self.text.clone(), "window.toggle-maximized"), + PredefinedMenuItemType::CloseWindow => (self.text.clone(), "window.close"), + + // Fullscreen - no built-in GAction, need custom action + PredefinedMenuItemType::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 + PredefinedMenuItemType::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 = gtk::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) + PredefinedMenuItemType::Quit + | PredefinedMenuItemType::Hide + | PredefinedMenuItemType::HideOthers + | PredefinedMenuItemType::ShowAll + | PredefinedMenuItemType::Services + | PredefinedMenuItemType::BringAllToFront + | PredefinedMenuItemType::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(&self, icon: Option) {} + 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, "action", None) { + if let Some(action_str) = action.str() { + if action_str == detailed_action { + position = Some(i); + 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 + '_ { fn make_gtk_menu_item( &self, - app: >k4::Application, + app: >k::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. -fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { +/// Returns and creates the action group on this application if necessary. +fn action_group_from_app(app: >k::Application) -> gio::SimpleActionGroup { let action_group = unsafe { app.data::(ACTION_GROUP_DATA_KEY) }; let action_group = if let Some(action_group) = action_group { @@ -857,7 +1261,7 @@ fn action_group_from_app(app: >k4::Application) -> gio::SimpleActionGroup { action_group } -fn get_cursor_pos(window: >k4::Window) -> (i32, i32) { +fn get_cursor_pos(window: >k::Window) -> (i32, i32) { WidgetExt::display(window) .default_seat() .and_then(|s| s.pointer()) From 772d599fd4078c74d18d55bc48738b8a37ca4caa Mon Sep 17 00:00:00 2001 From: John Carmack Date: Tue, 7 Jul 2026 13:26:30 -0700 Subject: [PATCH 20/21] fix(gtk): render menu item icons on gtk4 GTK4's PopoverMenu does not render icons set on a gio::MenuItem, which is why menu icons were blank on the gtk4 backend. Mark icon items with a `custom` attribute and bind an icon+label widget (a flat button carrying the item's action) via PopoverMenu/PopoverMenuBar add_child; the binding reaches nested submenu slots too. Context menus rebuild on show so label, icon, and checked-state changes are reflected instead of showing a stale first build. Verified headless (xvfb) with a menu whose icon and label are mutated live: the icon renders and updates, addressing the icon issue tracked in tauri-apps/muda#270. --- src/platform_impl/gtk/mod.rs | 92 +++++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 6 deletions(-) diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 050ab016..ea6f5236 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -82,6 +82,52 @@ impl GtkMenuBar { GtkMenuBar::ContextMenu { menu, .. } => menu, } } + + /// Bind a widget to a `custom` slot in the generated menu. + /// + /// GTK4's `PopoverMenu` does not render icons set on a `gio::MenuItem`. + /// To show one we mark the model item with a `custom` attribute and + /// attach the real icon+label widget here; the binding reaches slots in + /// nested submenus too, so a single call per item is enough. + fn add_custom_child(&self, id: &str, widget: &impl IsA) { + match self { + GtkMenuBar::MenuBar { widget: w, .. } => { + w.add_child(widget, id); + } + GtkMenuBar::ContextMenu { widget: w, .. } => { + w.add_child(widget, id); + } + } + } +} + +/// Build the icon + label row shown in place of a default menu item. +/// +/// A flat `Button` carrying the model item's action, so activating it fires +/// the same `MenuEvent` a normal row would and `PopoverMenu` closes on click. +fn custom_menu_row(icon: &crate::icon::Icon, label: &str, action: &str) -> gtk::Button { + let row = gtk::Box::new(gtk::Orientation::Horizontal, 8); + let image = gtk::Image::from_gicon(&icon.inner.to_bytes_icon()); + image.set_pixel_size(16); + let label = gtk::Label::new(Some(&strip_mnemonic(label))); + label.set_xalign(0.0); + label.set_hexpand(true); + row.append(&image); + row.append(&label); + + let button = gtk::Button::builder() + .child(&row) + .has_frame(false) + .action_name(action) + .build(); + button.add_css_class("model"); + button +} + +fn strip_mnemonic(text: &str) -> String { + text.replace("__", "\u{0}") + .replace('_', "") + .replace('\u{0}', "_") } pub struct Menu { @@ -135,6 +181,21 @@ impl Menu { Ok(()) } + /// Attach the icon+label widgets for every `custom` slot in the tree to + /// the given instance. Run after the instance's model is fully built. + fn bind_custom_children(&self, menu_bar: &GtkMenuBar) { + fn walk(children: &[Rc>], menu_bar: &GtkMenuBar) { + for child in children { + let child = child.borrow(); + if let Some((id, row)) = child.custom_child() { + menu_bar.add_custom_child(&id, &row); + } + walk(&child.children, menu_bar); + } + } + walk(&self.children, menu_bar); + } + pub fn remove(&mut self, item: &dyn IsMenuItem) -> crate::Result<()> { let child = item.child(); let child_id = child.borrow().id().clone(); @@ -200,6 +261,8 @@ impl Menu { self.add_menu_item_with_id(item.as_ref(), id)?; } + self.bind_custom_children(&self.instances[&id]); + let menu_bar = self.instances[&id].menu_bar(); // add the menubar to the specified widget, otherwise to the window @@ -299,7 +362,7 @@ impl Menu { } pub fn gtk_context_menu(&mut self) -> gtk::PopoverMenu { - if self.instances.get(&self.ctx_menu_id).is_none() { + if !self.instances.contains_key(&self.ctx_menu_id) { let app = gio::Application::default() .and_downcast::() .expect("`gtk_context_menu` requires a running `gtk::Application`"); @@ -308,6 +371,7 @@ impl Menu { for item in self.items() { let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); } + self.bind_custom_children(&self.instances[&self.ctx_menu_id]); } self.instances .get(&self.ctx_menu_id) @@ -325,7 +389,11 @@ impl Menu { return false; // TODO: better error }; - if self.instances.get(&self.ctx_menu_id).is_none() { + // Rebuild the context instance every time so it reflects the current + // labels, icons, and checked state rather than a stale first build. + self.instances.remove(&self.ctx_menu_id); + + { let action_group = action_group_from_app(&app); window.insert_action_group(DEFAULT_ACTION_GROUP, Some(&action_group)); @@ -336,6 +404,7 @@ impl Menu { for item in self.items() { let _ = self.add_menu_item_with_id(item.as_ref(), self.ctx_menu_id); } + self.bind_custom_children(&self.instances[&self.ctx_menu_id]); } let (x, y) = match position { @@ -581,7 +650,7 @@ impl MenuChild { } pub fn gtk_context_menu(&mut self) -> gtk::PopoverMenu { - if self.instances.get(&self.ctx_menu_id).is_none() { + if !self.instances.contains_key(&self.ctx_menu_id) { let app = gio::Application::default() .and_downcast::() .expect("`gtk_context_menu` requires a running `gtk::Application`"); @@ -616,7 +685,7 @@ impl MenuChild { return false; // TODO: better error }; - if self.instances.get(&self.ctx_menu_id).is_none() { + if !self.instances.contains_key(&self.ctx_menu_id) { let menu = gio::Menu::new(); let widget = gtk::PopoverMenu::from_model(Some(&menu)); @@ -972,8 +1041,10 @@ impl MenuChild { app.set_accels_for_action(&detailed_action, &[&accelerator.to_gtk()]); } - if let Some(icon) = &self.icon { - item.set_icon(&icon.inner.to_bytes_icon()); + // GTK4's PopoverMenu ignores icons set on the model item, so mark + // this row as custom and render the icon ourselves via `add_child`. + if self.icon.is_some() { + item.set_attribute_value("custom", Some(&self.id.as_ref().to_variant())); } if self.action.is_none() { @@ -998,6 +1069,15 @@ impl MenuChild { Ok(item) } + /// The `custom` slot id and rendered row for an icon item, if any. + fn custom_child(&self) -> Option<(String, gtk::Button)> { + let icon = self.icon.as_ref()?; + Some(( + self.id.as_ref().to_string(), + custom_menu_row(icon, &self.text, &self.detailed_action()), + )) + } + fn create_gtk_item_for_predefined_menu_item( &mut self, app: >k::Application, From 802425b40d3f276848a45ac182abbb5bb5cfd81f Mon Sep 17 00:00:00 2001 From: John Carmack Date: Tue, 7 Jul 2026 13:26:30 -0700 Subject: [PATCH 21/21] ci: install gtk4 and pin the msrv toolchain to 1.92 The runners need libgtk-4-dev next to the existing gtk3 packages, which stay for the tao and wry dev-dependencies. The test job's pinned toolchain moves to 1.92 to match the crate's MSRV, set via the action's toolchain input because dtolnay/rust-toolchain has no 1.92 tag. --- .github/workflows/clippy-fmt.yml | 2 +- .github/workflows/test.yml | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index 6d4b77e7..35cc66e7 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -29,7 +29,7 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libxdo-dev libwebkit2gtk-4.1-dev + sudo apt-get install -y libgtk-3-dev libgtk-4-dev libxdo-dev libwebkit2gtk-4.1-dev - uses: dtolnay/rust-toolchain@stable with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 643bf9a1..ebf3e90d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,11 @@ jobs: if: matrix.platform == 'ubuntu-latest' run: | sudo apt-get update - sudo apt-get install -y libgtk-3-dev libxdo-dev libwebkit2gtk-4.1-dev + sudo apt-get install -y libgtk-3-dev libgtk-4-dev libxdo-dev libwebkit2gtk-4.1-dev - - uses: dtolnay/rust-toolchain@1.73 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.92" - run: cargo build - uses: dtolnay/rust-toolchain@stable