From 3b5bbfe5ebc78b6840ff6d15a2334191d8df0ed7 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Sun, 2 Feb 2025 01:28:56 +0200 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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)