diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 512bba8e..f393110d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -156,7 +156,7 @@ where } fn theme(&self) -> Self::Theme { - THEME.with(|t| t.borrow().clone()) + crate::theme::active() } #[cfg(feature = "wayland")] diff --git a/src/app/settings.rs b/src/app/settings.rs index 1819407e..3adecc83 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -91,7 +91,7 @@ impl Default for Settings { size: (1024, 768), #[cfg(feature = "wayland")] size_limits: Limits::NONE.min_height(1.0).min_width(1.0), - theme: crate::theme::theme(), + theme: crate::theme::system_preference(), transparent: false, } } diff --git a/src/theme/button.rs b/src/theme/button.rs deleted file mode 100644 index 3aed8186..00000000 --- a/src/theme/button.rs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2023 System76 -// SPDX-License-Identifier: MPL-2.0 - -use cosmic_theme::Component; -use iced_core::{Background, Color}; -use palette::{rgb::Rgb, Alpha}; - -use crate::{ - app, - widget::button::{Appearance, StyleSheet}, -}; - -#[derive(Copy, Clone, Debug, Default)] -pub enum Button { - Destructive, - Link, - Icon, - #[default] - Standard, - Suggested, - Text, -} - -pub fn appearance( - theme: &crate::Theme, - focused: bool, - style: &Button, - color: fn(&Component>) -> Color, -) -> Appearance { - let cosmic = theme.cosmic(); - let mut corner_radii = &cosmic.corner_radii.radius_xl; - let mut appearance = Appearance::new(); - - match style { - Button::Standard => { - let component = &theme.current_container().component; - appearance.background = Some(Background::Color(color(component))); - appearance.text_color = component.on.into(); - } - - Button::Icon | Button::Text => { - let component = &cosmic.text_button; - appearance.background = None; - appearance.text_color = component.on.into(); - } - - Button::Suggested => { - let component = &cosmic.accent_button; - appearance.background = Some(Background::Color(color(component))); - appearance.icon_color = Some(component.on.into()); - appearance.text_color = component.on.into(); - } - - Button::Destructive => { - let component = &cosmic.destructive_button; - appearance.background = Some(Background::Color(color(component))); - appearance.icon_color = Some(component.on.into()); - appearance.text_color = component.on.into(); - } - - Button::Link => { - appearance.background = None; - appearance.icon_color = Some(cosmic.accent.base.into()); - appearance.text_color = cosmic.accent.base.into(); - corner_radii = &cosmic.corner_radii.radius_0; - } - } - - appearance.border_radius = (*corner_radii).into(); - - if focused { - appearance.outline_width = 1.0; - appearance.outline_color = cosmic.accent.base.into(); - appearance.border_width = 2.0; - appearance.border_color = Color::TRANSPARENT; - } - - appearance -} - -impl StyleSheet for crate::Theme { - type Style = Button; - - fn active(&self, focused: bool, style: &Self::Style) -> Appearance { - appearance(self, focused, style, |component| component.base.into()) - } - - fn disabled(&self, style: &Self::Style) -> Appearance { - appearance(self, false, style, |component| { - let mut color = Color::from(component.base); - color.a *= 0.5; - color - }) - } - - fn drop_target(&self, style: &Self::Style) -> Appearance { - let mut appearance = self.active(false, style); - - appearance - } - - fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance { - appearance(self, focused, style, |component| component.hover.into()) - } - - fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance { - appearance(self, focused, style, |component| component.pressed.into()) - } - - fn selected(&self, focused: bool, style: &Self::Style) -> Appearance { - appearance(self, focused, style, |component| component.selected.into()) - } -} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 3cfa7820..979e941c 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,50 +1,20 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! Use COSMIC's themes and styles. +//! Contains the [`Theme`] type and its widget stylesheet implementations. -pub mod expander; - -mod button; -pub use self::button::Button; - -mod segmented_button; -pub use self::segmented_button::SegmentedButton; - -use std::cell::RefCell; -use std::f32::consts::PI; -use std::rc::Rc; -use std::sync::Arc; +pub mod style; +pub use style::*; use cosmic_config::config_subscription; use cosmic_config::CosmicConfigEntry; -use cosmic_theme::composite::over; use cosmic_theme::util::CssColor; use cosmic_theme::Component; use cosmic_theme::LayeredTheme; -use iced_core::gradient::Linear; -use iced_core::BorderRadius; -use iced_core::Radians; use iced_futures::Subscription; -use iced_style::application; -use iced_style::button as iced_button; -use iced_style::checkbox; -use iced_style::container; -use iced_style::menu; -use iced_style::pane_grid; -use iced_style::pick_list; -use iced_style::progress_bar; -use iced_style::radio; -use iced_style::rule; -use iced_style::scrollable; -use iced_style::slider; -use iced_style::slider::Rail; -use iced_style::svg; -use iced_style::text_input; -use iced_style::toggler; - -use iced_core::{Background, Color}; use palette::Srgba; +use std::cell::RefCell; +use std::sync::Arc; pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; @@ -75,6 +45,67 @@ thread_local! { pub(crate) static THEME: RefCell = RefCell::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background }); } +/// Currently-defined theme. +pub fn active() -> Theme { + THEME.with(|theme| theme.borrow().clone()) +} + +/// Currently-defined theme type. +pub fn active_type() -> ThemeType { + THEME.with(|theme| theme.borrow().theme_type.clone()) +} + +/// Whether the active theme has a dark preference. +#[must_use] +pub fn is_dark() -> bool { + active_type().is_dark() +} + +/// Whether the active theme is high contrast. +#[must_use] +pub fn is_high_contrast() -> bool { + active_type().is_high_contrast() +} + +/// Watches for changes to the system's theme preference. +pub fn subscription(id: u64) -> Subscription { + config_subscription::>( + id, + crate::cosmic_theme::NAME.into(), + crate::cosmic_theme::Theme::::version(), + ) + .map(|(_, res)| { + let theme = res.unwrap_or_else(|(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme + }); + + Theme::system(Arc::new(theme)) + }) +} + +/// Loads the preferred system theme from `cosmic-config`. +pub fn system_preference() -> Theme { + let Ok(helper) = crate::cosmic_config::Config::new( + crate::cosmic_theme::NAME, + crate::cosmic_theme::Theme::::version(), + ) else { + return Theme::dark(); + }; + + let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme + }); + + Theme::system(Arc::new(t)) +} + +#[must_use] #[derive(Debug, Clone, PartialEq, Default)] pub enum ThemeType { #[default] @@ -86,6 +117,29 @@ pub enum ThemeType { System(Arc), } +impl ThemeType { + /// Whether the theme has a dark preference. + #[must_use] + pub fn is_dark(&self) -> bool { + match self { + Self::Dark | Self::HighContrastDark => true, + Self::Light | Self::HighContrastLight => false, + Self::Custom(theme) | Self::System(theme) => theme.is_dark, + } + } + + /// Whether the theme has a high contrast. + #[must_use] + pub fn is_high_contrast(&self) -> bool { + match self { + Self::Dark | Self::Light => false, + Self::HighContrastDark | Self::HighContrastLight => true, + Self::Custom(theme) | Self::System(theme) => theme.is_high_contrast, + } + } +} + +#[must_use] #[derive(Debug, Clone, PartialEq, Default)] pub struct Theme { pub theme_type: ThemeType, @@ -104,7 +158,6 @@ impl Theme { } } - #[must_use] pub fn dark() -> Self { Self { theme_type: ThemeType::Dark, @@ -112,7 +165,6 @@ impl Theme { } } - #[must_use] pub fn light() -> Self { Self { theme_type: ThemeType::Light, @@ -120,7 +172,6 @@ impl Theme { } } - #[must_use] pub fn dark_hc() -> Self { Self { theme_type: ThemeType::HighContrastDark, @@ -128,7 +179,6 @@ impl Theme { } } - #[must_use] pub fn light_hc() -> Self { Self { theme_type: ThemeType::HighContrastLight, @@ -136,7 +186,6 @@ impl Theme { } } - #[must_use] pub fn custom(theme: Arc) -> Self { Self { theme_type: ThemeType::Custom(theme), @@ -144,7 +193,6 @@ impl Theme { } } - #[must_use] pub fn system(theme: Arc) -> Self { Self { theme_type: ThemeType::System(theme), @@ -174,1074 +222,3 @@ impl LayeredTheme for Theme { self.layer = layer; } } - -#[derive(Default)] -pub enum Application { - #[default] - Default, - Custom(Box application::Appearance>), -} - -impl Application { - pub fn custom application::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl application::StyleSheet for Theme { - type Style = Application; - - fn appearance(&self, style: &Self::Style) -> application::Appearance { - let cosmic = self.cosmic(); - - match style { - Application::Default => application::Appearance { - icon_color: cosmic.bg_color().into(), - background_color: cosmic.bg_color().into(), - text_color: cosmic.on_bg_color().into(), - }, - Application::Custom(f) => f(self), - } - } -} - -/// Styles for the button widget from iced-rs. -#[derive(Default)] -pub enum IcedButton { - Deactivated, - Destructive, - Positive, - #[default] - Primary, - Secondary, - Text, - Link, - LinkActive, - Transparent, - Card, - Custom { - active: Box iced_button::Appearance>, - hover: Box iced_button::Appearance>, - }, -} - -impl IcedButton { - #[allow(clippy::trivially_copy_pass_by_ref)] - #[allow(clippy::match_same_arms)] - fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { - let cosmic = theme.cosmic(); - match self { - IcedButton::Primary => &cosmic.accent_button, - IcedButton::Secondary => &theme.current_container().component, - IcedButton::Positive => &cosmic.success_button, - IcedButton::Destructive => &cosmic.destructive_button, - IcedButton::Text => &cosmic.text_button, - IcedButton::Link => &cosmic.accent_button, - IcedButton::LinkActive => &cosmic.accent_button, - IcedButton::Transparent => &TRANSPARENT_COMPONENT, - IcedButton::Deactivated => &theme.current_container().component, - IcedButton::Card => &theme.current_container().component, - IcedButton::Custom { .. } => &TRANSPARENT_COMPONENT, - } - } -} - -impl iced_button::StyleSheet for Theme { - type Style = IcedButton; - - fn active(&self, style: &Self::Style) -> iced_button::Appearance { - if let IcedButton::Custom { active, .. } = style { - return active(self); - } - - let corner_radii = &self.cosmic().corner_radii; - let component = style.cosmic(self); - iced_button::Appearance { - border_radius: match style { - IcedButton::Link => corner_radii.radius_0.into(), - IcedButton::Card => corner_radii.radius_xs.into(), - _ => corner_radii.radius_xl.into(), - }, - background: match style { - IcedButton::Link | IcedButton::Text => None, - IcedButton::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.base.into())), - }, - text_color: match style { - IcedButton::Link | IcedButton::LinkActive => component.base.into(), - _ => component.on.into(), - }, - ..iced_button::Appearance::default() - } - } - - fn hovered(&self, style: &Self::Style) -> iced_button::Appearance { - if let IcedButton::Custom { hover, .. } = style { - return hover(self); - } - - let active = self.active(style); - let component = style.cosmic(self); - - iced_button::Appearance { - background: match style { - IcedButton::Link => None, - IcedButton::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.hover.into())), - }, - ..active - } - } - - fn focused(&self, style: &Self::Style) -> iced_button::Appearance { - if let IcedButton::Custom { hover, .. } = style { - return hover(self); - } - - let active = self.active(style); - let component = style.cosmic(self); - iced_button::Appearance { - background: match style { - IcedButton::Link => None, - IcedButton::LinkActive => Some(Background::Color(component.divider.into())), - _ => Some(Background::Color(component.hover.into())), - }, - ..active - } - } - - fn disabled(&self, style: &Self::Style) -> iced_button::Appearance { - let active = self.active(style); - - if matches!(style, IcedButton::Card) { - return active; - } - - iced_button::Appearance { - shadow_offset: iced_core::Vector::default(), - background: active.background.map(|background| match background { - Background::Color(color) => Background::Color(Color { - a: color.a * 0.5, - ..color - }), - Background::Gradient(gradient) => Background::Gradient(gradient.mul_alpha(0.5)), - }), - text_color: Color { - a: active.text_color.a * 0.5, - ..active.text_color - }, - ..active - } - } -} - -/* - * TODO: Checkbox - */ -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Checkbox { - Primary, - Secondary, - Success, - Danger, -} - -impl Default for Checkbox { - fn default() -> Self { - Self::Primary - } -} - -impl checkbox::StyleSheet for Theme { - type Style = Checkbox; - - fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { - let cosmic = self.cosmic(); - - let corners = &cosmic.corner_radii; - match style { - Checkbox::Primary => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.accent.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.accent.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - Checkbox::Secondary => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.background.component.base.into() - } else { - cosmic.background.base.into() - }), - icon_color: cosmic.background.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: cosmic.button.border.into(), - text_color: None, - }, - Checkbox::Success => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.success.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.success.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - Checkbox::Danger => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.destructive.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.destructive.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - } - } - - fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { - let cosmic = self.cosmic(); - let corners = &cosmic.corner_radii; - - match style { - Checkbox::Primary => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.accent.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.accent.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - Checkbox::Secondary => checkbox::Appearance { - background: Background::Color(if is_checked { - self.current_container().base.into() - } else { - cosmic.button.base.into() - }), - icon_color: self.current_container().on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - self.current_container().base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - Checkbox::Success => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.success.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.success.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - Checkbox::Danger => checkbox::Appearance { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.destructive.on.into(), - border_radius: corners.radius_xs.into(), - border_width: if is_checked { 0.0 } else { 1.0 }, - border_color: if is_checked { - cosmic.destructive.base - } else { - cosmic.button.border - } - .into(), - text_color: None, - }, - } - } -} - -#[derive(Default)] -pub enum Expander { - #[default] - Default, - Custom(Box expander::Appearance>), -} - -impl Expander { - pub fn custom expander::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl expander::StyleSheet for Theme { - type Style = Expander; - - fn appearance(&self, style: Self::Style) -> expander::Appearance { - match style { - Expander::Default => expander::Appearance::default(), - Expander::Custom(f) => f(self), - } - } -} - -/* - * TODO: Container - */ -#[derive(Default)] -pub enum Container { - Background, - Primary, - Secondary, - #[default] - Transparent, - HeaderBar, - Custom(Box container::Appearance>), - Card, -} - -impl Container { - pub fn custom container::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl container::StyleSheet for Theme { - type Style = Container; - - fn appearance(&self, style: &Self::Style) -> container::Appearance { - match style { - Container::Transparent => container::Appearance::default(), - Container::Custom(f) => f(self), - Container::Background => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.background.on)), - text_color: Some(Color::from(palette.background.on)), - background: Some(iced::Background::Color(palette.background.base.into())), - border_radius: 2.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - Container::HeaderBar => { - let palette = self.cosmic(); - let mut header_top = palette.background.base; - let header_bottom = palette.background.base; - header_top.alpha = 0.8; - - container::Appearance { - icon_color: Some(Color::from(palette.accent.base)), - text_color: Some(Color::from(palette.background.on)), - background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( - Linear::new(Radians(3.0 * PI / 2.0)) - .add_stop(0.0, header_top.into()) - .add_stop(1.0, header_bottom.into()), - ))), - border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - Container::Primary => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.primary.on)), - text_color: Some(Color::from(palette.primary.on)), - background: Some(iced::Background::Color(palette.primary.base.into())), - border_radius: 2.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - Container::Secondary => { - let palette = self.cosmic(); - - container::Appearance { - icon_color: Some(Color::from(palette.secondary.on)), - text_color: Some(Color::from(palette.secondary.on)), - background: Some(iced::Background::Color(palette.secondary.base.into())), - border_radius: 2.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - Container::Card => { - let palette = self.cosmic(); - - match self.layer { - cosmic_theme::Layer::Background => container::Appearance { - icon_color: Some(Color::from(palette.background.component.on)), - text_color: Some(Color::from(palette.background.component.on)), - background: Some(iced::Background::Color( - palette.background.component.base.into(), - )), - border_radius: 8.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - cosmic_theme::Layer::Primary => container::Appearance { - icon_color: Some(Color::from(palette.primary.component.on)), - text_color: Some(Color::from(palette.primary.component.on)), - background: Some(iced::Background::Color( - palette.primary.component.base.into(), - )), - border_radius: 8.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - cosmic_theme::Layer::Secondary => container::Appearance { - icon_color: Some(Color::from(palette.secondary.component.on)), - text_color: Some(Color::from(palette.secondary.component.on)), - background: Some(iced::Background::Color( - palette.secondary.component.base.into(), - )), - border_radius: 8.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - } - } -} - -/* - * Slider - */ -impl slider::StyleSheet for Theme { - type Style = (); - - fn active(&self, _style: &Self::Style) -> slider::Appearance { - let cosmic = self.cosmic(); - - //TODO: no way to set rail thickness - slider::Appearance { - rail: Rail { - colors: ( - cosmic.accent.base.into(), - //TODO: no way to set color before/after slider - Color::TRANSPARENT, - ), - width: 4.0, - border_radius: 2.0.into(), - }, - - handle: slider::Handle { - shape: slider::HandleShape::Circle { radius: 10.0 }, - color: cosmic.accent.base.into(), - border_color: Color::TRANSPARENT, - border_width: 0.0, - }, - } - } - - fn hovered(&self, style: &Self::Style) -> slider::Appearance { - let mut style = self.active(style); - style.handle.shape = slider::HandleShape::Circle { radius: 16.0 }; - style.handle.border_width = 6.0; - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.1; - style.handle.border_color = border_color.into(); - style - } - - fn dragging(&self, style: &Self::Style) -> slider::Appearance { - let mut style = self.hovered(style); - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.2; - style.handle.border_color = border_color.into(); - - style - } -} - -/* - * TODO: Menu - */ -impl menu::StyleSheet for Theme { - type Style = (); - - fn appearance(&self, _style: &Self::Style) -> menu::Appearance { - let cosmic = self.cosmic(); - - menu::Appearance { - text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background.base.into()), - border_width: 0.0, - border_radius: 16.0.into(), - border_color: Color::TRANSPARENT, - selected_text_color: cosmic.on_bg_color().into(), - // TODO doesn't seem to be specified - selected_background: Background::Color(cosmic.background.component.hover.into()), - } - } -} - -/* - * TODO: Pick List - */ -impl pick_list::StyleSheet for Theme { - type Style = (); - - fn active(&self, _style: &()) -> pick_list::Appearance { - let cosmic = &self.cosmic(); - - pick_list::Appearance { - text_color: cosmic.on_bg_color().into(), - background: Color::TRANSPARENT.into(), - placeholder_color: cosmic.on_bg_color().into(), - border_radius: 24.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - // icon_size: 0.7, // TODO: how to replace - handle_color: cosmic.on_bg_color().into(), - } - } - - fn hovered(&self, style: &()) -> pick_list::Appearance { - let cosmic = &self.cosmic(); - - pick_list::Appearance { - background: Background::Color(cosmic.background.base.into()), - ..self.active(style) - } - } -} - -/* - * TODO: Radio - */ -impl radio::StyleSheet for Theme { - type Style = (); - - fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { - let theme = self.cosmic(); - - radio::Appearance { - background: if is_selected { - Color::from(theme.accent.base).into() - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.background.base).into() - }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) - }, - text_color: None, - } - } - - fn hovered(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { - let theme = self.cosmic(); - let mut neutral_10 = theme.palette.neutral_10; - neutral_10.alpha = 0.1; - - radio::Appearance { - background: if is_selected { - Color::from(theme.accent.base).into() - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(neutral_10).into() - }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) - }, - text_color: None, - } - } -} - -/* - * Toggler - */ -impl toggler::StyleSheet for Theme { - type Style = (); - - fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance { - let theme = self.cosmic(); - toggler::Appearance { - background: if is_active { - theme.accent.base.into() - } else { - theme.palette.neutral_5.into() - }, - background_border: None, - foreground: theme.palette.neutral_2.into(), - foreground_border: None, - } - } - - fn hovered(&self, style: &Self::Style, is_active: bool) -> toggler::Appearance { - let cosmic = self.cosmic(); - //TODO: grab colors from palette - let mut neutral_10 = cosmic.palette.neutral_10; - neutral_10.alpha = 0.1; - - toggler::Appearance { - background: if is_active { - over(neutral_10, cosmic.accent_color()) - } else { - over(neutral_10, cosmic.palette.neutral_5) - } - .into(), - ..self.active(style, is_active) - } - } -} - -/* - * TODO: Pane Grid - */ -impl pane_grid::StyleSheet for Theme { - type Style = (); - - fn picked_split(&self, _style: &Self::Style) -> Option { - let theme = self.cosmic(); - - Some(pane_grid::Line { - color: theme.accent.base.into(), - width: 2.0, - }) - } - - fn hovered_split(&self, _style: &Self::Style) -> Option { - let theme = self.cosmic(); - - Some(pane_grid::Line { - color: theme.accent.hover.into(), - width: 2.0, - }) - } - - fn hovered_region(&self, _style: &Self::Style) -> pane_grid::Appearance { - let theme = self.cosmic(); - pane_grid::Appearance { - background: Background::Color(theme.bg_color().into()), - border_width: 2.0, - border_color: theme.bg_divider().into(), - border_radius: 0.0.into(), - } - } -} - -/* - * TODO: Progress Bar - */ -#[derive(Default)] -pub enum ProgressBar { - #[default] - Primary, - Success, - Danger, - Custom(Box progress_bar::Appearance>), -} - -impl ProgressBar { - pub fn custom progress_bar::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl progress_bar::StyleSheet for Theme { - type Style = ProgressBar; - - fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { - let theme = self.cosmic(); - - match style { - ProgressBar::Primary => progress_bar::Appearance { - background: Color::from(theme.background.divider).into(), - bar: Color::from(theme.accent.base).into(), - border_radius: 2.0.into(), - }, - ProgressBar::Success => progress_bar::Appearance { - background: Color::from(theme.background.divider).into(), - bar: Color::from(theme.success.base).into(), - border_radius: 2.0.into(), - }, - ProgressBar::Danger => progress_bar::Appearance { - background: Color::from(theme.background.divider).into(), - bar: Color::from(theme.destructive.base).into(), - border_radius: 2.0.into(), - }, - ProgressBar::Custom(f) => f(self), - } - } -} - -/* - * TODO: Rule - */ -#[derive(Default)] -pub enum Rule { - #[default] - Default, - LightDivider, - HeavyDivider, - Custom(Box rule::Appearance>), -} - -impl Rule { - pub fn custom rule::Appearance + 'static>(f: F) -> Self { - Self::Custom(Box::new(f)) - } -} - -impl rule::StyleSheet for Theme { - type Style = Rule; - - fn appearance(&self, style: &Self::Style) -> rule::Appearance { - match style { - Rule::Default => rule::Appearance { - color: self.current_container().divider.into(), - width: 1, - radius: 0.0.into(), - fill_mode: rule::FillMode::Full, - }, - Rule::LightDivider => rule::Appearance { - color: self.current_container().divider.into(), - width: 1, - radius: 0.0.into(), - fill_mode: rule::FillMode::Padded(10), - }, - Rule::HeavyDivider => rule::Appearance { - color: self.current_container().divider.into(), - width: 4, - radius: 4.0.into(), - fill_mode: rule::FillMode::Full, - }, - Rule::Custom(f) => f(self), - } - } -} - -/* - * TODO: Scrollable - */ -impl scrollable::StyleSheet for Theme { - type Style = (); - - fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { - scrollable::Scrollbar { - background: Some(Background::Color( - self.current_container().component.base.into(), - )), - border_radius: 4.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: self.current_container().component.divider.into(), - border_radius: 4.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } - - fn hovered( - &self, - _style: &Self::Style, - _is_mouse_over_scrollbar: bool, - ) -> scrollable::Scrollbar { - let theme = self.cosmic(); - - scrollable::Scrollbar { - background: Some(Background::Color( - self.current_container().component.hover.into(), - )), - border_radius: 4.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - scroller: scrollable::Scroller { - color: theme.accent.base.into(), - border_radius: 4.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - }, - } - } -} - -#[derive(Clone, Default)] -pub enum Svg { - /// Apply a custom appearance filter - Custom(Rc svg::Appearance>), - /// No filtering is applied - #[default] - Default, -} - -impl Svg { - pub fn custom svg::Appearance + 'static>(f: F) -> Self { - Self::Custom(Rc::new(f)) - } -} - -impl svg::StyleSheet for Theme { - type Style = Svg; - - fn appearance(&self, style: &Self::Style) -> svg::Appearance { - #[allow(clippy::match_same_arms)] - match style { - Svg::Default => svg::Appearance::default(), - Svg::Custom(appearance) => appearance(self), - } - } -} - -/* - * TODO: Text - */ -#[derive(Clone, Copy, Default)] -pub enum Text { - Accent, - #[default] - Default, - Color(Color), - // TODO: Can't use dyn Fn since this must be copy - Custom(fn(&Theme) -> iced_widget::text::Appearance), -} - -impl From for Text { - fn from(color: Color) -> Self { - Text::Color(color) - } -} - -impl iced_widget::text::StyleSheet for Theme { - type Style = Text; - - fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance { - match style { - Text::Accent => iced_widget::text::Appearance { - color: Some(self.cosmic().accent.base.into()), - }, - Text::Default => iced_widget::text::Appearance { color: None }, - Text::Color(c) => iced_widget::text::Appearance { color: Some(c) }, - Text::Custom(f) => f(self), - } - } -} - -#[derive(Copy, Clone, Default)] -pub enum TextInput { - #[default] - Default, - Search, -} - -/* - * TODO: Text Input - */ -impl text_input::StyleSheet for Theme { - type Style = TextInput; - - fn active(&self, style: &Self::Style) -> text_input::Appearance { - let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; - match style { - TextInput::Default => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 8.0.into(), - border_width: 1.0, - border_color: self.current_container().component.divider.into(), - icon_color: self.current_container().on.into(), - }, - TextInput::Search => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 24.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - icon_color: self.current_container().on.into(), - }, - } - } - - fn hovered(&self, style: &Self::Style) -> text_input::Appearance { - let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; - - match style { - TextInput::Default => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 8.0.into(), - border_width: 1.0, - border_color: palette.accent.base.into(), - icon_color: self.current_container().on.into(), - }, - TextInput::Search => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 24.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - icon_color: self.current_container().on.into(), - }, - } - } - - fn focused(&self, style: &Self::Style) -> text_input::Appearance { - let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; - - match style { - TextInput::Default => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 8.0.into(), - border_width: 1.0, - border_color: palette.accent.base.into(), - icon_color: self.current_container().on.into(), - }, - TextInput::Search => text_input::Appearance { - background: Color::from(bg).into(), - border_radius: 24.0.into(), - border_width: 0.0, - border_color: Color::TRANSPARENT, - icon_color: self.current_container().on.into(), - }, - } - } - - fn placeholder_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - let mut neutral_9 = palette.palette.neutral_9; - neutral_9.alpha = 0.7; - neutral_9.into() - } - - fn value_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - - palette.palette.neutral_9.into() - } - - fn selection_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - - palette.accent.base.into() - } - - fn disabled_color(&self, _style: &Self::Style) -> Color { - let palette = self.cosmic(); - let mut neutral_9 = palette.palette.neutral_9; - neutral_9.alpha = 0.5; - neutral_9.into() - } - - fn disabled(&self, style: &Self::Style) -> text_input::Appearance { - self.active(style) - } -} - -#[must_use] -pub fn theme() -> Theme { - let Ok(helper) = crate::cosmic_config::Config::new( - crate::cosmic_theme::NAME, - crate::cosmic_theme::Theme::::version(), - ) else { - return crate::theme::Theme::dark(); - }; - let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { - for err in errors { - tracing::error!("{:?}", err); - } - theme - }); - crate::theme::Theme::system(Arc::new(t)) -} - -pub fn subscription(id: u64) -> Subscription { - config_subscription::>( - id, - crate::cosmic_theme::NAME.into(), - crate::cosmic_theme::Theme::::version(), - ) - .map(|(_, res)| { - let theme = res.unwrap_or_else(|(errors, theme)| { - for err in errors { - tracing::error!("{:?}", err); - } - theme - }); - - crate::theme::Theme::system(Arc::new(theme)) - }) -} - -impl crate::widget::card::style::StyleSheet for Theme { - fn default(&self) -> crate::widget::card::style::Appearance { - let cosmic = self.cosmic(); - - match self.layer { - cosmic_theme::Layer::Background => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.background.component.hover.into()), - card_2: Background::Color(cosmic.background.component.pressed.into()), - }, - cosmic_theme::Layer::Primary => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.primary.component.hover.into()), - card_2: Background::Color(cosmic.primary.component.pressed.into()), - }, - cosmic_theme::Layer::Secondary => crate::widget::card::style::Appearance { - card_1: Background::Color(cosmic.secondary.component.hover.into()), - card_2: Background::Color(cosmic.secondary.component.pressed.into()), - }, - } - } -} diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs new file mode 100644 index 00000000..0d092f36 --- /dev/null +++ b/src/theme/style/button.rs @@ -0,0 +1,134 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Contains stylesheet implementation for [`crate::widget::button`]. + +use cosmic_theme::Component; +use iced_core::{Background, Color}; +use palette::{rgb::Rgb, Alpha}; + +use crate::widget::button::{Appearance, StyleSheet}; + +#[derive(Copy, Clone, Debug, Default)] +pub enum Button { + Destructive, + Link, + Icon, + IconVertical, + #[default] + Standard, + Suggested, + Text, +} + +pub fn appearance( + theme: &crate::Theme, + focused: bool, + style: &Button, + color: impl Fn(&Component>) -> (Color, Color, Option), +) -> Appearance { + let cosmic = theme.cosmic(); + let mut corner_radii = &cosmic.corner_radii.radius_xl; + let mut appearance = Appearance::new(); + + match style { + Button::Standard | Button::Text | Button::Suggested | Button::Destructive => { + let style_component = match style { + Button::Standard => &cosmic.button, + Button::Text => &cosmic.text_button, + Button::Suggested => &cosmic.accent_button, + Button::Destructive => &cosmic.destructive_button, + _ => return appearance, + }; + + let (background, text, icon) = color(style_component); + appearance.background = Some(Background::Color(background)); + appearance.text_color = text; + appearance.icon_color = icon; + } + + Button::Icon | Button::IconVertical => { + if let Button::IconVertical = style { + corner_radii = &cosmic.corner_radii.radius_m; + } + + let (background, text, icon) = color(&cosmic.icon_button); + appearance.background = Some(Background::Color(background)); + if focused { + appearance.text_color = cosmic.accent.on.into(); + appearance.icon_color = Some(cosmic.accent.on.into()); + } else { + appearance.text_color = text; + appearance.icon_color = icon; + } + } + + Button::Link => { + appearance.background = None; + appearance.icon_color = Some(cosmic.accent.base.into()); + appearance.text_color = cosmic.accent.base.into(); + corner_radii = &cosmic.corner_radii.radius_0; + } + } + + appearance.border_radius = (*corner_radii).into(); + + if focused { + appearance.outline_width = 1.0; + appearance.outline_color = cosmic.accent.base.into(); + appearance.border_width = 2.0; + appearance.border_color = Color::TRANSPARENT; + } + + appearance +} + +impl StyleSheet for crate::Theme { + type Style = Button; + + fn active(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| { + ( + component.base.into(), + component.on.into(), + Some(component.on.into()), + ) + }) + } + + fn disabled(&self, style: &Self::Style) -> Appearance { + appearance(self, false, style, |component| { + let mut background = Color::from(component.base); + background.a *= 0.5; + ( + background, + component.on_disabled.into(), + Some(component.on_disabled.into()), + ) + }) + } + + fn drop_target(&self, style: &Self::Style) -> Appearance { + self.active(false, style) + } + + fn hovered(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| { + ( + component.hover.into(), + component.on.into(), + Some(component.on.into()), + ) + }) + } + + fn pressed(&self, focused: bool, style: &Self::Style) -> Appearance { + appearance(self, focused, style, |component| { + ( + component.pressed.into(), + component.on.into(), + Some(component.on.into()), + ) + }) + } +} diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs new file mode 100644 index 00000000..ad3d9f13 --- /dev/null +++ b/src/theme/style/iced.rs @@ -0,0 +1,1057 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Contains stylesheet implementations for widgets native to iced. + +use crate::theme::{CosmicComponent, Theme, TRANSPARENT_COMPONENT}; +use cosmic_theme::composite::over; +use iced_core::gradient::Linear; +use iced_core::BorderRadius; +use iced_core::Radians; +use iced_core::{Background, Color}; +use iced_style::application; +use iced_style::button as iced_button; +use iced_style::checkbox; +use iced_style::container; +use iced_style::menu; +use iced_style::pane_grid; +use iced_style::pick_list; +use iced_style::progress_bar; +use iced_style::radio; +use iced_style::rule; +use iced_style::scrollable; +use iced_style::slider; +use iced_style::slider::Rail; +use iced_style::svg; +use iced_style::text_input; +use iced_style::toggler; +use std::f32::consts::PI; +use std::rc::Rc; + +#[derive(Default)] +pub enum Application { + #[default] + Default, + Custom(Box application::Appearance>), +} + +impl Application { + pub fn custom application::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl application::StyleSheet for Theme { + type Style = Application; + + fn appearance(&self, style: &Self::Style) -> application::Appearance { + let cosmic = self.cosmic(); + + match style { + Application::Default => application::Appearance { + icon_color: cosmic.bg_color().into(), + background_color: cosmic.bg_color().into(), + text_color: cosmic.on_bg_color().into(), + }, + Application::Custom(f) => f(self), + } + } +} + +/// Styles for the button widget from iced-rs. +#[derive(Default)] +pub enum Button { + Deactivated, + Destructive, + Positive, + #[default] + Primary, + Secondary, + Text, + Link, + LinkActive, + Transparent, + Card, + Custom { + active: Box iced_button::Appearance>, + hover: Box iced_button::Appearance>, + }, +} + +impl Button { + #[allow(clippy::trivially_copy_pass_by_ref)] + #[allow(clippy::match_same_arms)] + fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { + let cosmic = theme.cosmic(); + match self { + Button::Primary => &cosmic.accent_button, + Button::Secondary => &theme.current_container().component, + Button::Positive => &cosmic.success_button, + Button::Destructive => &cosmic.destructive_button, + Button::Text => &cosmic.text_button, + Button::Link => &cosmic.accent_button, + Button::LinkActive => &cosmic.accent_button, + Button::Transparent => &TRANSPARENT_COMPONENT, + Button::Deactivated => &theme.current_container().component, + Button::Card => &theme.current_container().component, + Button::Custom { .. } => &TRANSPARENT_COMPONENT, + } + } +} + +impl iced_button::StyleSheet for Theme { + type Style = Button; + + fn active(&self, style: &Self::Style) -> iced_button::Appearance { + if let Button::Custom { active, .. } = style { + return active(self); + } + + let corner_radii = &self.cosmic().corner_radii; + let component = style.cosmic(self); + iced_button::Appearance { + border_radius: match style { + Button::Link => corner_radii.radius_0.into(), + Button::Card => corner_radii.radius_xs.into(), + _ => corner_radii.radius_xl.into(), + }, + background: match style { + Button::Link | Button::Text => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.base.into())), + }, + text_color: match style { + Button::Link | Button::LinkActive => component.base.into(), + _ => component.on.into(), + }, + ..iced_button::Appearance::default() + } + } + + fn hovered(&self, style: &Self::Style) -> iced_button::Appearance { + if let Button::Custom { hover, .. } = style { + return hover(self); + } + + let active = self.active(style); + let component = style.cosmic(self); + + iced_button::Appearance { + background: match style { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.hover.into())), + }, + ..active + } + } + + fn focused(&self, style: &Self::Style) -> iced_button::Appearance { + if let Button::Custom { hover, .. } = style { + return hover(self); + } + + let active = self.active(style); + let component = style.cosmic(self); + iced_button::Appearance { + background: match style { + Button::Link => None, + Button::LinkActive => Some(Background::Color(component.divider.into())), + _ => Some(Background::Color(component.hover.into())), + }, + ..active + } + } + + fn disabled(&self, style: &Self::Style) -> iced_button::Appearance { + let active = self.active(style); + + if matches!(style, Button::Card) { + return active; + } + + iced_button::Appearance { + shadow_offset: iced_core::Vector::default(), + background: active.background.map(|background| match background { + Background::Color(color) => Background::Color(Color { + a: color.a * 0.5, + ..color + }), + Background::Gradient(gradient) => Background::Gradient(gradient.mul_alpha(0.5)), + }), + text_color: Color { + a: active.text_color.a * 0.5, + ..active.text_color + }, + ..active + } + } +} + +/* + * TODO: Checkbox + */ +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Checkbox { + Primary, + Secondary, + Success, + Danger, +} + +impl Default for Checkbox { + fn default() -> Self { + Self::Primary + } +} + +impl checkbox::StyleSheet for Theme { + type Style = Checkbox; + + fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { + let cosmic = self.cosmic(); + + let corners = &cosmic.corner_radii; + match style { + Checkbox::Primary => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.accent.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.accent.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.accent.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + Checkbox::Secondary => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.background.component.base.into() + } else { + cosmic.background.base.into() + }), + icon_color: cosmic.background.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: cosmic.button.border.into(), + text_color: None, + }, + Checkbox::Success => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.success.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.success.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.success.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + Checkbox::Danger => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.destructive.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.destructive.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.destructive.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + } + } + + fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance { + let cosmic = self.cosmic(); + let corners = &cosmic.corner_radii; + + match style { + Checkbox::Primary => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.accent.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.accent.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.accent.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + Checkbox::Secondary => checkbox::Appearance { + background: Background::Color(if is_checked { + self.current_container().base.into() + } else { + cosmic.button.base.into() + }), + icon_color: self.current_container().on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + self.current_container().base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + Checkbox::Success => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.success.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.success.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.success.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + Checkbox::Danger => checkbox::Appearance { + background: Background::Color(if is_checked { + cosmic.destructive.base.into() + } else { + cosmic.button.base.into() + }), + icon_color: cosmic.destructive.on.into(), + border_radius: corners.radius_xs.into(), + border_width: if is_checked { 0.0 } else { 1.0 }, + border_color: if is_checked { + cosmic.destructive.base + } else { + cosmic.button.border + } + .into(), + text_color: None, + }, + } + } +} + +/* + * TODO: Container + */ +#[derive(Default)] +pub enum Container { + Background, + Card, + Custom(Box container::Appearance>), + HeaderBar, + Primary, + Secondary, + Tooltip, + #[default] + Transparent, +} + +impl Container { + pub fn custom container::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl container::StyleSheet for Theme { + type Style = Container; + + #[allow(clippy::too_many_lines)] + fn appearance(&self, style: &Self::Style) -> container::Appearance { + match style { + Container::Transparent => container::Appearance::default(), + Container::Custom(f) => f(self), + Container::Background => { + let palette = self.cosmic(); + + container::Appearance { + icon_color: Some(Color::from(palette.background.on)), + text_color: Some(Color::from(palette.background.on)), + background: Some(iced::Background::Color(palette.background.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::HeaderBar => { + let palette = self.cosmic(); + let mut header_top = palette.background.base; + let header_bottom = palette.background.base; + header_top.alpha = 0.8; + + container::Appearance { + icon_color: Some(Color::from(palette.accent.base)), + text_color: Some(Color::from(palette.background.on)), + background: Some(iced::Background::Gradient(iced_core::Gradient::Linear( + Linear::new(Radians(3.0 * PI / 2.0)) + .add_stop(0.0, header_top.into()) + .add_stop(1.0, header_bottom.into()), + ))), + border_radius: BorderRadius::from([16.0, 16.0, 0.0, 0.0]), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Primary => { + let palette = self.cosmic(); + + container::Appearance { + icon_color: Some(Color::from(palette.primary.on)), + text_color: Some(Color::from(palette.primary.on)), + background: Some(iced::Background::Color(palette.primary.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + Container::Secondary => { + let palette = self.cosmic(); + + container::Appearance { + icon_color: Some(Color::from(palette.secondary.on)), + text_color: Some(Color::from(palette.secondary.on)), + background: Some(iced::Background::Color(palette.secondary.base.into())), + border_radius: 2.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + + Container::Tooltip => { + let theme = self.cosmic(); + + container::Appearance { + icon_color: None, + text_color: None, + background: Some(iced::Background::Color(theme.palette.neutral_2.into())), + border_radius: f32::from(theme.space_xl()).into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + } + } + + Container::Card => { + let palette = self.cosmic(); + + match self.layer { + cosmic_theme::Layer::Background => container::Appearance { + icon_color: Some(Color::from(palette.background.component.on)), + text_color: Some(Color::from(palette.background.component.on)), + background: Some(iced::Background::Color( + palette.background.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + cosmic_theme::Layer::Primary => container::Appearance { + icon_color: Some(Color::from(palette.primary.component.on)), + text_color: Some(Color::from(palette.primary.component.on)), + background: Some(iced::Background::Color( + palette.primary.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + cosmic_theme::Layer::Secondary => container::Appearance { + icon_color: Some(Color::from(palette.secondary.component.on)), + text_color: Some(Color::from(palette.secondary.component.on)), + background: Some(iced::Background::Color( + palette.secondary.component.base.into(), + )), + border_radius: 8.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + } + } +} + +/* + * Slider + */ +impl slider::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> slider::Appearance { + let cosmic = self.cosmic(); + + //TODO: no way to set rail thickness + slider::Appearance { + rail: Rail { + colors: ( + cosmic.accent.base.into(), + //TODO: no way to set color before/after slider + Color::TRANSPARENT, + ), + width: 4.0, + border_radius: 2.0.into(), + }, + + handle: slider::Handle { + shape: slider::HandleShape::Circle { radius: 10.0 }, + color: cosmic.accent.base.into(), + border_color: Color::TRANSPARENT, + border_width: 0.0, + }, + } + } + + fn hovered(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.active(style); + style.handle.shape = slider::HandleShape::Circle { radius: 16.0 }; + style.handle.border_width = 6.0; + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.1; + style.handle.border_color = border_color.into(); + style + } + + fn dragging(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.hovered(style); + let mut border_color = self.cosmic().palette.neutral_10; + border_color.alpha = 0.2; + style.handle.border_color = border_color.into(); + + style + } +} + +/* + * TODO: Menu + */ +impl menu::StyleSheet for Theme { + type Style = (); + + fn appearance(&self, _style: &Self::Style) -> menu::Appearance { + let cosmic = self.cosmic(); + + menu::Appearance { + text_color: cosmic.on_bg_color().into(), + background: Background::Color(cosmic.background.base.into()), + border_width: 0.0, + border_radius: 16.0.into(), + border_color: Color::TRANSPARENT, + selected_text_color: cosmic.on_bg_color().into(), + // TODO doesn't seem to be specified + selected_background: Background::Color(cosmic.background.component.hover.into()), + } + } +} + +/* + * TODO: Pick List + */ +impl pick_list::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic(); + + pick_list::Appearance { + text_color: cosmic.on_bg_color().into(), + background: Color::TRANSPARENT.into(), + placeholder_color: cosmic.on_bg_color().into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + // icon_size: 0.7, // TODO: how to replace + handle_color: cosmic.on_bg_color().into(), + } + } + + fn hovered(&self, style: &()) -> pick_list::Appearance { + let cosmic = &self.cosmic(); + + pick_list::Appearance { + background: Background::Color(cosmic.background.base.into()), + ..self.active(style) + } + } +} + +/* + * TODO: Radio + */ +impl radio::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + let theme = self.cosmic(); + + radio::Appearance { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.background.base).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, + } + } + + fn hovered(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { + let theme = self.cosmic(); + let mut neutral_10 = theme.palette.neutral_10; + neutral_10.alpha = 0.1; + + radio::Appearance { + background: if is_selected { + Color::from(theme.accent.base).into() + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(neutral_10).into() + }, + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + // TODO: this seems to be defined weirdly in FIGMA + Color::from(theme.palette.neutral_7) + }, + text_color: None, + } + } +} + +/* + * Toggler + */ +impl toggler::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance { + let theme = self.cosmic(); + toggler::Appearance { + background: if is_active { + theme.accent.base.into() + } else { + theme.palette.neutral_5.into() + }, + background_border: None, + foreground: theme.palette.neutral_2.into(), + foreground_border: None, + } + } + + fn hovered(&self, style: &Self::Style, is_active: bool) -> toggler::Appearance { + let cosmic = self.cosmic(); + //TODO: grab colors from palette + let mut neutral_10 = cosmic.palette.neutral_10; + neutral_10.alpha = 0.1; + + toggler::Appearance { + background: if is_active { + over(neutral_10, cosmic.accent_color()) + } else { + over(neutral_10, cosmic.palette.neutral_5) + } + .into(), + ..self.active(style, is_active) + } + } +} + +/* + * TODO: Pane Grid + */ +impl pane_grid::StyleSheet for Theme { + type Style = (); + + fn picked_split(&self, _style: &Self::Style) -> Option { + let theme = self.cosmic(); + + Some(pane_grid::Line { + color: theme.accent.base.into(), + width: 2.0, + }) + } + + fn hovered_split(&self, _style: &Self::Style) -> Option { + let theme = self.cosmic(); + + Some(pane_grid::Line { + color: theme.accent.hover.into(), + width: 2.0, + }) + } + + fn hovered_region(&self, _style: &Self::Style) -> pane_grid::Appearance { + let theme = self.cosmic(); + pane_grid::Appearance { + background: Background::Color(theme.bg_color().into()), + border_width: 2.0, + border_color: theme.bg_divider().into(), + border_radius: 0.0.into(), + } + } +} + +/* + * TODO: Progress Bar + */ +#[derive(Default)] +pub enum ProgressBar { + #[default] + Primary, + Success, + Danger, + Custom(Box progress_bar::Appearance>), +} + +impl ProgressBar { + pub fn custom progress_bar::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl progress_bar::StyleSheet for Theme { + type Style = ProgressBar; + + fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { + let theme = self.cosmic(); + + match style { + ProgressBar::Primary => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.accent.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Success => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.success.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Danger => progress_bar::Appearance { + background: Color::from(theme.background.divider).into(), + bar: Color::from(theme.destructive.base).into(), + border_radius: 2.0.into(), + }, + ProgressBar::Custom(f) => f(self), + } + } +} + +/* + * TODO: Rule + */ +#[derive(Default)] +pub enum Rule { + #[default] + Default, + LightDivider, + HeavyDivider, + Custom(Box rule::Appearance>), +} + +impl Rule { + pub fn custom rule::Appearance + 'static>(f: F) -> Self { + Self::Custom(Box::new(f)) + } +} + +impl rule::StyleSheet for Theme { + type Style = Rule; + + fn appearance(&self, style: &Self::Style) -> rule::Appearance { + match style { + Rule::Default => rule::Appearance { + color: self.current_container().divider.into(), + width: 1, + radius: 0.0.into(), + fill_mode: rule::FillMode::Full, + }, + Rule::LightDivider => rule::Appearance { + color: self.current_container().divider.into(), + width: 1, + radius: 0.0.into(), + fill_mode: rule::FillMode::Padded(10), + }, + Rule::HeavyDivider => rule::Appearance { + color: self.current_container().divider.into(), + width: 4, + radius: 4.0.into(), + fill_mode: rule::FillMode::Full, + }, + Rule::Custom(f) => f(self), + } + } +} + +/* + * TODO: Scrollable + */ +impl scrollable::StyleSheet for Theme { + type Style = (); + + fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { + scrollable::Scrollbar { + background: Some(Background::Color( + self.current_container().component.base.into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: self.current_container().component.divider.into(), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } + + fn hovered( + &self, + _style: &Self::Style, + _is_mouse_over_scrollbar: bool, + ) -> scrollable::Scrollbar { + let theme = self.cosmic(); + + scrollable::Scrollbar { + background: Some(Background::Color( + self.current_container().component.hover.into(), + )), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + scroller: scrollable::Scroller { + color: theme.accent.base.into(), + border_radius: 4.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + }, + } + } +} + +#[derive(Clone, Default)] +pub enum Svg { + /// Apply a custom appearance filter + Custom(Rc svg::Appearance>), + /// No filtering is applied + #[default] + Default, +} + +impl Svg { + pub fn custom svg::Appearance + 'static>(f: F) -> Self { + Self::Custom(Rc::new(f)) + } +} + +impl svg::StyleSheet for Theme { + type Style = Svg; + + fn appearance(&self, style: &Self::Style) -> svg::Appearance { + #[allow(clippy::match_same_arms)] + match style { + Svg::Default => svg::Appearance::default(), + Svg::Custom(appearance) => appearance(self), + } + } +} + +/* + * TODO: Text + */ +#[derive(Clone, Copy, Default)] +pub enum Text { + Accent, + #[default] + Default, + Color(Color), + // TODO: Can't use dyn Fn since this must be copy + Custom(fn(&Theme) -> iced_widget::text::Appearance), +} + +impl From for Text { + fn from(color: Color) -> Self { + Text::Color(color) + } +} + +impl iced_widget::text::StyleSheet for Theme { + type Style = Text; + + fn appearance(&self, style: Self::Style) -> iced_widget::text::Appearance { + match style { + Text::Accent => iced_widget::text::Appearance { + color: Some(self.cosmic().accent.base.into()), + }, + Text::Default => iced_widget::text::Appearance { color: None }, + Text::Color(c) => iced_widget::text::Appearance { color: Some(c) }, + Text::Custom(f) => f(self), + } + } +} + +#[derive(Copy, Clone, Default)] +pub enum TextInput { + #[default] + Default, + Search, +} + +/* + * TODO: Text Input + */ +impl text_input::StyleSheet for Theme { + type Style = TextInput; + + fn active(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: self.current_container().component.divider.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn hovered(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn focused(&self, style: &Self::Style) -> text_input::Appearance { + let palette = self.cosmic(); + let mut bg = palette.palette.neutral_7; + bg.alpha = 0.25; + + match style { + TextInput::Default => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 8.0.into(), + border_width: 1.0, + border_color: palette.accent.base.into(), + icon_color: self.current_container().on.into(), + }, + TextInput::Search => text_input::Appearance { + background: Color::from(bg).into(), + border_radius: 24.0.into(), + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: self.current_container().on.into(), + }, + } + } + + fn placeholder_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.7; + neutral_9.into() + } + + fn value_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + + palette.palette.neutral_9.into() + } + + fn selection_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + + palette.accent.base.into() + } + + fn disabled_color(&self, _style: &Self::Style) -> Color { + let palette = self.cosmic(); + let mut neutral_9 = palette.palette.neutral_9; + neutral_9.alpha = 0.5; + neutral_9.into() + } + + fn disabled(&self, style: &Self::Style) -> text_input::Appearance { + self.active(style) + } +} + +impl crate::widget::card::style::StyleSheet for Theme { + fn default(&self) -> crate::widget::card::style::Appearance { + let cosmic = self.cosmic(); + + match self.layer { + cosmic_theme::Layer::Background => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.background.component.hover.into()), + card_2: Background::Color(cosmic.background.component.pressed.into()), + }, + cosmic_theme::Layer::Primary => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.primary.component.hover.into()), + card_2: Background::Color(cosmic.primary.component.pressed.into()), + }, + cosmic_theme::Layer::Secondary => crate::widget::card::style::Appearance { + card_1: Background::Color(cosmic.secondary.component.hover.into()), + card_2: Background::Color(cosmic.secondary.component.pressed.into()), + }, + } + } +} diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs new file mode 100644 index 00000000..6b350ce0 --- /dev/null +++ b/src/theme/style/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Stylesheet implements for [`crate::Theme`] + +mod button; +pub use self::button::Button; + +pub mod iced; +pub use iced::Application; +pub use iced::Checkbox; +pub use iced::Container; +pub use iced::ProgressBar; +pub use iced::Rule; +pub use iced::Svg; +pub use iced::Text; + +mod segmented_button; +pub use self::segmented_button::SegmentedButton; + +mod text_input; +pub use self::text_input::TextInput; diff --git a/src/theme/segmented_button.rs b/src/theme/style/segmented_button.rs similarity index 98% rename from src/theme/segmented_button.rs rename to src/theme/style/segmented_button.rs index 1e1f53c3..57fc62d5 100644 --- a/src/theme/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -1,6 +1,8 @@ -// Copyright 2023 System76 +// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! Contains stylesheet implementation for [`crate::widget::segmented_button`]. + use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; diff --git a/src/theme/style/text_input.rs b/src/theme/style/text_input.rs new file mode 100644 index 00000000..1970eabe --- /dev/null +++ b/src/theme/style/text_input.rs @@ -0,0 +1,328 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Contains stylesheet implementation for [`cosmic::widget::text_input`]. + +use crate::ext::ColorExt; +use crate::widget::text_input::{Appearance, StyleSheet}; +use iced_core::Color; + +#[derive(Default)] +pub enum TextInput { + #[default] + Default, + ExpandableSearch, + Search, + Inline, + Custom { + active: Box Appearance>, + error: Box Appearance>, + hovered: Box Appearance>, + focused: Box Appearance>, + disabled: Box Appearance>, + }, +} + +impl StyleSheet for crate::Theme { + type Style = TextInput; + + fn active(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let container = self.current_container(); + + let mut background: Color = container.component.base.into(); + background.a = 0.25; + + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + match style { + TextInput::Default => Appearance { + background: background.into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: None, + border_color: container.component.divider.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::ExpandableSearch => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search => Appearance { + background: background.into(), + border_radius: corner.radius_xl.into(), + border_width: 1.0, + border_offset: None, + border_color: container.component.divider.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Custom { active, .. } => active(self), + } + } + + fn error(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let container = self.current_container(); + + let mut background: Color = container.component.base.into(); + background.a = 0.25; + + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: background.into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: Some(2.0), + border_color: Color::from(palette.destructive_color()), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search | TextInput::ExpandableSearch => Appearance { + background: background.into(), + border_radius: corner.radius_xl.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::TRANSPARENT.into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Custom { error, .. } => error(self), + } + } + + fn hovered(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let container = self.current_container(); + + let mut background: Color = container.component.base.into(); + background.a = 0.25; + + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: background.into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: None, + border_color: palette.accent.base.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search => Appearance { + background: background.into(), + border_radius: corner.radius_xl.into(), + border_offset: None, + border_width: 1.0, + border_color: palette.accent.base.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::ExpandableSearch => Appearance { + background: background.into(), + border_radius: corner.radius_xl.into(), + border_offset: None, + border_width: 0.0, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::from(container.component.hover).into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Custom { hovered, .. } => hovered(self), + } + } + + fn focused(&self, style: &Self::Style) -> Appearance { + let palette = self.cosmic(); + let container = self.current_container(); + + let mut background: Color = container.component.base.into(); + background.a = 0.25; + + let corner = palette.corner_radii; + let label_color = palette.palette.neutral_9; + + match style { + TextInput::Default => Appearance { + background: background.into(), + border_radius: corner.radius_s.into(), + border_width: 1.0, + border_offset: Some(2.0), + border_color: palette.accent.base.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Search | TextInput::ExpandableSearch => Appearance { + background: background.into(), + border_radius: corner.radius_xl.into(), + border_width: 1.0, + border_offset: Some(2.0), + border_color: palette.accent.base.into(), + icon_color: container.on.into(), + text_color: container.on.into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Inline => Appearance { + background: Color::from(palette.accent.base).into(), + border_radius: corner.radius_0.into(), + border_width: 0.0, + border_offset: None, + border_color: Color::TRANSPARENT, + icon_color: container.on.into(), + // TODO use regular text color here after text rendering handles multiple colors + // in this case, for selected and unselected text + text_color: palette.on_accent_color().into(), + placeholder_color: { + let color: Color = container.on.into(); + color.blend_alpha(background, 0.7) + }, + selected_text_color: palette.on_accent_color().into(), + selected_fill: palette.accent_color().into(), + label_color: label_color.into(), + }, + TextInput::Custom { focused, .. } => focused(self), + } + } + + fn disabled(&self, style: &Self::Style) -> Appearance { + if let TextInput::Custom { disabled, .. } = style { + return disabled(self); + } + + let mut appearance = self.active(style); + + // TODO: iced will not render alpha itself on text or icon colors. + let background: Color = self.current_container().component.base.into(); + appearance.text_color = appearance.text_color.blend_alpha(background, 0.5); + appearance.icon_color = appearance.icon_color.blend_alpha(background, 0.5); + + appearance + } +}