diff --git a/.changes/about-metadata-builder.md b/.changes/about-metadata-builder.md new file mode 100644 index 0000000..87b680a --- /dev/null +++ b/.changes/about-metadata-builder.md @@ -0,0 +1,5 @@ +--- +"muda": "patch" +--- + +Add `AboutMetadataBuilder`. diff --git a/.changes/builders.md b/.changes/builders.md new file mode 100644 index 0000000..48adc92 --- /dev/null +++ b/.changes/builders.md @@ -0,0 +1,5 @@ +--- +"muda": "patch" +--- + +Add `builders` module with `MenuItemBuilder`, `SubmenuBuilder`, `CheckMenuItemBuilder` and `IconMenuItemBuilder`. diff --git a/.changes/try-from.md b/.changes/try-from.md new file mode 100644 index 0000000..62afaac --- /dev/null +++ b/.changes/try-from.md @@ -0,0 +1,5 @@ +--- +"muda": "patch" +--- + +Impl `TryFrom<&str>` and `TryFrom` for `Accelerator`. diff --git a/src/about_metadata.rs b/src/about_metadata.rs new file mode 100644 index 0000000..7d173b6 --- /dev/null +++ b/src/about_metadata.rs @@ -0,0 +1,176 @@ +use crate::icon::Icon; + +/// Application metadata for the [`PredefinedMenuItem::about`]. +#[derive(Debug, Clone, Default)] +pub struct AboutMetadata { + /// Sets the application name. + pub name: Option, + /// The application version. + pub version: Option, + /// The short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub short_version: Option, + /// The authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub authors: Option>, + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub comments: Option, + /// The copyright of the application. + pub copyright: Option, + /// The license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub license: Option, + /// The application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website: Option, + /// The website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub website_label: Option, + /// The credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub credits: Option, + /// The application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub icon: Option, +} + +impl AboutMetadata { + #[allow(unused)] + pub(crate) fn full_version(&self) -> Option { + Some(format!( + "{}{}", + (self.version.as_ref())?, + (self.short_version.as_ref()) + .map(|v| format!(" ({v})")) + .unwrap_or_default() + )) + } +} + +/// A builder type for [`AboutMetadata`]. +#[derive(Clone, Debug, Default)] +pub struct AboutMetadataBuilder(AboutMetadata); + +impl AboutMetadataBuilder { + pub fn new() -> Self { + Default::default() + } + + /// Sets the application name. + pub fn name>(mut self, name: Option) -> Self { + self.0.name = name.map(|s| s.into()); + self + } + /// Sets the application version. + pub fn version>(mut self, version: Option) -> Self { + self.0.version = version.map(|s| s.into()); + self + } + /// Sets the short version, e.g. "1.0". + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Appended to the end of `version` in parentheses. + pub fn short_version>(mut self, short_version: Option) -> Self { + self.0.short_version = short_version.map(|s| s.into()); + self + } + /// Sets the authors of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn authors(mut self, authors: Option>) -> Self { + self.0.authors = authors; + self + } + /// Application comments. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn comments>(mut self, comments: Option) -> Self { + self.0.comments = comments.map(|s| s.into()); + self + } + /// Sets the copyright of the application. + pub fn copyright>(mut self, copyright: Option) -> Self { + self.0.copyright = copyright.map(|s| s.into()); + self + } + /// Sets the license of the application. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn license>(mut self, license: Option) -> Self { + self.0.license = license.map(|s| s.into()); + self + } + /// Sets the application website. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website>(mut self, website: Option) -> Self { + self.0.website = website.map(|s| s.into()); + self + } + /// Sets the website label. + /// + /// ## Platform-specific + /// + /// - **macOS:** Unsupported. + pub fn website_label>(mut self, website_label: Option) -> Self { + self.0.website_label = website_label.map(|s| s.into()); + self + } + /// Sets the credits. + /// + /// ## Platform-specific + /// + /// - **Windows / Linux:** Unsupported. + pub fn credits>(mut self, credits: Option) -> Self { + self.0.credits = credits.map(|s| s.into()); + self + } + /// Sets the application icon. + /// + /// ## Platform-specific + /// + /// - **Windows:** Unsupported. + pub fn icon(mut self, icon: Option) -> Self { + self.0.icon = icon; + self + } + + /// Construct the final [`AboutMetadata`] + pub fn build(self) -> AboutMetadata { + self.0 + } +} diff --git a/src/accelerator.rs b/src/accelerator.rs index 737214e..57b21bd 100644 --- a/src/accelerator.rs +++ b/src/accelerator.rs @@ -101,6 +101,22 @@ impl FromStr for Accelerator { } } +impl TryFrom<&str> for Accelerator { + type Error = crate::Error; + + fn try_from(value: &str) -> Result { + parse_accelerator(value) + } +} + +impl TryFrom for Accelerator { + type Error = crate::Error; + + fn try_from(value: String) -> Result { + parse_accelerator(&value) + } +} + fn parse_accelerator(accelerator: &str) -> crate::Result { let tokens = accelerator.split('+').collect::>(); diff --git a/src/builders/check.rs b/src/builders/check.rs new file mode 100644 index 0000000..38d0c37 --- /dev/null +++ b/src/builders/check.rs @@ -0,0 +1,57 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{accelerator::Accelerator, CheckMenuItem}; + +/// A builder type for [`CheckMenuItem`] +#[derive(Clone, Debug, Default)] +pub struct CheckMenuItemBuilder { + text: String, + enabled: bool, + checked: bool, + acccelerator: Option, +} + +impl CheckMenuItemBuilder { + pub fn new() -> Self { + Default::default() + } + + /// Set the text for this check menu item. + /// + /// See [`CheckMenuItem::set_text`] for more info. + pub fn text>(mut self, text: S) -> Self { + self.text = text.into(); + self + } + + /// Enable or disable this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Check or uncheck this menu item. + pub fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } + + /// Set this check menu item accelerator. + pub fn acccelerator>( + mut self, + acccelerator: Option, + ) -> crate::Result + where + crate::Error: From<>::Error>, + { + self.acccelerator = acccelerator.map(|a| a.try_into()).transpose()?; + Ok(self) + } + + /// Build this check menu item. + pub fn build(self) -> CheckMenuItem { + CheckMenuItem::new(self.text, self.enabled, self.checked, self.acccelerator) + } +} diff --git a/src/builders/icon.rs b/src/builders/icon.rs new file mode 100644 index 0000000..e8a1dc6 --- /dev/null +++ b/src/builders/icon.rs @@ -0,0 +1,79 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{ + accelerator::Accelerator, + icon::{Icon, NativeIcon}, + IconMenuItem, +}; + +/// A builder type for [`IconMenuItem`] +#[derive(Clone, Debug, Default)] +pub struct IconMenuItemBuilder { + text: String, + enabled: bool, + acccelerator: Option, + icon: Option, + native_icon: Option, +} + +impl IconMenuItemBuilder { + pub fn new() -> Self { + Default::default() + } + + /// Set the text for this icon menu item. + /// + /// See [`IconMenuItem::set_text`] for more info. + pub fn text>(mut self, text: S) -> Self { + self.text = text.into(); + self + } + + /// Enable or disable this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set this icon menu item icon. + pub fn icon(mut self, icon: Option) -> Self { + self.icon = icon; + self.native_icon = None; + self + } + + /// Set this icon menu item native icon. + pub fn native_icon(mut self, icon: Option) -> Self { + self.native_icon = icon; + self.icon = None; + self + } + + /// Set this icon menu item accelerator. + pub fn acccelerator>( + mut self, + acccelerator: Option, + ) -> crate::Result + where + crate::Error: From<>::Error>, + { + self.acccelerator = acccelerator.map(|a| a.try_into()).transpose()?; + Ok(self) + } + + /// Build this icon menu item. + pub fn build(self) -> IconMenuItem { + if self.icon.is_some() { + IconMenuItem::new(self.text, self.enabled, self.icon, self.acccelerator) + } else { + IconMenuItem::with_native_icon( + self.text, + self.enabled, + self.native_icon, + self.acccelerator, + ) + } + } +} diff --git a/src/builders/mod.rs b/src/builders/mod.rs new file mode 100644 index 0000000..5ac8889 --- /dev/null +++ b/src/builders/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! A module containting builder types + +mod check; +mod icon; +mod normal; +mod submenu; + +pub use crate::about_metadata::AboutMetadataBuilder; +pub use check::*; +pub use icon::*; +pub use normal::*; +pub use submenu::*; diff --git a/src/builders/normal.rs b/src/builders/normal.rs new file mode 100644 index 0000000..4fc459e --- /dev/null +++ b/src/builders/normal.rs @@ -0,0 +1,50 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{accelerator::Accelerator, MenuItem}; + +/// A builder type for [`MenuItem`] +#[derive(Clone, Debug, Default)] +pub struct MenuItemBuilder { + text: String, + enabled: bool, + acccelerator: Option, +} + +impl MenuItemBuilder { + pub fn new() -> Self { + Default::default() + } + + /// Set the text for this menu item. + /// + /// See [`MenuItem::set_text`] for more info. + pub fn text>(mut self, text: S) -> Self { + self.text = text.into(); + self + } + + /// Enable or disable this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Set this menu item accelerator. + pub fn acccelerator>( + mut self, + acccelerator: Option, + ) -> crate::Result + where + crate::Error: From<>::Error>, + { + self.acccelerator = acccelerator.map(|a| a.try_into()).transpose()?; + Ok(self) + } + + /// Build this menu item. + pub fn build(self) -> MenuItem { + MenuItem::new(self.text, self.enabled, self.acccelerator) + } +} diff --git a/src/builders/submenu.rs b/src/builders/submenu.rs new file mode 100644 index 0000000..d51e25d --- /dev/null +++ b/src/builders/submenu.rs @@ -0,0 +1,59 @@ +// Copyright 2022-2022 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use crate::{IsMenuItem, Submenu}; + +/// A builder type for [`Submenu`] +#[derive(Clone, Default)] +pub struct SubmenuBuilder<'a> { + text: String, + enabled: bool, + items: Vec<&'a dyn IsMenuItem>, +} + +impl std::fmt::Debug for SubmenuBuilder<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SubmenuBuilder") + .field("text", &self.text) + .field("enabled", &self.enabled) + .finish() + } +} + +impl<'a> SubmenuBuilder<'a> { + pub fn new() -> Self { + Default::default() + } + + /// Set the text for this menu item. + /// + /// See [`Submenu::set_text`] for more info. + pub fn text>(mut self, text: S) -> Self { + self.text = text.into(); + self + } + + /// Enable or disable this menu item. + pub fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + /// Add an item to this submenu. + pub fn item(mut self, item: &'a dyn IsMenuItem) -> Self { + self.items.push(item); + self + } + + /// Add these items to this submenu. + pub fn items(mut self, items: &[&'a dyn IsMenuItem]) -> Self { + self.items.extend_from_slice(items); + self + } + + /// Build this menu item. + pub fn build(self) -> crate::Result { + Submenu::with_items(self.text, self.enabled, &self.items) + } +} diff --git a/src/items/check.rs b/src/items/check.rs index 086d601..b4ac9f7 100644 --- a/src/items/check.rs +++ b/src/items/check.rs @@ -60,7 +60,7 @@ impl CheckMenuItem { self.0.borrow().text() } - /// Get the text for this check menu item. `text` could optionally contain + /// Set the text for this check 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 `&&`. pub fn set_text>(&self, text: S) { diff --git a/src/items/icon.rs b/src/items/icon.rs index ec5c450..187c0d4 100644 --- a/src/items/icon.rs +++ b/src/items/icon.rs @@ -48,6 +48,29 @@ impl IconMenuItem { ))) } + /// Create a new icon menu item but with a native icon. + /// + /// See [`IconMenuItem::new`] for more info. + /// + /// ## Platform-specific: + /// + /// - **Windows / Linux**: Unsupported. + pub fn with_native_icon>( + text: S, + enabled: bool, + native_icon: Option, + acccelerator: Option, + ) -> Self { + Self(Rc::new(RefCell::new( + crate::platform_impl::MenuChild::new_native_icon( + text.as_ref(), + enabled, + native_icon, + acccelerator, + ), + ))) + } + /// Returns a unique identifier associated with this submenu. pub fn id(&self) -> u32 { self.0.borrow().id() @@ -58,7 +81,7 @@ impl IconMenuItem { self.0.borrow().text() } - /// Get the text for this check menu item. `text` could optionally contain + /// Set the text for this check 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 `&&`. pub fn set_text>(&self, text: S) { diff --git a/src/items/normal.rs b/src/items/normal.rs index 6546761..db9e7a3 100644 --- a/src/items/normal.rs +++ b/src/items/normal.rs @@ -41,7 +41,7 @@ impl MenuItem { self.0.borrow().id() } - /// Get the text for this menu item. + /// Set the text for this menu item. pub fn text(&self) -> String { self.0.borrow().text() } diff --git a/src/items/predefined.rs b/src/items/predefined.rs index 7c2fa29..d2b9de9 100644 --- a/src/items/predefined.rs +++ b/src/items/predefined.rs @@ -4,7 +4,7 @@ use std::{cell::RefCell, rc::Rc}; -use crate::{accelerator::Accelerator, icon::Icon, IsMenuItem, MenuItemType}; +use crate::{accelerator::Accelerator, AboutMetadata, IsMenuItem, MenuItemType}; use keyboard_types::{Code, Modifiers}; #[cfg(target_os = "macos")] @@ -182,78 +182,6 @@ impl PredefinedMenuItem { } } -/// Application metadata for the [`PredefinedMenuItem::about`]. -#[derive(Debug, Clone, Default)] -pub struct AboutMetadata { - /// The application name. - pub name: Option, - /// The application version. - pub version: Option, - /// The short version, e.g. "1.0". - /// - /// ## Platform-specific - /// - /// - **Windows / Linux:** Appended to the end of `version` in parentheses. - pub short_version: Option, - /// The authors of the application. - /// - /// ## Platform-specific - /// - /// - **macOS:** Unsupported. - pub authors: Option>, - /// Application comments. - /// - /// ## Platform-specific - /// - /// - **macOS:** Unsupported. - pub comments: Option, - /// The copyright of the application. - pub copyright: Option, - /// The license of the application. - /// - /// ## Platform-specific - /// - /// - **macOS:** Unsupported. - pub license: Option, - /// The application website. - /// - /// ## Platform-specific - /// - /// - **macOS:** Unsupported. - pub website: Option, - /// The website label. - /// - /// ## Platform-specific - /// - /// - **macOS:** Unsupported. - pub website_label: Option, - /// The credits. - /// - /// ## Platform-specific - /// - /// - **Windows / Linux:** Unsupported. - pub credits: Option, - /// The application icon. - /// - /// ## Platform-specific - /// - /// - **Windows:** Unsupported. - pub icon: Option, -} - -impl AboutMetadata { - #[allow(unused)] - pub(crate) fn full_version(&self) -> Option { - Some(format!( - "{}{}", - (self.version.as_ref())?, - (self.short_version.as_ref()) - .map(|v| format!(" ({v})")) - .unwrap_or_default() - )) - } -} - #[test] fn test_about_metadata() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 853f3fc..eacd403 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,7 +128,9 @@ use crossbeam_channel::{unbounded, Receiver, Sender}; use once_cell::sync::{Lazy, OnceCell}; +mod about_metadata; pub mod accelerator; +pub mod builders; mod error; mod items; mod menu; @@ -139,7 +141,8 @@ mod util; #[macro_use] extern crate objc; -pub use self::error::*; +pub use about_metadata::AboutMetadata; +pub use error::*; pub use items::*; pub use menu::Menu; pub mod icon; diff --git a/src/platform_impl/gtk/mod.rs b/src/platform_impl/gtk/mod.rs index e9112ad..0752d9d 100644 --- a/src/platform_impl/gtk/mod.rs +++ b/src/platform_impl/gtk/mod.rs @@ -9,7 +9,7 @@ pub(crate) use icon::PlatformIcon; use crate::{ accelerator::Accelerator, - icon::Icon, + icon::{Icon, NativeIcon}, items::*, util::{AddOp, Counter}, MenuEvent, MenuItemType, @@ -480,6 +480,24 @@ impl MenuChild { ..Default::default() } } + + pub fn new_native_icon( + text: &str, + enabled: bool, + _native_icon: Option, + accelerator: Option, + ) -> Self { + Self { + text: text.to_string(), + enabled, + accelerator, + id: COUNTER.next(), + type_: MenuItemType::Icon, + gtk_menu_items: Rc::new(RefCell::new(HashMap::new())), + is_syncing_checked_state: Rc::new(AtomicBool::new(false)), + ..Default::default() + } + } } /// Shared methods diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 4e95aa5..383ea5f 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -311,6 +311,23 @@ impl MenuChild { ..Default::default() } } + + pub fn new_native_icon( + text: &str, + enabled: bool, + native_icon: Option, + accelerator: Option, + ) -> Self { + Self { + type_: MenuItemType::Icon, + text: text.to_string(), + enabled, + id: COUNTER.next(), + native_icon, + accelerator, + ..Default::default() + } + } } /// Shared methods @@ -695,7 +712,11 @@ impl MenuChild { let () = msg_send![ns_menu_item, setEnabled: NO]; } - menuitem_set_icon(ns_menu_item, self.icon.as_ref()); + if self.icon.is_some() { + menuitem_set_icon(ns_menu_item, self.icon.as_ref()); + } else if self.native_icon.is_some() { + menuitem_set_native_icon(ns_menu_item, self.native_icon); + } } self.ns_menu_items diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 3bc7bd4..88de8d9 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -10,11 +10,11 @@ pub(crate) use self::icon::WinIcon as PlatformIcon; use crate::{ accelerator::Accelerator, - icon::Icon, - items::{AboutMetadata, PredfinedMenuItemType}, + icon::{Icon, NativeIcon}, + items::PredfinedMenuItemType, util::{AddOp, Counter}, - CheckMenuItem, IconMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemType, PredefinedMenuItem, - Submenu, + AboutMetadata, CheckMenuItem, IconMenuItem, IsMenuItem, MenuEvent, MenuItem, MenuItemType, + PredefinedMenuItem, Submenu, }; use std::{ cell::{RefCell, RefMut}, @@ -474,6 +474,24 @@ impl MenuChild { ..Default::default() } } + + pub fn new_native_icon( + text: &str, + enabled: bool, + _native_icon: Option, + accelerator: Option, + ) -> Self { + Self { + type_: MenuItemType::Icon, + text: text.to_string(), + enabled, + parents_hemnu: Vec::new(), + id: COUNTER.next(), + accelerator, + root_menu_haccel_stores: Some(Vec::new()), + ..Default::default() + } + } } /// Shared methods