From c474b3e9554f1c0ea990632544cfeb5ddba24ca9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 8 Aug 2023 18:09:57 -0400 Subject: [PATCH] wip: add applet module --- Cargo.toml | 10 ++ src/app/applet/mod.rs | 292 ++++++++++++++++++++++++++++++++++++++++++ src/app/core.rs | 6 + src/app/cosmic.rs | 11 +- src/app/mod.rs | 7 + 5 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 src/app/applet/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 9e9dcbd..adbbbfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ winit_tokio = ["iced/winit", "iced_winit", "tokio"] winit_wgpu = ["winit", "wgpu"] # Enables XDG portal integrations xdg-portal = ["ashpd"] +applet = ["wayland", "tokio", "a11y", "cosmic-panel-config", "ron"] [dependencies] apply = "0.3.0" @@ -98,6 +99,15 @@ optional = true path = "iced/wgpu" optional = true +[dependencies.cosmic-panel-config] +git = "https://github.com/pop-os/cosmic-panel" +optional = true + +[dependencies.ron] +version = "0.8" +optional = true + + [workspace] members = [ "cosmic-config", diff --git a/src/app/applet/mod.rs b/src/app/applet/mod.rs new file mode 100644 index 0000000..b3cdd1d --- /dev/null +++ b/src/app/applet/mod.rs @@ -0,0 +1,292 @@ +use std::sync::Arc; + +use crate::{ + app::Core, + cosmic_config::CosmicConfigEntry, + cosmic_theme::util::CssColor, + iced::{ + self, + alignment::{Horizontal, Vertical}, + widget::Container, + window, Color, Length, Limits, Rectangle, + }, + iced_style, iced_widget, sctk, + theme::{self, Button, THEME}, + Application, Element, Renderer, +}; +pub use cosmic_panel_config; +use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; +use iced_style::{button::StyleSheet, container::Appearance}; +use iced_widget::runtime::command::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, +}; +use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use tracing::error; + +use super::cosmic; + +const APPLET_PADDING: u32 = 8; + +#[must_use] +pub fn applet_button_theme() -> Button { + Button::Custom { + active: Box::new(|t| iced_style::button::Appearance { + border_radius: 0.0.into(), + ..t.active(&Button::Text) + }), + hover: Box::new(|t| iced_style::button::Appearance { + border_radius: 0.0.into(), + ..t.hovered(&Button::Text) + }), + } +} + +#[derive(Debug, Clone)] +pub struct CosmicAppletHelper { + pub size: Size, + pub anchor: PanelAnchor, + pub background: CosmicPanelBackground, + pub output_name: String, +} + +#[derive(Clone, Debug)] +pub enum Size { + PanelSize(PanelSize), + // (width, height) + Hardcoded((u16, u16)), +} + +impl Default for CosmicAppletHelper { + fn default() -> Self { + Self { + size: Size::PanelSize( + std::env::var("COSMIC_PANEL_SIZE") + .ok() + .and_then(|size| ron::from_str(size.as_str()).ok()) + .unwrap_or(PanelSize::S), + ), + anchor: std::env::var("COSMIC_PANEL_ANCHOR") + .ok() + .and_then(|size| ron::from_str(size.as_str()).ok()) + .unwrap_or(PanelAnchor::Top), + background: std::env::var("COSMIC_PANEL_BACKGROUND") + .ok() + .and_then(|size| ron::from_str(size.as_str()).ok()) + .unwrap_or(CosmicPanelBackground::ThemeDefault), + output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(), + } + } +} + +impl CosmicAppletHelper { + #[must_use] + pub fn suggested_size(&self) -> (u16, u16) { + match &self.size { + Size::PanelSize(size) => match size { + PanelSize::XL => (64, 64), + PanelSize::L => (36, 36), + PanelSize::M => (24, 24), + PanelSize::S => (16, 16), + PanelSize::XS => (12, 12), + }, + Size::Hardcoded((width, height)) => (*width, *height), + } + } + + // Set the default window size. Helper for application init with hardcoded size. + pub fn window_size(&mut self, width: u16, height: u16) { + self.size = Size::Hardcoded((width, height)); + } + + #[must_use] + #[allow(clippy::cast_precision_loss)] + pub fn window_settings(&self) -> super::Settings { + let (width, height) = self.suggested_size(); + let width = u32::from(width); + let height = u32::from(height); + super::Settings::default() + .size((width + APPLET_PADDING * 2, height + APPLET_PADDING * 2)) + .size_limits( + Limits::NONE + .min_height(height as f32 + APPLET_PADDING as f32 * 2.0) + .max_height(height as f32 + APPLET_PADDING as f32 * 2.0) + .min_width(width as f32 + APPLET_PADDING as f32 * 2.0) + .max_width(width as f32 + APPLET_PADDING as f32 * 2.0), + ) + .resizable(None) + .default_text_size(18.0) + .default_font(crate::font::FONT) + .transparent(true) + .theme(self.theme()) + } + + #[must_use] + pub fn icon_button<'a, Message: 'static>( + &self, + icon_name: &'a str, + ) -> iced::widget::Button<'a, Message, Renderer> { + crate::widget::button(theme::Button::Text) + .icon(theme::Svg::Symbolic, icon_name, self.suggested_size().0) + .padding(8) + } + + // TODO popup container which tracks the size of itself and requests the popup to resize to match + pub fn popup_container<'a, Message: 'static>( + &self, + content: impl Into>, + ) -> Container<'a, Message, Renderer> { + let (vertical_align, horizontal_align) = match self.anchor { + PanelAnchor::Left => (Vertical::Center, Horizontal::Left), + PanelAnchor::Right => (Vertical::Center, Horizontal::Right), + PanelAnchor::Top => (Vertical::Top, Horizontal::Center), + PanelAnchor::Bottom => (Vertical::Bottom, Horizontal::Center), + }; + + Container::::new(Container::::new(content).style( + theme::Container::custom(|theme| Appearance { + text_color: Some(theme.cosmic().background.on.into()), + background: Some(Color::from(theme.cosmic().background.base).into()), + border_radius: 12.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }), + )) + .width(Length::Shrink) + .height(Length::Shrink) + .align_x(horizontal_align) + .align_y(vertical_align) + } + + #[must_use] + #[allow(clippy::cast_possible_wrap)] + pub fn get_popup_settings( + &self, + parent: window::Id, + id: window::Id, + size: Option<(u32, u32)>, + width_padding: Option, + height_padding: Option, + ) -> SctkPopupSettings { + let (width, height) = self.suggested_size(); + let pixel_offset = 8; + let (offset, anchor, gravity) = match self.anchor { + PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), + PanelAnchor::Right => ((-pixel_offset, 0), Anchor::Left, Gravity::Left), + PanelAnchor::Top => ((0, pixel_offset), Anchor::Bottom, Gravity::Bottom), + PanelAnchor::Bottom => ((0, -pixel_offset), Anchor::Top, Gravity::Top), + }; + SctkPopupSettings { + parent, + id, + positioner: SctkPositioner { + anchor, + gravity, + offset, + size, + anchor_rect: Rectangle { + x: 0, + y: 0, + width: width_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(width), + height: height_padding.unwrap_or(APPLET_PADDING as i32) * 2 + i32::from(height), + }, + reactive: true, + constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y + ..Default::default() + }, + parent_size: None, + grab: true, + } + } + + #[must_use] + pub fn theme(&self) -> theme::Theme { + match self.background { + CosmicPanelBackground::ThemeDefault | CosmicPanelBackground::Color(_) => { + let Ok(helper) = cosmic_config::Config::new( + cosmic_theme::NAME, + cosmic_theme::Theme::::version(), + ) else { + return theme::Theme::dark(); + }; + let t = + cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { + for err in errors { + error!("{:?}", err); + } + theme + }); + theme::Theme::custom(Arc::new(t)) + } + CosmicPanelBackground::Dark => theme::Theme::dark(), + CosmicPanelBackground::Light => theme::Theme::light(), + } + } +} + +/// Launch the application with the given settings. +/// +/// # Errors +/// +/// Returns error on application failure. +pub fn run(autosize: bool, flags: App::Flags) -> iced::Result { + let helper = CosmicAppletHelper::default(); + let mut settings = helper.window_settings(); + settings.autosize = autosize; + + if let Some(icon_theme) = settings.default_icon_theme { + crate::icon_theme::set_default(icon_theme); + } + + let mut core = Core::default(); + core.window.show_window_menu = false; + core.window.show_headerbar = false; + core.window.sharp_corners = true; + core.window.show_maximize = false; + core.window.show_minimize = false; + core.window.use_template = false; + + core.debug = settings.debug; + core.set_scale_factor(settings.scale_factor); + core.set_window_width(settings.size.0); + core.set_window_height(settings.size.1); + + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + cosmic_theme.set_theme(settings.theme.theme_type); + }); + + let mut iced = iced::Settings::with_flags((core, flags)); + + iced.antialiasing = settings.antialiasing; + iced.default_font = settings.default_font; + iced.default_text_size = settings.default_text_size; + iced.id = Some(App::APP_ID.to_owned()); + + { + use iced::wayland::actions::window::SctkWindowSettings; + use iced_sctk::settings::InitialSurface; + iced.initial_surface = InitialSurface::XdgWindow(SctkWindowSettings { + app_id: Some(App::APP_ID.to_owned()), + autosize: settings.autosize, + client_decorations: settings.client_decorations, + resizable: settings.resizable, + size: settings.size, + size_limits: settings.size_limits, + title: None, + transparent: settings.transparent, + ..SctkWindowSettings::default() + }); + } + + as iced::Application>::run(iced) +} + +#[must_use] +pub fn style() -> ::Style { + ::Style::Custom(Box::new(|theme| { + iced_style::application::Appearance { + background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), + text_color: theme.cosmic().on_bg_color().into(), + } + })) +} diff --git a/src/app/core.rs b/src/app/core.rs index 79f09a4..d5a979a 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -15,6 +15,7 @@ pub struct NavBar { #[allow(clippy::struct_excessive_bools)] #[derive(Clone)] pub struct Window { + pub use_template: bool, pub can_fullscreen: bool, pub sharp_corners: bool, pub show_headerbar: bool, @@ -43,6 +44,8 @@ pub struct Core { pub system_theme: Theme, pub(crate) title: String, pub window: Window, + #[cfg(feature = "applet")] + pub applet_helper: super::applet::CosmicAppletHelper, } impl Default for Core { @@ -59,6 +62,7 @@ impl Default for Core { system_theme: theme::theme(), title: String::new(), window: Window { + use_template: true, can_fullscreen: false, sharp_corners: false, show_headerbar: true, @@ -68,6 +72,8 @@ impl Default for Core { height: 0, width: 0, }, + #[cfg(feature = "applet")] + applet_helper: super::applet::CosmicAppletHelper::default(), } } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9b6bab9..6758e4d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -103,7 +103,9 @@ where } fn style(&self) -> ::Style { - if self.app.core().window.sharp_corners { + if let Some(style) = self.app.style() { + style + } else if self.app.core().window.sharp_corners { theme::Application::default() } else { theme::Application::Custom(Box::new(|theme| iced_style::application::Appearance { @@ -163,8 +165,11 @@ where if id != window::Id(0) { return self.app.view_window(id).map(super::Message::App); } - - self.app.view_main() + if self.app.core().window.use_template { + self.app.view_main() + } else { + self.app.view().map(super::Message::App) + } } #[cfg(not(feature = "wayland"))] diff --git a/src/app/mod.rs b/src/app/mod.rs index 5d98cbd..6182f76 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -6,6 +6,8 @@ //! Check out our [application](https://github.com/pop-os/libcosmic/tree/master/examples/application) //! example in our repository. +#[cfg(feature = "applet")] +pub mod applet; pub mod command; mod core; pub mod cosmic; @@ -197,6 +199,11 @@ where fn view_window(&self, id: window::Id) -> Element { panic!("no view for window {}", id.0); } + + /// Overrides the default style for applications + fn style(&self) -> Option<::Style> { + None + } } /// Methods automatically derived for all types implementing [`Application`].