From d8357d0ea3a60b28ed7fa115604b97077f5b62b8 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Sun, 10 Nov 2024 02:42:16 +0100 Subject: [PATCH] refactor: about page as a widget --- Cargo.toml | 4 +- examples/about/Cargo.toml | 1 + examples/about/src/main.rs | 42 ++++---- src/app/about.rs | 119 --------------------- src/app/cosmic.rs | 8 -- src/app/mod.rs | 88 ---------------- src/widget/about.rs | 211 +++++++++++++++++++++++++++++++++++++ src/widget/mod.rs | 6 ++ 8 files changed, 239 insertions(+), 240 deletions(-) delete mode 100644 src/app/about.rs create mode 100644 src/widget/about.rs diff --git a/Cargo.toml b/Cargo.toml index 19cb553..57b59b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ desktop = [ "process", "dep:freedesktop-desktop-entry", "dep:mime", - "dep:open", + "dep:license", "dep:shlex", "tokio?/io-util", "tokio?/net", @@ -98,8 +98,8 @@ fraction = "0.15.3" image = { version = "0.25.1", optional = true } lazy_static = "1.4.0" libc = { version = "0.2.155", optional = true } +license = { version = "3.5.1", optional = true } mime = { version = "0.3.17", optional = true } -open = { version = "5.3.0", optional = true } palette = "0.7.3" rfd = { version = "0.14.0", optional = true } rustix = { version = "0.38.34", features = [ diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index 4dd3aed..76a3579 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" tracing = "0.1.37" tracing-subscriber = "0.3.17" tracing-log = "0.2.0" +open = "5.3.0" [dependencies.libcosmic] path = "../../" diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index ed061f6..6bc4a4f 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -3,10 +3,10 @@ //! Application API example -use cosmic::app::{about::About, Core, Settings, Task}; +use cosmic::app::{Core, Settings, Task}; use cosmic::iced::widget::column; use cosmic::iced_core::Size; -use cosmic::widget::{self, nav_bar}; +use cosmic::widget::{self, about::About, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; /// Runs application with these settings @@ -27,7 +27,7 @@ fn main() -> Result<(), Box> { #[derive(Clone, Debug)] pub enum Message { ToggleAbout, - Cosmic(cosmic::app::cosmic::Message), + Open(String), } /// The [`App`] stores application-specific state. @@ -64,14 +64,17 @@ impl cosmic::Application for App { let nav_model = nav_bar::Model::default(); let about = About::default() - .set_application_name("About Demo") - .set_application_icon(Self::APP_ID) - .set_developer_name("System 76") - .set_license_type("GPL-3.0") - .set_website("https://system76.com/cosmic") - .set_repository_url("https://github.com/pop-os/libcosmic") - .set_support_url("https://github.com/pop-os/libcosmic/issues") - .set_developers([("Michael Murphy".into(), "mmstick@system76.com".into())]); + .name("About Demo") + .icon(Self::APP_ID) + .version("0.1.0") + .author("System 76") + .license("GPL-3.0-only") + .developers([("Michael Murphy", "mmstick@system76.com")]) + .links([ + ("Website", "https://system76.com/cosmic"), + ("Repository", "https://github.com/pop-os/libcosmic"), + ("Support", "https://github.com/pop-os/libcosmic/issues"), + ]); let mut app = App { core, @@ -100,11 +103,7 @@ impl cosmic::Application for App { return None; } - if let Some(abuot_view) = self.about_view() { - Some(abuot_view.map(Message::Cosmic)) - } else { - None - } + Some(widget::about(&self.about, Message::Open)) } /// Handle application events here. @@ -114,17 +113,14 @@ impl cosmic::Application for App { self.core.window.show_context = !self.core.window.show_context; self.core.set_show_context(self.core.window.show_context) } - Message::Cosmic(message) => { - return cosmic::command::message(cosmic::app::Message::Cosmic(message)) - } + Message::Open(url) => match open::that_detached(url) { + Ok(_) => (), + Err(err) => tracing::error!("Failed to open URL: {err}"), + }, } Task::none() } - fn about(&self) -> Option<&About> { - Some(&self.about) - } - /// Creates a view after each update. fn view(&self) -> Element { let centered = cosmic::widget::container( diff --git a/src/app/about.rs b/src/app/about.rs deleted file mode 100644 index 5bb3422..0000000 --- a/src/app/about.rs +++ /dev/null @@ -1,119 +0,0 @@ -#[cfg(feature = "desktop")] -use std::collections::BTreeMap; - -#[cfg(feature = "desktop")] -#[derive(Debug, Default, Clone, derive_setters::Setters)] -#[setters(prefix = "set_", into, strip_option)] -pub struct About { - /// The application's name. - pub application_name: Option, - /// The application's icon name. - pub application_icon: Option, - /// Artists who contributed to the application. - #[setters(skip)] - pub artists: BTreeMap, - /// Comments about the application. - pub comments: Option, - /// The application's copyright. - pub copyright: Option, - /// Designers who contributed to the application. - #[setters(skip)] - pub designers: BTreeMap, - /// Name of the application's developer. - pub developer_name: Option, - /// Developers who contributed to the application. - #[setters(skip)] - pub developers: BTreeMap, - /// Documenters who contributed to the application. - #[setters(skip)] - pub documenters: BTreeMap, - /// The license text. - pub license: Option, - /// The license from a list of known licenses. - pub license_type: Option, - /// The URL of the application’s support page. - #[setters(skip)] - pub support_url: Option, - /// The URL of the application’s repository. - #[setters(skip)] - pub repository_url: Option, - /// Translators who contributed to the application. - #[setters(skip)] - pub translators: BTreeMap, - /// Links associated with the application. - #[setters(skip)] - pub links: BTreeMap, - /// The application’s version. - pub version: Option, - /// The application’s website. - #[setters(skip)] - pub website: Option, -} - -impl About { - pub fn set_repository_url(mut self, repository_url: impl Into) -> Self { - let repository_url = repository_url.into(); - self.repository_url = Some(repository_url.clone()); - self.links.insert("Repository".into(), repository_url); - self - } - - pub fn set_support_url(mut self, support_url: impl Into) -> Self { - let support_url = support_url.into(); - self.support_url = Some(support_url.clone()); - self.links.insert("Support".into(), support_url); - self - } - - pub fn set_website(mut self, website: impl Into) -> Self { - let website = website.into(); - self.website = Some(website.clone()); - self.links.insert("Website".into(), website); - self - } - - pub fn set_artists(mut self, artists: impl Into>) -> Self { - let artists: BTreeMap = artists.into(); - self.artists = artists - .into_iter() - .map(|(k, v)| (k, format!("mailto:{v}"))) - .collect(); - self - } - - pub fn set_designers(mut self, designers: impl Into>) -> Self { - let designers: BTreeMap = designers.into(); - self.designers = designers - .into_iter() - .map(|(k, v)| (k, format!("mailto:{v}"))) - .collect(); - self - } - - pub fn set_developers(mut self, developers: impl Into>) -> Self { - let developers: BTreeMap = developers.into(); - self.developers = developers - .into_iter() - .map(|(k, v)| (k, format!("mailto:{v}"))) - .collect(); - self - } - - pub fn set_documenters(mut self, documenters: impl Into>) -> Self { - let documenters: BTreeMap = documenters.into(); - self.documenters = documenters - .into_iter() - .map(|(k, v)| (k, format!("mailto:{v}"))) - .collect(); - self - } - - pub fn set_translators(mut self, translators: impl Into>) -> Self { - let translators: BTreeMap = translators.into(); - self.translators = translators - .into_iter() - .map(|(k, v)| (k, format!("mailto:{v}"))) - .collect(); - self - } -} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 969209c..a2b229e 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -78,9 +78,6 @@ pub enum Message { /// Tracks updates to window suggested size. #[cfg(feature = "applet")] SuggestedBounds(Option), - #[cfg(feature = "desktop")] - /// Opens the provided URL. - OpenUrl(String), } #[derive(Default)] @@ -664,11 +661,6 @@ impl Cosmic { let core = self.app.core_mut(); core.applet.suggested_bounds = b; } - #[cfg(feature = "desktop")] - Message::OpenUrl(url) => match open::that_detached(url) { - Ok(_) => (), - Err(err) => tracing::error!("{err}"), - }, _ => {} } diff --git a/src/app/mod.rs b/src/app/mod.rs index 12940d7..96a3f5f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,9 +6,6 @@ //! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) //! example in our repository. -#[cfg(feature = "desktop")] -pub mod about; - pub mod command; mod core; pub mod cosmic; @@ -76,7 +73,6 @@ use { #[cfg(feature = "desktop")] use { - crate::app::about::About, crate::widget, iced::{alignment::Vertical, Alignment}, std::collections::BTreeMap, @@ -602,90 +598,6 @@ where panic!("no view for window {id:?}"); } - #[cfg(feature = "desktop")] - /// Provides information about the application. - fn about(&self) -> Option<&About> { - None - } - - #[cfg(feature = "desktop")] - /// Constructs the view for the about section. - fn about_view<'a>(&'a self) -> Option> { - let about = self.about()?; - - let spacing = crate::theme::active().cosmic().spacing; - - let section = |list: &'a BTreeMap, title: &'a str| { - if list.is_empty() { - None - } else { - let developers: Vec> = list - .into_iter() - .map(|(name, url)| { - widget::button::custom( - widget::row() - .push(widget::text(name)) - .push(horizontal_space()) - .push(crate::widget::icon::from_name("link-symbolic").icon()) - .padding(spacing.space_xxs) - .align_y(Vertical::Center), - ) - .class(crate::theme::Button::Text) - .on_press(crate::app::cosmic::Message::OpenUrl(url.clone())) - .width(Length::Fill) - .into() - }) - .collect(); - Some(widget::settings::section().title(title).extend(developers)) - } - }; - - let application_name = about.application_name.as_ref().map(widget::text::title3); - let application_icon = about - .application_icon - .as_ref() - .map(|icon| crate::desktop::IconSource::Name(icon.clone()).as_cosmic_icon()); - - let links_section = section(&about.links, "Links"); - let developers_section = section(&about.developers, "Developers"); - let designers_section = section(&about.designers, "Designers"); - let artists_section = section(&about.artists, "Artists"); - let translators_section = section(&about.translators, "Translators"); - let documenters_section = section(&about.documenters, "Documenters"); - - let developer_name = about.developer_name.as_ref().map(widget::text); - let version = about.version.as_ref().map(widget::button::standard); - let license = about.license_type.as_ref().map(widget::button::standard); - let copyright = about.copyright.as_ref().map(widget::text::body); - let comments = about.comments.as_ref().map(widget::text::body); - - let about = widget::scrollable( - widget::column() - .push_maybe(application_icon) - .push_maybe(application_name) - .push_maybe(developer_name) - .push( - widget::row() - .push_maybe(version) - .push_maybe(license) - .spacing(spacing.space_xs), - ) - .push_maybe(links_section) - .push_maybe(developers_section) - .push_maybe(designers_section) - .push_maybe(artists_section) - .push_maybe(translators_section) - .push_maybe(documenters_section) - .push_maybe(comments) - .push_maybe(copyright) - .align_x(Alignment::Center) - .spacing(spacing.space_xs) - .width(Length::Fill), - ) - .into(); - Some(about) - } - /// Overrides the default style for applications fn style(&self) -> Option { None diff --git a/src/widget/about.rs b/src/widget/about.rs new file mode 100644 index 0000000..8e45833 --- /dev/null +++ b/src/widget/about.rs @@ -0,0 +1,211 @@ +#[cfg(feature = "desktop")] +use { + crate::{ + iced::{alignment::Vertical, Alignment, Length}, + widget::{self, horizontal_space}, + Element, + }, + license::License, +}; + +#[derive(Debug, Default, Clone, derive_setters::Setters)] +#[setters(into, strip_option)] +/// Information about the application. +pub struct About { + /// The application's name. + name: Option, + /// The application's icon name. + icon: Option, + /// The application’s version. + version: Option, + /// Name of the application's author. + author: Option, + /// Comments about the application. + comments: Option, + /// The application's copyright. + copyright: Option, + /// The license name. + license: Option, + /// Artists who contributed to the application. + #[setters(skip)] + artists: Vec<(String, String)>, + /// Designers who contributed to the application. + #[setters(skip)] + designers: Vec<(String, String)>, + /// Developers who contributed to the application. + #[setters(skip)] + developers: Vec<(String, String)>, + /// Documenters who contributed to the application. + #[setters(skip)] + documenters: Vec<(String, String)>, + /// Translators who contributed to the application. + #[setters(skip)] + translators: Vec<(String, String)>, + /// Links associated with the application. + #[setters(skip)] + links: Vec<(String, String)>, +} + +impl<'a> About { + /// Artists who contributed to the application. + pub fn artists(mut self, artists: impl Into>) -> Self { + let artists: Vec<(&'a str, &'a str)> = artists.into(); + self.artists = artists + .into_iter() + .map(|(k, v)| (k.to_string(), format!("mailto:{v}"))) + .collect(); + self + } + + /// Designers who contributed to the application. + pub fn designers(mut self, designers: impl Into>) -> Self { + let designers: Vec<(&'a str, &'a str)> = designers.into(); + self.designers = designers + .into_iter() + .map(|(k, v)| (k.to_string(), format!("mailto:{v}"))) + .collect(); + self + } + + /// Developers who contributed to the application. + pub fn developers(mut self, developers: impl Into>) -> Self { + let developers: Vec<(&'a str, &'a str)> = developers.into(); + self.developers = developers + .into_iter() + .map(|(k, v)| (k.to_string(), format!("mailto:{v}"))) + .collect(); + self + } + + /// Documenters who contributed to the application. + pub fn documenters(mut self, documenters: impl Into>) -> Self { + let documenters: Vec<(&'a str, &'a str)> = documenters.into(); + self.documenters = documenters + .into_iter() + .map(|(k, v)| (k.to_string(), format!("mailto:{v}"))) + .collect(); + self + } + + /// Translators who contributed to the application. + pub fn translators(mut self, translators: impl Into>) -> Self { + let translators: Vec<(&'a str, &'a str)> = translators.into(); + self.translators = translators + .into_iter() + .map(|(k, v)| (k.to_string(), format!("mailto:{v}"))) + .collect(); + self + } + + /// Links associated with the application. + pub fn links>(mut self, links: impl Into>) -> Self { + let links: Vec<(T, &'a str)> = links.into(); + self.links = links + .into_iter() + .map(|(k, v)| (k.into(), v.to_string())) + .collect(); + self + } + + fn license_url(&self) -> Option { + let license: &dyn License = match self.license.as_ref() { + Some(license) => license.parse().ok()?, + None => return None, + }; + + self.license + .as_ref() + .map(|_| format!("https://spdx.org/licenses/{}.html", license.id())) + } +} + +/// Constructs the widget for the about section. +pub fn about<'a, Message: Clone + 'static>( + about: &'a About, + on_url_press: impl Fn(String) -> Message, +) -> Element<'a, Message> { + let spacing = crate::theme::active().cosmic().spacing; + + let section = |list: &'a Vec<(String, String)>, title: &'a str| { + (!list.is_empty()).then_some({ + let developers: Vec> = + list.iter() + .map(|(name, url)| { + widget::button::custom( + widget::row() + .push(widget::text(name)) + .push(horizontal_space()) + .push_maybe((!url.is_empty()).then_some( + crate::widget::icon::from_name("link-symbolic").icon(), + )) + .padding(spacing.space_xxs) + .align_y(Vertical::Center), + ) + .class(crate::theme::Button::Text) + .on_press(on_url_press(url.clone())) + .width(Length::Fill) + .into() + }) + .collect(); + widget::settings::section().title(title).extend(developers) + }) + }; + + let application_name = about.name.as_ref().map(widget::text::title3); + let application_icon = about + .icon + .as_ref() + .map(|icon| crate::desktop::IconSource::Name(icon.clone()).as_cosmic_icon()); + + let links_section = section(&about.links, "Links"); + let developers_section = section(&about.developers, "Developers"); + let designers_section = section(&about.designers, "Designers"); + let artists_section = section(&about.artists, "Artists"); + let translators_section = section(&about.translators, "Translators"); + let documenters_section = section(&about.documenters, "Documenters"); + let author = about.author.as_ref().map(widget::text); + let version = about.version.as_ref().map(widget::button::standard); + let license = about.license.as_ref().map(|license| { + let url = about.license_url(); + widget::settings::section().title("License").add( + widget::button::custom( + widget::row() + .push(widget::text(license)) + .push(horizontal_space()) + .push_maybe( + url.is_some() + .then_some(crate::widget::icon::from_name("link-symbolic").icon()), + ) + .padding(spacing.space_xxs) + .align_y(Vertical::Center), + ) + .class(crate::theme::Button::Text) + .on_press(on_url_press(url.unwrap_or(String::new()))) + .width(Length::Fill), + ) + }); + let copyright = about.copyright.as_ref().map(widget::text::body); + let comments = about.comments.as_ref().map(widget::text::body); + + widget::scrollable( + widget::column() + .push_maybe(application_icon) + .push_maybe(application_name) + .push_maybe(author) + .push_maybe(version) + .push_maybe(license) + .push_maybe(links_section) + .push_maybe(developers_section) + .push_maybe(designers_section) + .push_maybe(artists_section) + .push_maybe(translators_section) + .push_maybe(documenters_section) + .push_maybe(comments) + .push_maybe(copyright) + .align_x(Alignment::Center) + .spacing(spacing.space_xs) + .width(Length::Fill), + ) + .spacing(spacing.space_xxxs) + .into() +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 6c7fcd0..b84e54e 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -365,3 +365,9 @@ pub use warning::*; #[cfg(feature = "markdown")] #[doc(inline)] pub use iced::widget::markdown; + +#[cfg(feature = "desktop")] +pub mod about; +#[cfg(feature = "desktop")] +#[doc(inline)] +pub use about::about;