diff --git a/Cargo.lock b/Cargo.lock index 094da987..500064ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,12 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "arg_enum_proc_macro" version = "0.3.4" @@ -1771,6 +1777,7 @@ dependencies = [ name = "muda" version = "0.15.3" dependencies = [ + "arc-swap", "crossbeam-channel", "dpi", "gtk", diff --git a/Cargo.toml b/Cargo.toml index 4440677f..50fe00b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,8 +16,10 @@ default = ["libxdo"] libxdo = ["dep:libxdo"] common-controls-v6 = [] serde = ["dep:serde", "dpi/serde"] +linux-ksni = ["arc-swap"] [dependencies] +arc-swap = { version = "1.7.1", optional = true } crossbeam-channel = "0.5" keyboard-types = "0.7" once_cell = "1" diff --git a/src/icon.rs b/src/icon.rs index 14294538..bcaa8567 100644 --- a/src/icon.rs +++ b/src/icon.rs @@ -163,6 +163,12 @@ impl Icon { let win_icon = PlatformIcon::from_resource(ordinal, size)?; Ok(Icon { inner: win_icon }) } + + /// Convert the icon into a GTK pixbuf object. + #[cfg(target_os = "linux")] + pub fn to_pixbuf(&self) -> gtk::gdk_pixbuf::Pixbuf { + self.inner.to_pixbuf() + } } /// A native Icon to be used for the menu item diff --git a/src/items/check.rs b/src/items/check.rs index 8ab4a056..60ca9e83 100644 --- a/src/items/check.rs +++ b/src/items/check.rs @@ -4,6 +4,12 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind}; /// A check menu item inside a [`Menu`] or [`Submenu`] @@ -12,10 +18,12 @@ use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId /// /// [`Menu`]: crate::Menu /// [`Submenu`]: crate::Submenu -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct CheckMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for CheckMenuItem {} @@ -34,6 +42,19 @@ impl IsMenuItem for CheckMenuItem { } impl CheckMenuItem { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_menu_item( + item: &crate::platform_impl::MenuChild, + ) -> crate::CompatMenuItem { + crate::CompatCheckmarkItem { + id: item.id().0.clone(), + label: super::strip_mnemonic(item.text()), + enabled: item.is_enabled(), + checked: item.is_checked(), + } + .into() + } + /// Create a new check menu item. /// /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic @@ -44,16 +65,22 @@ impl CheckMenuItem { checked: bool, accelerator: Option, ) -> Self { - let item = crate::platform_impl::MenuChild::new_check( + let inner = crate::platform_impl::MenuChild::new_check( text.as_ref(), enabled, checked, accelerator, None, ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(item.id().clone()), - inner: Rc::new(RefCell::new(item)), + id: Rc::new(inner.id().clone()), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -69,15 +96,22 @@ impl CheckMenuItem { accelerator: Option, ) -> Self { let id = id.into(); + let inner = crate::platform_impl::MenuChild::new_check( + text.as_ref(), + enabled, + checked, + accelerator, + Some(id.clone()), + ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new_check( - text.as_ref(), - enabled, - checked, - accelerator, - Some(id), - ))), + id: Rc::new(id), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -95,7 +129,14 @@ impl CheckMenuItem { /// an `&` before a character to assign this character as the mnemonic /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`. pub fn set_text>(&self, text: S) { - self.inner.borrow_mut().set_text(text.as_ref()) + let mut inner = self.inner.borrow_mut(); + inner.set_text(text.as_ref()); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Get whether this check menu item is enabled or not. @@ -105,7 +146,14 @@ impl CheckMenuItem { /// Enable or disable this check menu item. pub fn set_enabled(&self, enabled: bool) { - self.inner.borrow_mut().set_enabled(enabled) + let mut inner = self.inner.borrow_mut(); + inner.set_enabled(enabled); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Set this check menu item accelerator. @@ -120,7 +168,23 @@ impl CheckMenuItem { /// Check or Uncheck this check menu item. pub fn set_checked(&self, checked: bool) { - self.inner.borrow_mut().set_checked(checked) + #[cfg(target_os = "macos")] + { + let inner = self.inner.borrow(); + inner.set_checked(checked); + } + + #[cfg(not(target_os = "macos"))] + { + let mut inner = self.inner.borrow_mut(); + inner.set_checked(checked); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + { + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + crate::send_menu_update(); + } + } } /// Convert this menu item into its menu ID. diff --git a/src/items/compat.rs b/src/items/compat.rs new file mode 100644 index 00000000..28797287 --- /dev/null +++ b/src/items/compat.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; + +use arc_swap::ArcSwap; + +use crate::PredefinedMenuItemKind; + +#[derive(Debug, Clone)] +pub struct CompatStandardItem { + pub id: String, + pub label: String, + pub enabled: bool, + pub icon: Option>, + pub predefined_menu_item_kind: Option, +} + +#[derive(Debug, Clone)] +pub struct CompatCheckmarkItem { + pub id: String, + pub label: String, + pub enabled: bool, + pub checked: bool, +} + +#[derive(Debug, Clone)] +pub struct CompatSubMenuItem { + pub label: String, + pub enabled: bool, + pub submenu: Vec>>, +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone)] +pub enum CompatMenuItem { + Standard(CompatStandardItem), + Checkmark(CompatCheckmarkItem), + SubMenu(CompatSubMenuItem), + Separator, +} + +impl From for CompatMenuItem { + fn from(item: CompatStandardItem) -> Self { + CompatMenuItem::Standard(item) + } +} + +impl From for CompatMenuItem { + fn from(item: CompatCheckmarkItem) -> Self { + CompatMenuItem::Checkmark(item) + } +} + +impl From for CompatMenuItem { + fn from(item: CompatSubMenuItem) -> Self { + CompatMenuItem::SubMenu(item) + } +} + +pub fn strip_mnemonic(text: impl AsRef) -> String { + text.as_ref() + .replace("&&", "[~~]") + .replace('&', "") + .replace("[~~]", "&") +} diff --git a/src/items/icon.rs b/src/items/icon.rs index 907a5492..a9ae2c9b 100644 --- a/src/items/icon.rs +++ b/src/items/icon.rs @@ -4,6 +4,12 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ accelerator::Accelerator, icon::{Icon, NativeIcon}, @@ -16,10 +22,12 @@ use crate::{ /// /// [`Menu`]: crate::Menu /// [`Submenu`]: crate::Submenu -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct IconMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for IconMenuItem {} @@ -38,6 +46,23 @@ impl IsMenuItem for IconMenuItem { } impl IconMenuItem { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_menu_item( + item: &crate::platform_impl::MenuChild, + ) -> crate::CompatMenuItem { + crate::CompatStandardItem { + id: item.id().0.clone(), + label: super::strip_mnemonic(item.text()), + enabled: item.is_enabled(), + icon: item + .icon + .as_ref() + .map(|icon| icon.to_pixbuf().save_to_bufferv("png", &[]).unwrap()), + predefined_menu_item_kind: None, + } + .into() + } + /// Create a new icon menu item. /// /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic @@ -48,16 +73,22 @@ impl IconMenuItem { icon: Option, accelerator: Option, ) -> Self { - let item = crate::platform_impl::MenuChild::new_icon( + let inner = crate::platform_impl::MenuChild::new_icon( text.as_ref(), enabled, icon, accelerator, None, ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(item.id().clone()), - inner: Rc::new(RefCell::new(item)), + id: Rc::new(inner.id().clone()), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -73,15 +104,22 @@ impl IconMenuItem { accelerator: Option, ) -> Self { let id = id.into(); + let inner = crate::platform_impl::MenuChild::new_icon( + text.as_ref(), + enabled, + icon, + accelerator, + Some(id.clone()), + ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new_icon( - text.as_ref(), - enabled, - icon, - accelerator, - Some(id), - ))), + id: Rc::new(id), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -98,16 +136,22 @@ impl IconMenuItem { native_icon: Option, accelerator: Option, ) -> Self { - let item = crate::platform_impl::MenuChild::new_native_icon( + let inner = crate::platform_impl::MenuChild::new_native_icon( text.as_ref(), enabled, native_icon, accelerator, None, ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(item.id().clone()), - inner: Rc::new(RefCell::new(item)), + id: Rc::new(inner.id().clone()), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -126,17 +170,22 @@ impl IconMenuItem { accelerator: Option, ) -> Self { let id = id.into(); + let inner = crate::platform_impl::MenuChild::new_native_icon( + text.as_ref(), + enabled, + native_icon, + accelerator, + Some(id.clone()), + ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new( - crate::platform_impl::MenuChild::new_native_icon( - text.as_ref(), - enabled, - native_icon, - accelerator, - Some(id), - ), - )), + id: Rc::new(id), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -145,26 +194,40 @@ impl IconMenuItem { &self.id } - /// Get the text for this check menu item. + /// Get the text for this icon menu item. pub fn text(&self) -> String { self.inner.borrow().text() } - /// Set the text for this check menu item. `text` could optionally contain + /// Set the text for this icon menu item. `text` could optionally contain /// an `&` before a character to assign this character as the mnemonic - /// for this check menu item. To display a `&` without assigning a mnemenonic, use `&&`. + /// for this icon menu item. To display a `&` without assigning a mnemenonic, use `&&`. pub fn set_text>(&self, text: S) { - self.inner.borrow_mut().set_text(text.as_ref()) + let mut inner = self.inner.borrow_mut(); + inner.set_text(text.as_ref()); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } - /// Get whether this check menu item is enabled or not. + /// Get whether this icon menu item is enabled or not. pub fn is_enabled(&self) -> bool { self.inner.borrow().is_enabled() } - /// Enable or disable this check menu item. + /// Enable or disable this icon menu item. pub fn set_enabled(&self, enabled: bool) { - self.inner.borrow_mut().set_enabled(enabled) + let mut inner = self.inner.borrow_mut(); + inner.set_enabled(enabled); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Set this icon menu item accelerator. @@ -172,9 +235,21 @@ impl IconMenuItem { self.inner.borrow_mut().set_accelerator(accelerator) } + /// Get the icon for this icon menu item. + pub fn icon(&self) -> Option { + self.inner.borrow().icon.clone() + } + /// Change this menu item icon or remove it. pub fn set_icon(&self, icon: Option) { - self.inner.borrow_mut().set_icon(icon) + let mut inner = self.inner.borrow_mut(); + inner.set_icon(icon); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Change this menu item icon to a native image or remove it. @@ -184,7 +259,7 @@ impl IconMenuItem { /// - **Windows / Linux**: Unsupported. pub fn set_native_icon(&self, _icon: Option) { #[cfg(target_os = "macos")] - self.inner.borrow_mut().set_native_icon(_icon) + self.inner.borrow_mut().set_native_icon(_icon); } /// Convert this menu item into its menu ID. diff --git a/src/items/mod.rs b/src/items/mod.rs index 39c9b157..855d67c0 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -8,12 +8,18 @@ mod normal; mod predefined; mod submenu; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +mod compat; + pub use check::*; pub use icon::*; pub use normal::*; pub use predefined::*; pub use submenu::*; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub use compat::*; + #[cfg(test)] mod test { use crate::{CheckMenuItem, IconMenuItem, MenuId, MenuItem, PredefinedMenuItem, Submenu}; diff --git a/src/items/normal.rs b/src/items/normal.rs index 7935e7f6..71e192f8 100644 --- a/src/items/normal.rs +++ b/src/items/normal.rs @@ -1,15 +1,23 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{accelerator::Accelerator, sealed::IsMenuItemBase, IsMenuItem, MenuId, MenuItemKind}; /// A menu item inside a [`Menu`] or [`Submenu`] and contains only text. /// /// [`Menu`]: crate::Menu /// [`Submenu`]: crate::Submenu -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct MenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for MenuItem {} @@ -28,15 +36,35 @@ impl IsMenuItem for MenuItem { } impl MenuItem { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_menu_item( + item: &crate::platform_impl::MenuChild, + ) -> crate::CompatMenuItem { + crate::CompatStandardItem { + id: item.id().0.clone(), + label: super::strip_mnemonic(item.text()), + enabled: item.is_enabled(), + icon: None, + predefined_menu_item_kind: None, + } + .into() + } + /// Create a new menu item. /// /// - `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 inner = crate::platform_impl::MenuChild::new(text.as_ref(), enabled, accelerator, None); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(item.id().clone()), - inner: Rc::new(RefCell::new(item)), + id: Rc::new(inner.id().clone()), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -51,14 +79,21 @@ impl MenuItem { accelerator: Option, ) -> Self { let id = id.into(); + let inner = crate::platform_impl::MenuChild::new( + text.as_ref(), + enabled, + accelerator, + Some(id.clone()), + ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new( - text.as_ref(), - enabled, - accelerator, - Some(id), - ))), + id: Rc::new(id), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -76,7 +111,14 @@ impl MenuItem { /// 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 set_text>(&self, text: S) { - self.inner.borrow_mut().set_text(text.as_ref()) + let mut inner = self.inner.borrow_mut(); + inner.set_text(text.as_ref()); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Get whether this menu item is enabled or not. @@ -86,7 +128,14 @@ impl MenuItem { /// Enable or disable this menu item. pub fn set_enabled(&self, enabled: bool) { - self.inner.borrow_mut().set_enabled(enabled) + let mut inner = self.inner.borrow_mut(); + inner.set_enabled(enabled); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Set this menu item accelerator. diff --git a/src/items/predefined.rs b/src/items/predefined.rs index 8ed4a342..05ed7b0b 100644 --- a/src/items/predefined.rs +++ b/src/items/predefined.rs @@ -4,6 +4,12 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ accelerator::{Accelerator, CMD_OR_CTRL}, sealed::IsMenuItemBase, @@ -12,10 +18,12 @@ use crate::{ use keyboard_types::{Code, Modifiers}; /// A predefined (native) menu item which has a predfined behavior by the OS or by this crate. -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct PredefinedMenuItem { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for PredefinedMenuItem {} @@ -34,29 +42,59 @@ impl IsMenuItem for PredefinedMenuItem { } impl PredefinedMenuItem { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_menu_item( + item: &crate::platform_impl::MenuChild, + ) -> crate::CompatMenuItem { + match &item.predefined_item_kind { + Some(PredefinedMenuItemKind::Separator) => crate::CompatMenuItem::Separator, + Some(predefined_menu_item_kind) => crate::CompatStandardItem { + id: item.id().0.clone(), + label: super::strip_mnemonic(item.text()), + enabled: true, + icon: None, + predefined_menu_item_kind: Some(predefined_menu_item_kind.clone()), + } + .into(), + _ => crate::CompatStandardItem { + id: item.id().0.clone(), + label: super::strip_mnemonic(item.text()), + enabled: true, + icon: None, + predefined_menu_item_kind: None, + } + .into(), + } + } + + /// The kind of predefined menu item + pub fn predefined_item_kind(&self) -> Option { + self.inner.borrow().predefined_item_kind.clone() + } + /// Separator menu item pub fn separator() -> PredefinedMenuItem { - PredefinedMenuItem::new::<&str>(PredefinedMenuItemType::Separator, None) + PredefinedMenuItem::new::<&str>(PredefinedMenuItemKind::Separator, None) } /// Copy menu item pub fn copy(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Copy, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Copy, text) } /// Cut menu item pub fn cut(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Cut, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Cut, text) } /// Paste menu item pub fn paste(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Paste, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Paste, text) } /// SelectAll menu item pub fn select_all(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::SelectAll, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::SelectAll, text) } /// Undo menu item @@ -65,7 +103,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn undo(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Undo, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Undo, text) } /// Redo menu item /// @@ -73,7 +111,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn redo(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Redo, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Redo, text) } /// Minimize window menu item @@ -82,7 +120,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn minimize(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Minimize, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Minimize, text) } /// Maximize window menu item @@ -91,7 +129,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn maximize(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Maximize, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Maximize, text) } /// Fullscreen menu item @@ -100,7 +138,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn fullscreen(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Fullscreen, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Fullscreen, text) } /// Hide window menu item @@ -109,7 +147,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn hide(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Hide, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Hide, text) } /// Hide other windows menu item @@ -118,7 +156,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn hide_others(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::HideOthers, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::HideOthers, text) } /// Show all app windows menu item @@ -127,7 +165,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn show_all(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::ShowAll, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::ShowAll, text) } /// Close window menu item @@ -136,7 +174,7 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn close_window(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::CloseWindow, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::CloseWindow, text) } /// Quit app menu item @@ -145,12 +183,12 @@ impl PredefinedMenuItem { /// /// - **Linux:** Unsupported. pub fn quit(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Quit, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Quit, text) } /// About app menu item pub fn about(text: Option<&str>, metadata: Option) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::About(metadata), text) + PredefinedMenuItem::new(PredefinedMenuItemKind::About(metadata), text) } /// Services menu item @@ -159,7 +197,7 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn services(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::Services, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::Services, text) } /// 'Bring all to front' menu item @@ -168,17 +206,23 @@ impl PredefinedMenuItem { /// /// - **Windows / Linux:** Unsupported. pub fn bring_all_to_front(text: Option<&str>) -> PredefinedMenuItem { - PredefinedMenuItem::new(PredefinedMenuItemType::BringAllToFront, text) + PredefinedMenuItem::new(PredefinedMenuItemKind::BringAllToFront, text) } - fn new>(item: PredefinedMenuItemType, text: Option) -> Self { - let item = crate::platform_impl::MenuChild::new_predefined( + fn new>(item: PredefinedMenuItemKind, text: Option) -> Self { + let inner = crate::platform_impl::MenuChild::new_predefined( item, text.map(|t| t.as_ref().to_string()), ); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&inner); + Self { - id: Rc::new(item.id().clone()), - inner: Rc::new(RefCell::new(item)), + id: Rc::new(inner.id().clone()), + inner: Rc::new(RefCell::new(inner)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -194,7 +238,14 @@ impl PredefinedMenuItem { /// Set the text for this predefined menu item. pub fn set_text>(&self, text: S) { - self.inner.borrow_mut().set_text(text.as_ref()) + let mut inner = self.inner.borrow_mut(); + inner.set_text(text.as_ref()); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Convert this menu item into its menu ID. @@ -241,7 +292,7 @@ fn test_about_metadata() { #[derive(Debug, Clone)] #[non_exhaustive] #[allow(clippy::large_enum_variant)] -pub(crate) enum PredefinedMenuItemType { +pub enum PredefinedMenuItemKind { Separator, Copy, Cut, @@ -263,85 +314,85 @@ pub(crate) enum PredefinedMenuItemType { None, } -impl Default for PredefinedMenuItemType { +impl Default for PredefinedMenuItemKind { fn default() -> Self { Self::None } } -impl PredefinedMenuItemType { +impl PredefinedMenuItemKind { pub(crate) fn text(&self) -> &str { match self { - PredefinedMenuItemType::Separator => "", - PredefinedMenuItemType::Copy => "&Copy", - PredefinedMenuItemType::Cut => "Cu&t", - PredefinedMenuItemType::Paste => "&Paste", - PredefinedMenuItemType::SelectAll => "Select &All", - PredefinedMenuItemType::Undo => "Undo", - PredefinedMenuItemType::Redo => "Redo", - PredefinedMenuItemType::Minimize => "&Minimize", + PredefinedMenuItemKind::Separator => "", + PredefinedMenuItemKind::Copy => "&Copy", + PredefinedMenuItemKind::Cut => "Cu&t", + PredefinedMenuItemKind::Paste => "&Paste", + PredefinedMenuItemKind::SelectAll => "Select &All", + PredefinedMenuItemKind::Undo => "Undo", + PredefinedMenuItemKind::Redo => "Redo", + PredefinedMenuItemKind::Minimize => "&Minimize", #[cfg(target_os = "macos")] - PredefinedMenuItemType::Maximize => "Zoom", + PredefinedMenuItemKind::Maximize => "Zoom", #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::Maximize => "Ma&ximize", - PredefinedMenuItemType::Fullscreen => "Toggle Full Screen", - PredefinedMenuItemType::Hide => "&Hide", - PredefinedMenuItemType::HideOthers => "Hide Others", - PredefinedMenuItemType::ShowAll => "Show All", + PredefinedMenuItemKind::Maximize => "Ma&ximize", + PredefinedMenuItemKind::Fullscreen => "Toggle Full Screen", + PredefinedMenuItemKind::Hide => "&Hide", + PredefinedMenuItemKind::HideOthers => "Hide Others", + PredefinedMenuItemKind::ShowAll => "Show All", #[cfg(windows)] - PredefinedMenuItemType::CloseWindow => "Close", + PredefinedMenuItemKind::CloseWindow => "Close", #[cfg(not(windows))] - PredefinedMenuItemType::CloseWindow => "C&lose Window", + PredefinedMenuItemKind::CloseWindow => "C&lose Window", #[cfg(windows)] - PredefinedMenuItemType::Quit => "&Exit", + PredefinedMenuItemKind::Quit => "&Exit", #[cfg(not(windows))] - PredefinedMenuItemType::Quit => "&Quit", - PredefinedMenuItemType::About(_) => "&About", - PredefinedMenuItemType::Services => "Services", - PredefinedMenuItemType::BringAllToFront => "Bring All to Front", - PredefinedMenuItemType::None => "", + PredefinedMenuItemKind::Quit => "&Quit", + PredefinedMenuItemKind::About(_) => "&About", + PredefinedMenuItemKind::Services => "Services", + PredefinedMenuItemKind::BringAllToFront => "Bring All to Front", + PredefinedMenuItemKind::None => "", } } 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)), + PredefinedMenuItemKind::Copy => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyC)), + PredefinedMenuItemKind::Cut => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyX)), + PredefinedMenuItemKind::Paste => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyV)), + PredefinedMenuItemKind::Undo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyZ)), #[cfg(target_os = "macos")] - PredefinedMenuItemType::Redo => Some(Accelerator::new( + PredefinedMenuItemKind::Redo => Some(Accelerator::new( Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyZ, )), #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)), - PredefinedMenuItemType::SelectAll => { + PredefinedMenuItemKind::Redo => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyY)), + PredefinedMenuItemKind::SelectAll => { Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyA)) } - PredefinedMenuItemType::Minimize => { + PredefinedMenuItemKind::Minimize => { Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyM)) } #[cfg(target_os = "macos")] - PredefinedMenuItemType::Fullscreen => Some(Accelerator::new( + PredefinedMenuItemKind::Fullscreen => Some(Accelerator::new( Some(Modifiers::META | Modifiers::CONTROL), Code::KeyF, )), - PredefinedMenuItemType::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)), - PredefinedMenuItemType::HideOthers => Some(Accelerator::new( + PredefinedMenuItemKind::Hide => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyH)), + PredefinedMenuItemKind::HideOthers => Some(Accelerator::new( Some(CMD_OR_CTRL | Modifiers::ALT), Code::KeyH, )), #[cfg(target_os = "macos")] - PredefinedMenuItemType::CloseWindow => { + PredefinedMenuItemKind::CloseWindow => { Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyW)) } #[cfg(not(target_os = "macos"))] - PredefinedMenuItemType::CloseWindow => { + PredefinedMenuItemKind::CloseWindow => { Some(Accelerator::new(Some(Modifiers::ALT), Code::F4)) } #[cfg(target_os = "macos")] - PredefinedMenuItemType::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)), + PredefinedMenuItemKind::Quit => Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyQ)), _ => None, } } diff --git a/src/items/submenu.rs b/src/items/submenu.rs index 3c21c382..21350e8b 100644 --- a/src/items/submenu.rs +++ b/src/items/submenu.rs @@ -4,6 +4,12 @@ use std::{cell::RefCell, mem, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{ dpi::Position, sealed::IsMenuItemBase, util::AddOp, ContextMenu, IsMenuItem, MenuId, MenuItemKind, @@ -12,10 +18,12 @@ use crate::{ /// A menu that can be added to a [`Menu`] or another [`Submenu`]. /// /// [`Menu`]: crate::Menu -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct Submenu { pub(crate) id: Rc, pub(crate) inner: Rc>, + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) compat: Arc>, } impl IsMenuItemBase for Submenu {} @@ -34,15 +42,33 @@ impl IsMenuItem for Submenu { } impl Submenu { + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_menu_item( + item: &crate::platform_impl::MenuChild, + ) -> crate::CompatMenuItem { + crate::CompatSubMenuItem { + label: super::strip_mnemonic(item.text()), + enabled: item.is_enabled(), + submenu: item.compat_items(), + } + .into() + } + /// Create a new submenu. /// /// - `text` could optionally contain an `&` before a character to assign this character as the mnemonic /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`. pub fn new>(text: S, enabled: bool) -> Self { - let submenu = crate::platform_impl::MenuChild::new_submenu(text.as_ref(), enabled, None); + let item = crate::platform_impl::MenuChild::new_submenu(text.as_ref(), enabled, None); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&item); + Self { - id: Rc::new(submenu.id().clone()), - inner: Rc::new(RefCell::new(submenu)), + id: Rc::new(item.id().clone()), + inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -52,14 +78,17 @@ impl Submenu { /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`. pub fn with_id, S: AsRef>(id: I, text: S, enabled: bool) -> Self { let id = id.into(); + let item = + crate::platform_impl::MenuChild::new_submenu(text.as_ref(), enabled, Some(id.clone())); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + let compat = Self::compat_menu_item(&item); Self { - id: Rc::new(id.clone()), - inner: Rc::new(RefCell::new(crate::platform_impl::MenuChild::new_submenu( - text.as_ref(), - enabled, - Some(id), - ))), + id: Rc::new(id), + inner: Rc::new(RefCell::new(item)), + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + compat: Arc::new(ArcSwap::from_pointee(compat)), } } @@ -93,7 +122,16 @@ impl Submenu { /// Add a menu item to the end of this menu. pub fn append(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - self.inner.borrow_mut().add_menu_item(item, AddOp::Append) + let mut inner = self.inner.borrow_mut(); + inner.add_menu_item(item, AddOp::Append)?; + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); + + Ok(()) } /// Add menu items to the end of this submenu. It calls [`Submenu::append`] in a loop. @@ -107,9 +145,16 @@ impl Submenu { /// Add a menu item to the beginning of this submenu. pub fn prepend(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - self.inner - .borrow_mut() - .add_menu_item(item, AddOp::Insert(0)) + let mut inner = self.inner.borrow_mut(); + inner.add_menu_item(item, AddOp::Insert(0))?; + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); + + Ok(()) } /// Add menu items to the beginning of this submenu. @@ -121,9 +166,16 @@ impl Submenu { /// Insert a menu item at the specified `postion` in the submenu. pub fn insert(&self, item: &dyn IsMenuItem, position: usize) -> crate::Result<()> { - self.inner - .borrow_mut() - .add_menu_item(item, AddOp::Insert(position)) + let mut inner = self.inner.borrow_mut(); + inner.add_menu_item(item, AddOp::Insert(position))?; + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); + + Ok(()) } /// Insert menu items at the specified `postion` in the submenu. @@ -137,7 +189,16 @@ impl Submenu { /// Remove a menu item from this submenu. pub fn remove(&self, item: &dyn IsMenuItem) -> crate::Result<()> { - self.inner.borrow_mut().remove(item) + let mut inner = self.inner.borrow_mut(); + inner.remove(item)?; + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); + + Ok(()) } /// Remove the menu item at the specified position from this submenu and returns it. @@ -166,7 +227,14 @@ impl Submenu { /// an `&` before a character to assign this character as the mnemonic /// for this submenu. To display a `&` without assigning a mnemenonic, use `&&`. pub fn set_text>(&self, text: S) { - self.inner.borrow_mut().set_text(text.as_ref()) + let mut inner = self.inner.borrow_mut(); + inner.set_text(text.as_ref()); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Get whether this submenu is enabled or not. @@ -176,7 +244,14 @@ impl Submenu { /// Enable or disable this submenu. pub fn set_enabled(&self, enabled: bool) { - self.inner.borrow_mut().set_enabled(enabled) + let mut inner = self.inner.borrow_mut(); + inner.set_enabled(enabled); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Set this submenu as the Window menu for the application on macOS. @@ -185,7 +260,14 @@ impl Submenu { /// certain other items to the menu. #[cfg(target_os = "macos")] pub fn set_as_windows_menu_for_nsapp(&self) { - self.inner.borrow_mut().set_as_windows_menu_for_nsapp() + let mut inner = self.inner.borrow_mut(); + inner.set_as_windows_menu_for_nsapp(); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Set this submenu as the Help menu for the application on macOS. @@ -196,7 +278,14 @@ impl Submenu { /// which has a title matching the localized word "Help". #[cfg(target_os = "macos")] pub fn set_as_help_menu_for_nsapp(&self) { - self.inner.borrow_mut().set_as_help_menu_for_nsapp() + let mut inner = self.inner.borrow_mut(); + inner.set_as_help_menu_for_nsapp(); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + self.compat.store(Arc::new(Self::compat_menu_item(&inner))); + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + crate::send_menu_update(); } /// Convert this submenu into its menu ID. @@ -249,6 +338,11 @@ impl ContextMenu for Submenu { self.inner.borrow_mut().gtk_context_menu() } + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>> { + self.inner.borrow_mut().compat_items() + } + #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/lib.rs b/src/lib.rs index 25773831..9e9edd12 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,6 +153,14 @@ //! [winit]: https://docs.rs/winit //! [tao]: https://docs.rs/tao +use std::{cell::RefCell, rc::Rc}; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crossbeam_channel::{unbounded, Receiver, Sender}; use once_cell::sync::{Lazy, OnceCell}; @@ -176,9 +184,14 @@ pub use items::*; pub use menu::*; pub use menu_id::MenuId; +#[cfg(target_os = "linux")] +pub use platform_impl::AboutDialog; + +use platform_impl::MenuChild; + /// An enumeration of all available menu types, useful to match against /// the items returned from [`Menu::items`] or [`Submenu::items`] -#[derive(Clone)] +#[derive(Debug, Clone)] pub enum MenuItemKind { MenuItem(MenuItem), Submenu(Submenu), @@ -289,6 +302,17 @@ impl MenuItemKind { MenuItemKind::Icon(i) => i.into_id(), } } + + /// Get the menu items inner value + pub(crate) fn inner(&self) -> Rc> { + match self { + MenuItemKind::MenuItem(i) => i.inner.clone(), + MenuItemKind::Submenu(i) => i.inner.clone(), + MenuItemKind::Predefined(i) => i.inner.clone(), + MenuItemKind::Check(i) => i.inner.clone(), + MenuItemKind::Icon(i) => i.inner.clone(), + } + } } /// A trait that defines a generic item in a menu, which may be one of [`MenuItemKind`] @@ -388,6 +412,10 @@ pub trait ContextMenu { #[cfg(target_os = "linux")] fn gtk_context_menu(&self) -> gtk::Menu; + /// Get all menu items within this context menu. + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>>; + /// 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. @@ -457,7 +485,7 @@ impl MenuEvent { } } - pub(crate) fn send(event: MenuEvent) { + pub fn send(event: MenuEvent) { if let Some(handler) = MENU_EVENT_HANDLER.get_or_init(|| None) { handler(event); } else { @@ -465,3 +493,16 @@ impl MenuEvent { } } } + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +static MENU_UPDATE_CHANNEL: Lazy<(Sender<()>, Receiver<()>)> = Lazy::new(unbounded); + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn recv_menu_update() -> std::result::Result<(), crossbeam_channel::RecvError> { + MENU_UPDATE_CHANNEL.1.recv() +} + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +pub fn send_menu_update() { + let _ = MENU_UPDATE_CHANNEL.0.send(()); +} diff --git a/src/menu.rs b/src/menu.rs index 8d5c79fe..05c872ec 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -4,6 +4,12 @@ use std::{cell::RefCell, rc::Rc}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + use crate::{dpi::Position, util::AddOp, ContextMenu, IsMenuItem, MenuId, MenuItemKind}; /// A root menu that can be added to a Window on Windows and Linux @@ -407,6 +413,11 @@ impl ContextMenu for Menu { self.inner.borrow_mut().gtk_context_menu() } + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + fn compat_items(&self) -> Vec>> { + self.inner.borrow_mut().compat_items() + } + #[cfg(target_os = "macos")] unsafe fn show_context_menu_for_nsview( &self, diff --git a/src/platform_impl/gtk/about_dialog.rs b/src/platform_impl/gtk/about_dialog.rs new file mode 100644 index 00000000..3886f1f7 --- /dev/null +++ b/src/platform_impl/gtk/about_dialog.rs @@ -0,0 +1,56 @@ +use gtk::prelude::*; + +use crate::AboutMetadata; + +/// Displays an about dialog using GTK with the provided metadata. +pub struct AboutDialog { + metadata: AboutMetadata, +} + +impl AboutDialog { + /// Create a new about dialog using `metadata` but without showing it. + pub fn new(metadata: AboutMetadata) -> AboutDialog { + AboutDialog { metadata } + } + + /// Show the about dialog and block until it is closed. + pub fn show(&self) { + let mut builder = gtk::AboutDialog::builder().modal(true).resizable(false); + + if let Some(name) = &self.metadata.name { + builder = builder.program_name(name); + } + if let Some(version) = &self.metadata.full_version() { + builder = builder.version(version); + } + if let Some(authors) = &self.metadata.authors { + builder = builder.authors(authors.clone()); + } + if let Some(comments) = &self.metadata.comments { + builder = builder.comments(comments); + } + if let Some(copyright) = &self.metadata.copyright { + builder = builder.copyright(copyright); + } + if let Some(license) = &self.metadata.license { + builder = builder.license(license); + } + if let Some(website) = &self.metadata.website { + builder = builder.website(website); + } + if let Some(website_label) = &self.metadata.website_label { + builder = builder.website_label(website_label); + } + if let Some(icon) = &self.metadata.icon { + builder = builder.logo(&icon.to_pixbuf()); + } + + let about = builder.build(); + + about.run(); + + unsafe { + about.destroy(); + } + } +} diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index 7a9875ab..75ef60e8 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT +mod about_dialog; mod accelerator; mod icon; +pub use about_dialog::AboutDialog; pub(crate) use icon::PlatformIcon; use crate::{ @@ -17,7 +19,7 @@ use crate::{ }; use accelerator::{from_gtk_mnemonic, parse_accelerator, to_gtk_mnemonic}; use glib::translate::ToGlibPtr; -use gtk::{gdk, glib, prelude::*, AboutDialog, Container, Orientation}; +use gtk::{gdk, glib, prelude::*, Container, Orientation}; use std::{ cell::RefCell, collections::{hash_map::Entry, HashMap}, @@ -25,21 +27,27 @@ use std::{ sync::atomic::{AtomicBool, Ordering}, }; +#[cfg(feature = "linux-ksni")] +use std::sync::Arc; + +#[cfg(feature = "linux-ksni")] +use arc_swap::ArcSwap; + 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 { + let supported = if let Some(predefined_item_kind) = &child_.predefined_item_kind { matches!( - predefined_item_type, - PredefinedMenuItemType::Separator - | PredefinedMenuItemType::Copy - | PredefinedMenuItemType::Cut - | PredefinedMenuItemType::Paste - | PredefinedMenuItemType::SelectAll - | PredefinedMenuItemType::About(_) + predefined_item_kind, + PredefinedMenuItemKind::Separator + | PredefinedMenuItemKind::Copy + | PredefinedMenuItemKind::Cut + | PredefinedMenuItemKind::Paste + | PredefinedMenuItemKind::SelectAll + | PredefinedMenuItemKind::About(_) ) } else { true @@ -59,7 +67,7 @@ macro_rules! return_if_item_not_supported { pub struct Menu { id: MenuId, - children: Vec>>, + children: Vec, // TODO: maybe save a reference to the window? gtk_menubars: HashMap, accel_group: Option, @@ -121,8 +129,8 @@ impl Menu { } match op { - AddOp::Append => self.children.push(item.child()), - AddOp::Insert(position) => self.children.insert(position, item.child()), + AddOp::Append => self.children.push(item.kind()), + AddOp::Insert(position) => self.children.insert(position, item.kind()), } Ok(()) @@ -170,7 +178,7 @@ impl Menu { let index = self .children .iter() - .position(|e| e.borrow().id == item.id()) + .position(|e| e.id() == item.id()) .ok_or(crate::Error::NotAChildOfThisMenu)?; if remove_from_cache { self.children.remove(index) @@ -189,9 +197,8 @@ impl Menu { 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 { + if let MenuItemKind::Submenu(child) = &child { + let mut child_ = child.inner.borrow_mut(); let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned(); if let Some(menus) = menus { for (id, menu) in menus { @@ -206,6 +213,9 @@ impl Menu { child_.gtk_menus.as_mut().unwrap().remove(menu_id); } + let child = child.inner(); + let child_ = child.borrow(); + // 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 { @@ -225,7 +235,8 @@ impl Menu { // 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(); + let child = child.inner(); + let child_ = child.borrow(); if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) { for item in items { menu.remove(&item); @@ -243,9 +254,15 @@ impl Menu { } pub fn items(&self) -> Vec { + self.children.to_vec() + } + + /// Returns a list of menu items that has been added to this menu. + #[cfg(feature = "linux-ksni")] + pub fn compat_items(&self) -> Vec>> { self.children .iter() - .map(|c| c.borrow().kind(c.clone())) + .map(MenuItemKind::compat_child) .collect() } @@ -408,17 +425,17 @@ pub struct MenuChild { gtk_accelerator: Option<(gdk::ModifierType, u32)>, // predefined menu item fields - predefined_item_type: Option, + pub(crate) predefined_item_kind: Option, // check menu item fields checked: Option>, is_syncing_checked_state: Option>, // icon menu item fields - icon: Option, + pub(crate) icon: Option, // submenu fields - pub children: Option>>>, + pub children: Option>, gtk_menus: Option>>, gtk_menu: Option<(u32, Option)>, // dedicated menu for tray or context menus accel_group: Option, @@ -456,9 +473,10 @@ impl Drop for MenuChild { fn drop_children_from_menu_and_destroy( id: u32, menu: &impl IsA, - children: &Vec>>, + children: &[MenuItemKind], ) { for child in children { + let child = child.inner(); let mut child_ = child.borrow_mut(); { let mut menu_items = child_.gtk_menu_items.borrow_mut(); @@ -510,7 +528,7 @@ impl MenuChild { gtk_menus: None, icon: None, is_syncing_checked_state: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -528,20 +546,20 @@ impl MenuChild { gtk_accelerator: None, icon: None, is_syncing_checked_state: None, - predefined_item_type: None, + predefined_item_kind: None, accelerator: None, checked: None, } } - pub(crate) fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub(crate) fn new_predefined(item_type: PredefinedMenuItemKind, 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), + predefined_item_kind: Some(item_type), gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), accel_group: None, checked: None, @@ -576,7 +594,7 @@ impl MenuChild { gtk_menu: None, gtk_menus: None, icon: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -602,7 +620,7 @@ impl MenuChild { gtk_menu: None, gtk_menus: None, is_syncing_checked_state: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -628,7 +646,7 @@ impl MenuChild { gtk_menus: None, icon: None, is_syncing_checked_state: None, - predefined_item_type: None, + predefined_item_kind: None, } } } @@ -807,12 +825,12 @@ impl MenuChild { } match op { - AddOp::Append => self.children.as_mut().unwrap().push(item.child()), + AddOp::Append => self.children.as_mut().unwrap().push(item.kind()), AddOp::Insert(position) => self .children .as_mut() .unwrap() - .insert(position, item.child()), + .insert(position, item.kind()), } Ok(()) @@ -864,7 +882,7 @@ impl MenuChild { .as_ref() .unwrap() .iter() - .position(|e| e.borrow().id == item.id()) + .position(|e| e.id() == item.id()) .ok_or(crate::Error::NotAChildOfThisMenu)?; if remove_from_cache { self.children.as_mut().unwrap().remove(index) @@ -884,9 +902,8 @@ impl MenuChild { 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 { + if let MenuItemKind::Submenu(child) = &child { + let mut child_ = child.inner.borrow_mut(); let menus = child_.gtk_menus.as_ref().unwrap().get(menu_id).cloned(); if let Some(menus) = menus { for (id, menu) in menus { @@ -901,6 +918,9 @@ impl MenuChild { child_.gtk_menus.as_mut().unwrap().remove(menu_id); } + let child = child.inner(); + let child_ = child.borrow(); + // 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 { @@ -921,7 +941,8 @@ impl MenuChild { // 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(); + let child = child.inner(); + let child_ = child.borrow(); if let Some(items) = child_.gtk_menu_items.borrow_mut().remove(id) { for item in items { menu.remove(&item); @@ -940,11 +961,16 @@ impl MenuChild { } pub fn items(&self) -> Vec { + self.children.as_ref().unwrap().to_vec() + } + + #[cfg(feature = "linux-ksni")] + pub fn compat_items(&self) -> Vec>> { self.children .as_ref() .unwrap() .iter() - .map(|c| c.borrow().kind(c.clone())) + .map(MenuItemKind::compat_child) .collect() } @@ -1092,7 +1118,7 @@ impl MenuChild { .as_ref() .map(parse_accelerator) .transpose()?; - let predefined_item_type = self.predefined_item_type.clone().unwrap(); + let predefined_item_kind = self.predefined_item_kind.clone().unwrap(); let make_item = || { gtk::MenuItem::builder() @@ -1115,17 +1141,17 @@ impl MenuChild { } }; - let item = match predefined_item_type { - PredefinedMenuItemType::Separator => { + let item = match predefined_item_kind { + PredefinedMenuItemKind::Separator => { gtk::SeparatorMenuItem::new().upcast::() } - PredefinedMenuItemType::Copy - | PredefinedMenuItemType::Cut - | PredefinedMenuItemType::Paste - | PredefinedMenuItemType::SelectAll => { + PredefinedMenuItemKind::Copy + | PredefinedMenuItemKind::Cut + | PredefinedMenuItemKind::Paste + | PredefinedMenuItemKind::SelectAll => { let item = make_item(); let (mods, key) = - parse_accelerator(&predefined_item_type.accelerator().unwrap()).unwrap(); + parse_accelerator(&predefined_item_kind.accelerator().unwrap()).unwrap(); item.child() .unwrap() .downcast::() @@ -1135,51 +1161,17 @@ impl MenuChild { // TODO: wayland #[cfg(feature = "libxdo")] if let Ok(xdo) = libxdo::XDo::new(None) { - let _ = xdo.send_keysequence(predefined_item_type.xdo_keys(), 0); + let _ = xdo.send_keysequence(predefined_item_kind.xdo_keys(), 0); } }); item } - PredefinedMenuItemType::About(metadata) => { + PredefinedMenuItemKind::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(); - } + if let Some(metadata) = metadata.clone() { + AboutDialog::new(metadata).show(); } }); item @@ -1447,14 +1439,14 @@ fn show_context_menu( false } -impl PredefinedMenuItemType { +impl PredefinedMenuItemKind { #[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", + PredefinedMenuItemKind::Copy => "ctrl+c", + PredefinedMenuItemKind::Cut => "ctrl+X", + PredefinedMenuItemKind::Paste => "ctrl+v", + PredefinedMenuItemKind::SelectAll => "ctrl+a", _ => unreachable!(), } } diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 5c41a735..28170b4d 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -200,13 +200,13 @@ pub struct MenuChild { accelerator: Option, // predefined menu item fields - predefined_item_type: Option, + pub(crate) predefined_item_kind: Option, // check menu item fields checked: Cell, // icon menu item fields - icon: Option, + pub(crate) icon: Option, native_icon: Option, // submenu fields @@ -267,7 +267,7 @@ impl MenuChild { ns_menu: None, ns_menu_items: HashMap::new(), ns_menus: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -295,11 +295,11 @@ impl MenuChild { native_icon: None, ns_menu_items: HashMap::new(), ns_menus: Some(HashMap::new()), - predefined_item_type: None, + predefined_item_kind: None, } } - pub(crate) fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub(crate) fn new_predefined(item_type: PredefinedMenuItemKind, text: Option) -> Self { let text = strip_mnemonic(text.unwrap_or_else(|| { // Gets the app's name from `NSRunningApplication::localizedName`. let app_name = || unsafe { @@ -308,11 +308,11 @@ impl MenuChild { }; match item_type { - PredefinedMenuItemType::About(_) => { + PredefinedMenuItemKind::About(_) => { format!("About {}", app_name()).trim().to_string() } - PredefinedMenuItemType::Hide => format!("Hide {}", app_name()).trim().to_string(), - PredefinedMenuItemType::Quit => format!("Quit {}", app_name()).trim().to_string(), + PredefinedMenuItemKind::Hide => format!("Hide {}", app_name()).trim().to_string(), + PredefinedMenuItemKind::Quit => format!("Quit {}", app_name()).trim().to_string(), _ => item_type.text().to_string(), } })); @@ -324,7 +324,7 @@ impl MenuChild { enabled: true, id: MenuId(COUNTER.next().to_string()), accelerator, - predefined_item_type: Some(item_type), + predefined_item_kind: Some(item_type), checked: Cell::new(false), children: None, icon: None, @@ -355,7 +355,7 @@ impl MenuChild { ns_menu: None, ns_menu_items: HashMap::new(), ns_menus: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -379,7 +379,7 @@ impl MenuChild { ns_menu: None, ns_menu_items: HashMap::new(), ns_menus: None, - predefined_item_type: None, + predefined_item_kind: None, } } @@ -403,7 +403,7 @@ impl MenuChild { ns_menu: None, ns_menu_items: HashMap::new(), ns_menus: None, - predefined_item_type: None, + predefined_item_kind: None, } } } @@ -766,14 +766,14 @@ impl MenuChild { menu_id: u32, ) -> crate::Result> { let mtm = MainThreadMarker::new().expect("can only create menu item on the main thread"); - let item_type = self.predefined_item_type.as_ref().unwrap(); + let item_type = self.predefined_item_kind.as_ref().unwrap(); let ns_menu_item = match item_type { - PredefinedMenuItemType::Separator => NSMenuItem::separatorItem(mtm), + PredefinedMenuItemKind::Separator => NSMenuItem::separatorItem(mtm), _ => { let ns_menu_item = MenuItem::create(mtm, &self.text, item_type.selector(), &self.accelerator)?; - if let PredefinedMenuItemType::About(_) = item_type { + if let PredefinedMenuItemKind::About(_) = item_type { unsafe { ns_menu_item.setTarget(Some(&ns_menu_item)); @@ -789,7 +789,7 @@ impl MenuChild { unsafe { ns_menu_item.setEnabled(self.enabled); - if let PredefinedMenuItemType::Services = item_type { + if let PredefinedMenuItemKind::Services = item_type { // we have to assign an empty menu as the app's services menu, and macOS will populate it let services_menu = NSMenu::new(mtm); NSApplication::sharedApplication(mtm).setServicesMenu(Some(&services_menu)); @@ -883,29 +883,29 @@ impl MenuChild { } } -impl PredefinedMenuItemType { +impl PredefinedMenuItemKind { pub(crate) fn selector(&self) -> Option { match self { - PredefinedMenuItemType::Separator => None, - PredefinedMenuItemType::Copy => Some(sel!(copy:)), - PredefinedMenuItemType::Cut => Some(sel!(cut:)), - PredefinedMenuItemType::Paste => Some(sel!(paste:)), - PredefinedMenuItemType::SelectAll => Some(sel!(selectAll:)), - PredefinedMenuItemType::Undo => Some(sel!(undo:)), - PredefinedMenuItemType::Redo => Some(sel!(redo:)), - PredefinedMenuItemType::Minimize => Some(sel!(performMiniaturize:)), - PredefinedMenuItemType::Maximize => Some(sel!(performZoom:)), - PredefinedMenuItemType::Fullscreen => Some(sel!(toggleFullScreen:)), - PredefinedMenuItemType::Hide => Some(sel!(hide:)), - PredefinedMenuItemType::HideOthers => Some(sel!(hideOtherApplications:)), - PredefinedMenuItemType::ShowAll => Some(sel!(unhideAllApplications:)), - PredefinedMenuItemType::CloseWindow => Some(sel!(performClose:)), - PredefinedMenuItemType::Quit => Some(sel!(terminate:)), + PredefinedMenuItemKind::Separator => None, + PredefinedMenuItemKind::Copy => Some(sel!(copy:)), + PredefinedMenuItemKind::Cut => Some(sel!(cut:)), + PredefinedMenuItemKind::Paste => Some(sel!(paste:)), + PredefinedMenuItemKind::SelectAll => Some(sel!(selectAll:)), + PredefinedMenuItemKind::Undo => Some(sel!(undo:)), + PredefinedMenuItemKind::Redo => Some(sel!(redo:)), + PredefinedMenuItemKind::Minimize => Some(sel!(performMiniaturize:)), + PredefinedMenuItemKind::Maximize => Some(sel!(performZoom:)), + PredefinedMenuItemKind::Fullscreen => Some(sel!(toggleFullScreen:)), + PredefinedMenuItemKind::Hide => Some(sel!(hide:)), + PredefinedMenuItemKind::HideOthers => Some(sel!(hideOtherApplications:)), + PredefinedMenuItemKind::ShowAll => Some(sel!(unhideAllApplications:)), + PredefinedMenuItemKind::CloseWindow => Some(sel!(performClose:)), + PredefinedMenuItemKind::Quit => Some(sel!(terminate:)), // manual implementation in `fire_menu_item_click` - PredefinedMenuItemType::About(_) => Some(sel!(fireMenuItemAction:)), - PredefinedMenuItemType::Services => None, - PredefinedMenuItemType::BringAllToFront => Some(sel!(arrangeInFront:)), - PredefinedMenuItemType::None => None, + PredefinedMenuItemKind::About(_) => Some(sel!(fireMenuItemAction:)), + PredefinedMenuItemKind::Services => None, + PredefinedMenuItemKind::BringAllToFront => Some(sel!(arrangeInFront:)), + PredefinedMenuItemKind::None => None, } } } @@ -973,7 +973,7 @@ impl MenuItem { let item = unsafe { self.ivars().get().as_ref() }.expect("MenuItem's MenuChild pointer was unset"); - if let Some(PredefinedMenuItemType::About(about_meta)) = &item.predefined_item_type { + if let Some(PredefinedMenuItemKind::About(about_meta)) = &item.predefined_item_kind { match about_meta { Some(about_meta) => { let mut keys: Vec<&NSString> = Default::default(); diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index c6abb720..0f68c713 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -12,12 +12,21 @@ mod platform; #[path = "macos/mod.rs"] mod platform; +#[cfg(target_os = "linux")] +pub use platform::AboutDialog; + use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, }; -use crate::{items::*, IsMenuItem, MenuItemKind, MenuItemType}; +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use std::sync::Arc; + +#[cfg(all(feature = "linux-ksni", target_os = "linux"))] +use arc_swap::ArcSwap; + +use crate::{IsMenuItem, MenuItemKind}; pub(crate) use self::platform::*; @@ -35,7 +44,10 @@ impl dyn IsMenuItem + '_ { /// Internal utilities impl MenuChild { + #[cfg(not(target_os = "linux"))] fn kind(&self, c: Rc>) -> MenuItemKind { + use crate::{items::*, MenuItemType}; + match self.item_type() { MenuItemType::Submenu => { let id = c.borrow().id().clone(); @@ -107,4 +119,16 @@ impl MenuItemKind { MenuItemKind::Icon(i) => i.inner.borrow_mut(), } } + + #[cfg(all(feature = "linux-ksni", target_os = "linux"))] + pub(crate) fn compat_child(&self) -> Arc> { + use crate::items::*; + match self { + MenuItemKind::MenuItem(i) => i.compat.clone(), + MenuItemKind::Submenu(i) => i.compat.clone(), + MenuItemKind::Predefined(i) => i.compat.clone(), + MenuItemKind::Check(i) => i.compat.clone(), + MenuItemKind::Icon(i) => i.compat.clone(), + } + } } diff --git a/src/platform_impl/windows/dark_menu_bar.rs b/src/platform_impl/windows/dark_menu_bar.rs index 558a4b1d..1f5b1502 100644 --- a/src/platform_impl/windows/dark_menu_bar.rs +++ b/src/platform_impl/windows/dark_menu_bar.rs @@ -6,7 +6,7 @@ #![allow(non_snake_case, clippy::upper_case_acronyms)] -use std::cell::Cell; +use std::cell::OnceCell; use once_cell::sync::Lazy; use windows_sys::{ @@ -94,7 +94,10 @@ fn background_brush() -> HBRUSH { static BACKGROUND_BRUSH: Win32Brush = const { Win32Brush::null() }; } const BACKGROUND_COLOR: u32 = 2829099; - BACKGROUND_BRUSH.with(|brush| brush.get_or_set(BACKGROUND_COLOR)) + static BACKGROUND_BRUSH: OnceCell = OnceCell::new(); + + let hbrush = BACKGROUND_BRUSH.get_or_init(|| HBrush(CreateSolidBrush(BACKGROUND_COLOR))); + hbrush.as_ref().unwrap().0 } fn selected_background_brush() -> HBRUSH { @@ -102,7 +105,11 @@ fn selected_background_brush() -> HBRUSH { static SELECTED_BACKGROUND_BRUSH: Win32Brush = const { Win32Brush::null() }; } const SELECTED_BACKGROUND_COLOR: u32 = 4276545; - SELECTED_BACKGROUND_BRUSH.with(|brush| brush.get_or_set(SELECTED_BACKGROUND_COLOR)) + static SELECTED_BACKGROUND_BRUSH: OnceCell = OnceCell::new(); + + let hbrush = SELECTED_BACKGROUND_BRUSH + .get_or_init(|| HBrush(CreateSolidBrush(SELECTED_BACKGROUND_COLOR))); + hbrush.as_ref().unwrap().0 } /// Draws a dark menu bar if needed and returns whether it draws it or not diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 67920ce6..18170ed4 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -14,7 +14,7 @@ use crate::{ accelerator::Accelerator, dpi::Position, icon::{Icon, NativeIcon}, - items::PredefinedMenuItemType, + items::PredefinedMenuItemKind, util::{AddOp, Counter}, AboutMetadata, IsMenuItem, MenuEvent, MenuId, MenuItemKind, MenuItemType, MenuTheme, }; @@ -67,9 +67,9 @@ macro_rules! inner_menu_child_and_flags { MenuItemKind::Predefined(i) => { let child = i.inner; let child_ = child.borrow(); - match child_.predefined_item_type.as_ref().unwrap() { - PredefinedMenuItemType::None => return Ok(()), - PredefinedMenuItemType::Separator => { + match child_.predefined_item_kind.as_ref().unwrap() { + PredefinedMenuItemKind::None => return Ok(()), + PredefinedMenuItemKind::Separator => { flags |= MF_SEPARATOR; } _ => { @@ -465,13 +465,13 @@ pub(crate) struct MenuChild { accelerator: Option, // predefined menu item fields - predefined_item_type: Option, + pub(crate) predefined_item_kind: Option, // check menu item fields checked: bool, // icon menu item fields - icon: Option, + pub(crate) icon: Option, // submenu fields hmenu: HMENU, @@ -514,7 +514,7 @@ impl MenuChild { id: id.unwrap_or_else(|| MenuId::new(internal_id.to_string())), accelerator, root_menu_haccel_stores: HashMap::new(), - predefined_item_type: None, + predefined_item_kind: None, icon: None, checked: false, children: None, @@ -536,14 +536,14 @@ impl MenuChild { id: id.unwrap_or_else(|| MenuId::new(internal_id.to_string())), hpopupmenu: unsafe { CreatePopupMenu() }, root_menu_haccel_stores: HashMap::new(), - predefined_item_type: None, + predefined_item_kind: None, icon: None, checked: false, accelerator: None, } } - pub fn new_predefined(item_type: PredefinedMenuItemType, text: Option) -> Self { + pub fn new_predefined(item_type: PredefinedMenuItemKind, text: Option) -> Self { let internal_id = COUNTER.next(); Self { item_type: MenuItemType::Predefined, @@ -553,7 +553,7 @@ impl MenuChild { internal_id, id: MenuId::new(internal_id.to_string()), accelerator: item_type.accelerator(), - predefined_item_type: Some(item_type), + predefined_item_kind: Some(item_type), root_menu_haccel_stores: HashMap::new(), icon: None, checked: false, @@ -581,7 +581,7 @@ impl MenuChild { accelerator, checked, root_menu_haccel_stores: HashMap::new(), - predefined_item_type: None, + predefined_item_kind: None, icon: None, children: None, hmenu: std::ptr::null_mut(), @@ -607,7 +607,7 @@ impl MenuChild { accelerator, icon, root_menu_haccel_stores: HashMap::new(), - predefined_item_type: None, + predefined_item_kind: None, checked: false, children: None, hmenu: std::ptr::null_mut(), @@ -632,7 +632,7 @@ impl MenuChild { id: id.unwrap_or_else(|| MenuId::new(internal_id.to_string())), accelerator, root_menu_haccel_stores: HashMap::new(), - predefined_item_type: None, + predefined_item_kind: None, icon: None, checked: false, children: None, @@ -1194,31 +1194,31 @@ unsafe fn menu_selected(hwnd: windows_sys::Win32::Foundation::HWND, item: &mut M item.set_checked(checked); } MenuItemType::Predefined => { - if let Some(predefined_item_type) = &item.predefined_item_type { - match predefined_item_type { - PredefinedMenuItemType::Copy => execute_edit_command(EditCommand::Copy), - PredefinedMenuItemType::Cut => execute_edit_command(EditCommand::Cut), - PredefinedMenuItemType::Paste => execute_edit_command(EditCommand::Paste), - PredefinedMenuItemType::SelectAll => { + if let Some(predefined_item_kind) = &item.predefined_item_kind { + match predefined_item_kind { + PredefinedMenuItemKind::Copy => execute_edit_command(EditCommand::Copy), + PredefinedMenuItemKind::Cut => execute_edit_command(EditCommand::Cut), + PredefinedMenuItemKind::Paste => execute_edit_command(EditCommand::Paste), + PredefinedMenuItemKind::SelectAll => { execute_edit_command(EditCommand::SelectAll) } - PredefinedMenuItemType::Separator => {} - PredefinedMenuItemType::Minimize => { + PredefinedMenuItemKind::Separator => {} + PredefinedMenuItemKind::Minimize => { ShowWindow(hwnd, SW_MINIMIZE); } - PredefinedMenuItemType::Maximize => { + PredefinedMenuItemKind::Maximize => { ShowWindow(hwnd, SW_MAXIMIZE); } - PredefinedMenuItemType::Hide => { + PredefinedMenuItemKind::Hide => { ShowWindow(hwnd, SW_HIDE); } - PredefinedMenuItemType::CloseWindow => { + PredefinedMenuItemKind::CloseWindow => { SendMessageW(hwnd, WM_CLOSE, 0, 0); } - PredefinedMenuItemType::Quit => { + PredefinedMenuItemKind::Quit => { PostQuitMessage(0); } - PredefinedMenuItemType::About(Some(ref metadata)) => { + PredefinedMenuItemKind::About(Some(ref metadata)) => { show_about_dialog(hwnd as _, metadata) }