From 8d4afb90da26d1a4526c4146c43856f7bff008b3 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Wed, 6 Nov 2024 02:36:33 +0000 Subject: [PATCH] feat(app): add context view method for creating About views --- Cargo.toml | 2 + examples/about/Cargo.toml | 26 ++++++ examples/about/src/main.rs | 165 +++++++++++++++++++++++++++++++++++++ src/app/about.rs | 119 ++++++++++++++++++++++++++ src/app/cosmic.rs | 8 ++ src/app/mod.rs | 93 +++++++++++++++++++++ 6 files changed, 413 insertions(+) create mode 100644 examples/about/Cargo.toml create mode 100644 examples/about/src/main.rs create mode 100644 src/app/about.rs diff --git a/Cargo.toml b/Cargo.toml index 3f225059..3567c2a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ desktop = [ "process", "dep:freedesktop-desktop-entry", "dep:mime", + "dep:open", "dep:shlex", "tokio?/io-util", "tokio?/net", @@ -97,6 +98,7 @@ image = { version = "0.25.1", optional = true } lazy_static = "1.4.0" libc = { version = "0.2.155", 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 new file mode 100644 index 00000000..4dd3aed9 --- /dev/null +++ b/examples/about/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "about" +version = "0.1.0" +edition = "2021" + +[dependencies] +tracing = "0.1.37" +tracing-subscriber = "0.3.17" +tracing-log = "0.2.0" + +[dependencies.libcosmic] +path = "../../" +default-features = false +features = [ + "debug", + "winit", + "tokio", + "xdg-portal", + "dbus-config", + "desktop", + "a11y", + "wayland", + "wgpu", + "single-instance", + "multi-window", +] diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs new file mode 100644 index 00000000..ed061f61 --- /dev/null +++ b/examples/about/src/main.rs @@ -0,0 +1,165 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Application API example + +use cosmic::app::{about::About, Core, Settings, Task}; +use cosmic::iced::widget::column; +use cosmic::iced_core::Size; +use cosmic::widget::{self, nav_bar}; +use cosmic::{executor, iced, ApplicationExt, Element}; + +/// Runs application with these settings +#[rustfmt::skip] +fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + let _ = tracing_log::LogTracer::init(); + + let settings = Settings::default() + .size(Size::new(1024., 768.)); + + cosmic::app::run::(settings, ())?; + + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message { + ToggleAbout, + Cosmic(cosmic::app::cosmic::Message), +} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, + nav_model: nav_bar::Model, + about: About, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "org.cosmic.AboutDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits command on initialize. + fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { + 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())]); + + let mut app = App { + core, + nav_model, + about, + }; + + let command = app.update_title(); + + (app, command) + } + + /// Allows COSMIC to integrate with your application's [`nav_bar::Model`]. + fn nav_model(&self) -> Option<&nav_bar::Model> { + Some(&self.nav_model) + } + + /// Called when a navigation item is selected. + fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { + self.nav_model.activate(id); + self.update_title() + } + + fn context_drawer(&self) -> Option> { + if !self.core.window.show_context { + return None; + } + + if let Some(abuot_view) = self.about_view() { + Some(abuot_view.map(Message::Cosmic)) + } else { + None + } + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::ToggleAbout => { + 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)) + } + } + 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( + column![widget::button::text("Show about").on_press(Message::ToggleAbout)] + .width(iced::Length::Fill) + .height(iced::Length::Shrink) + .align_x(iced::alignment::Horizontal::Center), + ) + .width(iced::Length::Fill) + .height(iced::Length::Shrink) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center); + + Element::from(centered) + } +} + +impl App +where + Self: cosmic::Application, +{ + fn active_page_title(&mut self) -> &str { + self.nav_model + .text(self.nav_model.active()) + .unwrap_or("Unknown Page") + } + + fn update_title(&mut self) -> Task { + let header_title = self.active_page_title().to_owned(); + let window_title = format!("{header_title} — COSMIC AppDemo"); + self.set_header_title(header_title); + if let Some(id) = self.core.main_window_id() { + self.set_window_title(window_title, id) + } else { + Task::none() + } + } +} diff --git a/src/app/about.rs b/src/app/about.rs new file mode 100644 index 00000000..5bb34225 --- /dev/null +++ b/src/app/about.rs @@ -0,0 +1,119 @@ +#[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 a2b229e6..969209ce 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -78,6 +78,9 @@ 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)] @@ -661,6 +664,11 @@ 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 fc90c75b..bdc2433d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,6 +6,9 @@ //! 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; @@ -71,6 +74,14 @@ use { zbus::{interface, proxy, zvariant::Value}, }; +#[cfg(feature = "desktop")] +use { + crate::app::about::About, + crate::widget, + iced::{alignment::Vertical, Alignment}, + std::collections::BTreeMap, +}; + pub(crate) fn iced_settings( settings: Settings, flags: App::Flags, @@ -591,6 +602,88 @@ 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::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