// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 //! Contains stylesheet implementations for widgets native to iced. use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; use cosmic_theme::composite::over; use iced::{ overlay::menu, theme::Base, widget::{ button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, pane_grid, pick_list, progress_bar, radio, rule, scrollable, slider::{self, Rail}, svg, toggler, }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; pub mod application { use crate::Theme; use iced_runtime::Appearance; #[derive(Default)] pub enum Application { #[default] Default, Custom(Box Appearance>), } impl Application { pub fn custom Appearance + 'static>(f: F) -> Self { Self::Custom(Box::new(f)) } } pub fn style(theme: &Theme) -> iced::theme::Style { let cosmic = theme.cosmic(); iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), icon_color: cosmic.on_bg_color().into(), } } } /// 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(Box iced_button::Style>), } impl iced_button::Catalog for Theme { type Class<'a> = Button; fn default<'a>() -> Self::Class<'a> { Button::default() } fn style(&self, class: &Self::Class<'_>, status: iced_button::Status) -> iced_button::Style { if let Button::Custom(f) = class { return f(self, status); } let cosmic = self.cosmic(); let corner_radii = &cosmic.corner_radii; let component = class.cosmic(self); let mut appearance = iced_button::Style { border_radius: match class { Button::Link => corner_radii.radius_0.into(), Button::Card => corner_radii.radius_xs.into(), _ => corner_radii.radius_xl.into(), }, border: Border { radius: match class { Button::Link => corner_radii.radius_0.into(), Button::Card => corner_radii.radius_xs.into(), _ => corner_radii.radius_xl.into(), }, ..Default::default() }, background: match class { Button::Link | Button::Text => None, Button::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.base.into())), }, text_color: match class { Button::Link | Button::LinkActive => component.base.into(), _ => component.on.into(), }, ..iced_button::Style::default() }; match status { iced_button::Status::Active => {} iced_button::Status::Hovered => { appearance.background = match class { Button::Link => None, Button::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.hover.into())), }; } iced_button::Status::Pressed => { appearance.background = match class { Button::Link => None, Button::LinkActive => Some(Background::Color(component.divider.into())), _ => Some(Background::Color(component.pressed.into())), }; } iced_button::Status::Disabled => { // Card color is not transparent when it isn't clickable if matches!(class, Button::Card) { return appearance; } appearance.background = appearance.background.map(|background| match background { Background::Color(color) => Background::Color(Color { a: color.a * 0.5, ..color }), Background::Gradient(gradient) => { Background::Gradient(gradient.scale_alpha(0.5)) } }); appearance.text_color = Color { a: appearance.text_color.a * 0.5, ..appearance.text_color }; } }; appearance } } impl Button { #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::match_same_arms)] fn cosmic<'a>(&'a self, theme: &'a Theme) -> &'a CosmicComponent { let cosmic = theme.cosmic(); match self { Self::Primary => &cosmic.accent_button, Self::Secondary => &theme.current_container().component, Self::Positive => &cosmic.success_button, Self::Destructive => &cosmic.destructive_button, Self::Text => &cosmic.text_button, Self::Link => &cosmic.link_button, Self::LinkActive => &cosmic.link_button, Self::Transparent => &TRANSPARENT_COMPONENT, Self::Deactivated => &theme.current_container().component, Self::Card => &theme.current_container().component, Self::Custom { .. } => &TRANSPARENT_COMPONENT, } } } /* * 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 iced_checkbox::Catalog for Theme { type Class<'a> = Checkbox; fn default<'a>() -> Self::Class<'a> { Checkbox::default() } #[allow(clippy::too_many_lines)] fn style( &self, class: &Self::Class<'_>, status: iced_checkbox::Status, ) -> iced_checkbox::Style { let cosmic = self.cosmic(); let corners = &cosmic.corner_radii; let disabled = matches!(status, iced_checkbox::Status::Disabled { .. }); match status { iced_checkbox::Status::Active { is_checked } | iced_checkbox::Status::Disabled { is_checked } => { let mut active = match class { Checkbox::Primary => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.accent.base.into() } else { self.current_container().small_widget.into() }), icon_color: cosmic.accent.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.accent.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, Checkbox::Secondary => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.background.component.base.into() } else { self.current_container().small_widget.into() }), icon_color: cosmic.background.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: cosmic.palette.neutral_8.into(), }, text_color: None, }, Checkbox::Success => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.success.base.into() } else { self.current_container().small_widget.into() }), icon_color: cosmic.success.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.success.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, Checkbox::Danger => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.destructive.base.into() } else { self.current_container().small_widget.into() }), icon_color: cosmic.destructive.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.destructive.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, }; if disabled { match &mut active.background { Background::Color(color) => { color.a /= 2.; } Background::Gradient(gradient) => { *gradient = gradient.scale_alpha(0.5); } } if let Some(c) = active.text_color.as_mut() { c.a /= 2. }; active.border.color.a /= 2.; } active } iced_checkbox::Status::Hovered { is_checked } => { let cur_container = self.current_container().small_widget; // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. let hovered_bg = over(cosmic.palette.neutral_0.with_alpha(0.1), cur_container); match class { Checkbox::Primary => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.accent.hover_state_color().into() } else { hovered_bg.into() }), icon_color: cosmic.accent.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.accent.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, Checkbox::Secondary => iced_checkbox::Style { background: Background::Color(if is_checked { self.current_container().component.hover.into() } else { hovered_bg.into() }), icon_color: self.current_container().on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { self.current_container().base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, Checkbox::Success => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.success.hover.into() } else { hovered_bg.into() }), icon_color: cosmic.success.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.success.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, Checkbox::Danger => iced_checkbox::Style { background: Background::Color(if is_checked { cosmic.destructive.hover.into() } else { hovered_bg.into() }), icon_color: cosmic.destructive.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, color: if is_checked { cosmic.destructive.base } else { cosmic.palette.neutral_8 } .into(), }, text_color: None, }, } } } } } /* * TODO: Container */ #[derive(Default)] pub enum Container<'a> { WindowBackground, Background, Card, ContextDrawer, Custom(Box iced_container::Style + 'a>), Dialog, Dropdown, HeaderBar { focused: bool, sharp_corners: bool, transparent: bool, }, List, Primary, Secondary, Tooltip, #[default] Transparent, } impl<'a> Container<'a> { pub fn custom iced_container::Style + 'a>(f: F) -> Self { Self::Custom(Box::new(f)) } #[must_use] pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { icon_color: Some(Color::from(theme.background.on)), text_color: Some(Color::from(theme.background.on)), background: Some(iced::Background::Color(theme.background.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, } } #[must_use] pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { icon_color: Some(Color::from(theme.primary.on)), text_color: Some(Color::from(theme.primary.on)), background: Some(iced::Background::Color(theme.primary.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, } } #[must_use] pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { iced_container::Style { icon_color: Some(Color::from(theme.secondary.on)), text_color: Some(Color::from(theme.secondary.on)), background: Some(iced::Background::Color(theme.secondary.base.into())), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, } } } impl<'a> From> for Container<'a> { fn from(value: iced_container::StyleFn<'a, Theme>) -> Self { Self::custom(value) } } impl iced_container::Catalog for Theme { type Class<'a> = Container<'a>; fn default<'a>() -> Self::Class<'a> { Container::default() } fn style(&self, class: &Self::Class<'_>) -> iced_container::Style { let cosmic = self.cosmic(); // Ensures visually aligned radii for content and window corners let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); match class { Container::Transparent => iced_container::Style::default(), Container::Custom(f) => f(self), Container::WindowBackground => iced_container::Style { icon_color: Some(Color::from(cosmic.background.on)), text_color: Some(Color::from(cosmic.background.on)), background: Some(iced::Background::Color(cosmic.background.base.into())), border: Border { radius: [ cosmic.corner_radii.radius_0[0], cosmic.corner_radii.radius_0[1], window_corner_radius[2], window_corner_radius[3], ] .into(), ..Default::default() }, shadow: Shadow::default(), snap: true, }, Container::List => { let component = &self.current_container().component; iced_container::Style { icon_color: Some(component.on.into()), text_color: Some(component.on.into()), background: Some(Background::Color(component.base.into())), border: iced::Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, } } Container::HeaderBar { focused, sharp_corners, transparent, } => { let (icon_color, text_color) = if *focused { ( Color::from(cosmic.accent_text_color()), Color::from(cosmic.background.on), ) } else { use crate::ext::ColorExt; let unfocused_color = Color::from(cosmic.background.component.on) .blend_alpha(cosmic.background.base.into(), 0.5); (unfocused_color, unfocused_color) }; iced_container::Style { icon_color: Some(icon_color), text_color: Some(text_color), background: if *transparent { None } else { Some(iced::Background::Color(cosmic.background.base.into())) }, border: Border { radius: [ if *sharp_corners { cosmic.corner_radii.radius_0[0] } else { window_corner_radius[0] }, if *sharp_corners { cosmic.corner_radii.radius_0[1] } else { window_corner_radius[1] }, cosmic.corner_radii.radius_0[2], cosmic.corner_radii.radius_0[3], ] .into(), ..Default::default() }, snap: true, shadow: Shadow::default(), } } Container::ContextDrawer => { let mut a = Container::primary(cosmic); if cosmic.is_high_contrast { a.border.width = 1.; a.border.color = cosmic.primary.divider.into(); } a } Container::Background => Container::background(cosmic), Container::Primary => Container::primary(cosmic), Container::Secondary => Container::secondary(cosmic), Container::Dropdown => iced_container::Style { icon_color: None, text_color: None, background: Some(iced::Background::Color(cosmic.bg_component_color().into())), border: Border { color: cosmic.bg_component_divider().into(), width: 1.0, radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), snap: true, }, Container::Tooltip => iced_container::Style { icon_color: None, text_color: None, background: Some(iced::Background::Color(cosmic.palette.neutral_2.into())), border: Border { radius: cosmic.corner_radii.radius_l.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, }, Container::Card => { let cosmic = self.cosmic(); match self.layer { cosmic_theme::Layer::Background => iced_container::Style { icon_color: Some(Color::from(cosmic.background.component.on)), text_color: Some(Color::from(cosmic.background.component.on)), background: Some(iced::Background::Color( cosmic.background.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.component.on)), text_color: Some(Color::from(cosmic.primary.component.on)), background: Some(iced::Background::Color( cosmic.primary.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { icon_color: Some(Color::from(cosmic.secondary.component.on)), text_color: Some(Color::from(cosmic.secondary.component.on)), background: Some(iced::Background::Color( cosmic.secondary.component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, shadow: Shadow::default(), snap: true, }, } } Container::Dialog => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.on)), text_color: Some(Color::from(cosmic.primary.on)), background: Some(iced::Background::Color(cosmic.primary.base.into())), border: Border { color: cosmic.primary.divider.into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, shadow: Shadow { color: cosmic.shade.into(), offset: Vector::new(0.0, 4.0), blur_radius: 16.0, }, snap: true, }, } } } #[derive(Default)] pub enum Slider { #[default] Standard, Custom { active: Rc slider::Style>, hovered: Rc slider::Style>, dragging: Rc slider::Style>, }, } /* * Slider */ impl slider::Catalog for Theme { type Class<'a> = Slider; fn default<'a>() -> Self::Class<'a> { Slider::default() } fn style(&self, class: &Self::Class<'_>, status: slider::Status) -> slider::Style { let cosmic: &cosmic_theme::Theme = self.cosmic(); let hc = self.theme_type.is_high_contrast(); let is_dark = self.theme_type.is_dark(); let mut appearance = match class { Slider::Standard => //TODO: no way to set rail thickness { let (active_track, inactive_track) = if hc { ( cosmic.accent_text_color(), if is_dark { cosmic.palette.neutral_5 } else { cosmic.palette.neutral_3 }, ) } else { (cosmic.accent.base, cosmic.palette.neutral_6) }; slider::Style { rail: Rail { backgrounds: ( Background::Color(active_track.into()), Background::Color(inactive_track.into()), ), border: Border { radius: cosmic.corner_radii.radius_xs.into(), color: if hc && !is_dark { self.current_container().component.border.into() } else { Color::TRANSPARENT }, width: if hc && !is_dark { 1. } else { 0. }, }, width: 4.0, }, handle: slider::Handle { shape: slider::HandleShape::Rectangle { height: 20, width: 20, border_radius: cosmic.corner_radii.radius_m.into(), }, border_color: Color::TRANSPARENT, border_width: 0.0, background: Background::Color(cosmic.accent.base.into()), }, breakpoint: slider::Breakpoint { color: cosmic.on_bg_color().into(), }, } } Slider::Custom { active, .. } => active(self), }; match status { slider::Status::Active => appearance, slider::Status::Hovered => match class { Slider::Standard => { appearance.handle.shape = slider::HandleShape::Rectangle { height: 26, width: 26, border_radius: cosmic.corner_radii.radius_m.into(), }; appearance.handle.border_width = 3.0; appearance.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance } Slider::Custom { hovered, .. } => hovered(self), }, slider::Status::Dragged => match class { Slider::Standard => { let mut style = { appearance.handle.shape = slider::HandleShape::Rectangle { height: 26, width: 26, border_radius: cosmic.corner_radii.radius_m.into(), }; appearance.handle.border_width = 3.0; appearance.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance }; style.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.2).into(); style } Slider::Custom { dragging, .. } => dragging(self), }, } } } impl menu::Catalog for Theme { type Class<'a> = (); fn default<'a>() -> ::Class<'a> {} fn style(&self, class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); menu::Style { text_color: cosmic.on_bg_color().into(), background: Background::Color(cosmic.background.base.into()), border: Border { radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.background.component.hover.into()), shadow: Default::default(), } } } impl pick_list::Catalog for Theme { type Class<'a> = (); fn default<'a>() -> ::Class<'a> {} fn style( &self, class: &::Class<'_>, status: pick_list::Status, ) -> pick_list::Style { let cosmic = &self.cosmic(); let hc = cosmic.is_high_contrast; let appearance = pick_list::Style { text_color: cosmic.on_bg_color().into(), background: Color::TRANSPARENT.into(), placeholder_color: cosmic.on_bg_color().into(), border: Border { radius: cosmic.corner_radii.radius_m.into(), width: if hc { 1. } else { 0. }, color: if hc { self.current_container().component.border.into() } else { Color::TRANSPARENT }, }, // icon_size: 0.7, // TODO: how to replace handle_color: cosmic.on_bg_color().into(), }; match status { pick_list::Status::Active => appearance, pick_list::Status::Hovered => pick_list::Style { background: Background::Color(cosmic.background.base.into()), ..appearance }, pick_list::Status::Opened { is_hovered: _ } => appearance, } } } /* * TODO: Radio */ impl radio::Catalog for Theme { type Class<'a> = (); fn default<'a>() -> Self::Class<'a> {} fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let cur_container = self.current_container(); let theme = self.cosmic(); match status { radio::Status::Active { is_selected } => radio::Style { background: if is_selected { Color::from(theme.accent.base).into() } else { // TODO: this seems to be defined weirdly in FIGMA Color::from(cur_container.small_widget).into() }, dot_color: theme.accent.on.into(), border_width: 1.0, border_color: if is_selected { Color::from(theme.accent.base) } else { Color::from(theme.palette.neutral_8) }, text_color: None, }, radio::Status::Hovered { is_selected } => { let bg = if is_selected { theme.accent.base } else { self.current_container().small_widget }; // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. let hovered_bg = Color::from(over(theme.palette.neutral_0.with_alpha(0.1), bg)); radio::Style { background: hovered_bg.into(), dot_color: theme.accent.on.into(), border_width: 1.0, border_color: if is_selected { Color::from(theme.accent.base) } else { Color::from(theme.palette.neutral_8) }, text_color: None, } } } } } /* * Toggler */ impl toggler::Catalog for Theme { type Class<'a> = (); fn default<'a>() -> Self::Class<'a> {} fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1); let mut active = toggler::Style { background: if matches!(status, toggler::Status::Active { is_toggled: true }) { cosmic.accent.base.into() } else if cosmic.is_dark { cosmic.palette.neutral_6.into() } else { cosmic.palette.neutral_5.into() }, foreground: cosmic.palette.neutral_2.into(), border_radius: cosmic.radius_xl().into(), handle_radius: cosmic .radius_xl() .map(|x| (x - HANDLE_MARGIN).max(0.0)) .into(), handle_margin: HANDLE_MARGIN, background_border_width: 0.0, background_border_color: Color::TRANSPARENT, foreground_border_width: 0.0, foreground_border_color: Color::TRANSPARENT, text_color: None, padding_ratio: 0.0, }; match status { toggler::Status::Active { is_toggled } => active, toggler::Status::Hovered { is_toggled } => { let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); toggler::Style { background: if is_active { over(neutral_10, cosmic.accent_color()) } else { over( neutral_10, if cosmic.is_dark { cosmic.palette.neutral_6 } else { cosmic.palette.neutral_5 }, ) } .into(), ..active } } toggler::Status::Disabled { is_toggled } => { active.background = active.background.scale_alpha(0.5); active.foreground = active.foreground.scale_alpha(0.5); active } } } } /* * TODO: Pane Grid */ impl pane_grid::Catalog for Theme { type Class<'a> = (); fn default<'a>() -> ::Class<'a> {} fn style(&self, class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); pane_grid::Style { hovered_region: Highlight { background: Background::Color(theme.bg_color().into()), border: Border { radius: theme.corner_radii.radius_0.into(), width: 2.0, color: theme.bg_divider().into(), }, }, picked_split: pane_grid::Line { color: theme.accent.base.into(), width: 2.0, }, hovered_split: pane_grid::Line { color: theme.accent.hover.into(), width: 2.0, }, } } } /* * TODO: Progress Bar */ #[derive(Default)] pub enum ProgressBar { #[default] Primary, Success, Danger, Custom(Box progress_bar::Style>), } impl ProgressBar { pub fn custom progress_bar::Style + 'static>(f: F) -> Self { Self::Custom(Box::new(f)) } } impl progress_bar::Catalog for Theme { type Class<'a> = ProgressBar; fn default<'a>() -> Self::Class<'a> { ProgressBar::default() } fn style(&self, class: &Self::Class<'_>) -> progress_bar::Style { let theme = self.cosmic(); let (active_track, inactive_track) = if theme.is_high_contrast { ( theme.accent_text_color(), if theme.is_dark { theme.palette.neutral_6 } else { theme.palette.neutral_4 }, ) } else { (theme.accent.base, theme.background.divider) }; let border = Border { radius: theme.corner_radii.radius_xl.into(), color: if theme.is_high_contrast && !theme.is_dark { self.current_container().component.border.into() } else { Color::TRANSPARENT }, width: if theme.is_high_contrast && !theme.is_dark { 1. } else { 0. }, }; match class { ProgressBar::Primary => progress_bar::Style { background: Color::from(inactive_track).into(), bar: Color::from(active_track).into(), border, }, ProgressBar::Success => progress_bar::Style { background: Color::from(inactive_track).into(), bar: Color::from(theme.success.base).into(), border, }, ProgressBar::Danger => progress_bar::Style { background: Color::from(inactive_track).into(), bar: Color::from(theme.destructive.base).into(), border, }, ProgressBar::Custom(f) => f(self), } } } /* * TODO: Rule */ #[derive(Default)] pub enum Rule { #[default] Default, LightDivider, HeavyDivider, Custom(Box rule::Style>), } impl Rule { pub fn custom rule::Style + 'static>(f: F) -> Self { Self::Custom(Box::new(f)) } } impl rule::Catalog for Theme { type Class<'a> = Rule; fn default<'a>() -> Self::Class<'a> { Rule::default() } fn style(&self, class: &Self::Class<'_>) -> rule::Style { match class { Rule::Default => rule::Style { color: self.current_container().divider.into(), radius: 0.0.into(), fill_mode: rule::FillMode::Full, snap: true, }, Rule::LightDivider => rule::Style { color: self.current_container().divider.into(), radius: 0.0.into(), fill_mode: rule::FillMode::Padded(8), snap: true, }, Rule::HeavyDivider => rule::Style { color: self.current_container().divider.into(), radius: 2.0.into(), fill_mode: rule::FillMode::Full, snap: true, }, Rule::Custom(f) => f(self), } } } #[derive(Default, Clone, Copy)] pub enum Scrollable { #[default] Permanent, Minimal, } /* * TODO: Scrollable */ impl scrollable::Catalog for Theme { type Class<'a> = Scrollable; fn default<'a>() -> Self::Class<'a> { Scrollable::default() } fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { scrollable::Status::Active { is_horizontal_scrollbar_disabled, is_vertical_scrollbar_disabled, } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); let mut a = scrollable::Style { container: iced_container::transparent(self), vertical_rail: scrollable::Rail { border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, background: None, scroller: scrollable::Scroller { background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, }, }, horizontal_rail: scrollable::Rail { border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, background: None, scroller: scrollable::Scroller { background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, }, }, gap: None, // TODO: what is auto scroll? auto_scroll: AutoScroll { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), icon: Color::TRANSPARENT.into(), }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); if matches!(class, Scrollable::Permanent) { a.horizontal_rail.background = Some(Background::Color(small_widget_container.into())); a.vertical_rail.background = Some(Background::Color(small_widget_container.into())); } a } // TODO handle vertical / horizontal scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); // if is_mouse_over_scrollbar { // let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2); // neutral_5 = over(hover_overlay, neutral_5); // } let mut a: scrollable::Style = scrollable::Style { container: iced_container::Style::default(), vertical_rail: scrollable::Rail { border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, background: None, scroller: scrollable::Scroller { background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, }, }, horizontal_rail: scrollable::Rail { border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, background: None, scroller: scrollable::Scroller { background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() }, }, }, gap: None, // TODO: what is auto scroll? auto_scroll: AutoScroll { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), icon: Color::TRANSPARENT.into(), }, }; if matches!(class, Scrollable::Permanent) { let small_widget_container = self.current_container().small_widget.with_alpha(0.7); a.horizontal_rail.background = Some(Background::Color(small_widget_container.into())); a.vertical_rail.background = Some(Background::Color(small_widget_container.into())); } a } } } } #[derive(Clone, Default)] pub enum Svg { /// Apply a custom appearance filter Custom(Rc svg::Style>), /// No filtering is applied #[default] Default, } impl Svg { pub fn custom svg::Style + 'static>(f: F) -> Self { Self::Custom(Rc::new(f)) } } impl svg::Catalog for Theme { type Class<'a> = Svg; fn default<'a>() -> Self::Class<'a> { Svg::default() } fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { #[allow(clippy::match_same_arms)] match class { Svg::Default => svg::Style::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::Style), } impl From for Text { fn from(color: Color) -> Self { Self::Color(color) } } impl iced_widget::text::Catalog for Theme { type Class<'a> = Text; fn default<'a>() -> Self::Class<'a> { Text::default() } fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { match class { Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent_text_color().into()), }, Text::Default => iced_widget::text::Style { color: None }, Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, Text::Custom(f) => f(self), } } } #[derive(Copy, Clone, Default)] pub enum TextInput { #[default] Default, Search, } /* * TODO: Text Input */ impl text_input::Catalog for Theme { type Class<'a> = TextInput; fn default<'a>() -> Self::Class<'a> { TextInput::default() } fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { let palette = self.cosmic(); let bg = self.current_container().small_widget.with_alpha(0.25); let neutral_9 = palette.palette.neutral_9; let value = neutral_9.into(); let placeholder = neutral_9.with_alpha(0.7).into(); let selection = palette.accent.base.into(); let mut appearance = match class { TextInput::Default => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_s.into(), width: 1.0, color: self.current_container().component.divider.into(), }, icon: self.current_container().on.into(), placeholder, value, selection, }, TextInput::Search => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_m.into(), ..Default::default() }, icon: self.current_container().on.into(), placeholder, value, selection, }, }; match status { text_input::Status::Active => appearance, text_input::Status::Hovered => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_s.into(), width: 1.0, color: self.current_container().on.into(), }, icon: self.current_container().on.into(), placeholder, value, selection, }, TextInput::Search => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_m.into(), ..Default::default() }, icon: self.current_container().on.into(), placeholder, value, selection, }, } } text_input::Status::Focused { is_hovered } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_s.into(), width: 1.0, color: palette.accent.base.into(), }, icon: self.current_container().on.into(), placeholder, value, selection, }, TextInput::Search => text_input::Style { background: Color::from(bg).into(), border: Border { radius: palette.corner_radii.radius_m.into(), ..Default::default() }, icon: self.current_container().on.into(), placeholder, value, selection, }, } } text_input::Status::Disabled => { appearance.background = match appearance.background { Background::Color(color) => Background::Color(Color { a: color.a * 0.5, ..color }), Background::Gradient(gradient) => { Background::Gradient(gradient.scale_alpha(0.5)) } }; appearance.border.color.a /= 2.; appearance.icon.a /= 2.; appearance.placeholder.a /= 2.; appearance.value.a /= 2.; appearance } } } } #[derive(Default)] pub enum TextEditor<'a> { #[default] Default, Custom(text_editor::StyleFn<'a, Theme>), } impl iced_widget::text_editor::Catalog for Theme { type Class<'a> = TextEditor<'a>; fn default<'a>() -> Self::Class<'a> { TextEditor::default() } fn style( &self, class: &Self::Class<'_>, status: iced_widget::text_editor::Status, ) -> iced_widget::text_editor::Style { if let TextEditor::Custom(style) = class { return style(self, status); } let cosmic = self.cosmic(); let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); let icon: Color = cosmic.background.on.into(); // TODO do we need to add icon color back? match status { iced_widget::text_editor::Status::Active | iced_widget::text_editor::Status::Hovered | iced_widget::text_editor::Status::Disabled => iced_widget::text_editor::Style { background: iced::Color::from(cosmic.bg_color()).into(), border: Border { radius: cosmic.corner_radii.radius_0.into(), width: f32::from(cosmic.space_xxxs()), color: iced::Color::from(cosmic.bg_divider()), }, placeholder, value, selection, }, iced_widget::text_editor::Status::Focused { is_hovered } => { iced_widget::text_editor::Style { background: iced::Color::from(cosmic.bg_color()).into(), border: Border { radius: cosmic.corner_radii.radius_0.into(), width: f32::from(cosmic.space_xxxs()), color: iced::Color::from(cosmic.accent.base), }, placeholder, value, selection, } } } } } #[cfg(feature = "markdown")] impl iced_widget::markdown::Catalog for Theme { fn code_block<'a>() -> ::Class<'a> { Container::custom(|_| iced_container::Style { background: Some(iced::color!(0x111111).into()), text_color: Some(Color::WHITE), border: iced::border::rounded(2), ..iced_container::Style::default() }) } } impl iced_widget::table::Catalog for Theme { type Class<'a> = iced_widget::table::StyleFn<'a, Self>; fn default<'a>() -> Self::Class<'a> { Box::new(|theme| iced_widget::table::Style { separator_x: theme.current_container().divider.into(), separator_y: theme.current_container().divider.into(), }) } fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style { class(self) } } #[cfg(feature = "qr_code")] impl iced_widget::qr_code::Catalog for Theme { type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>; fn default<'a>() -> Self::Class<'a> { Box::new(|_theme| iced_widget::qr_code::Style { cell: Color::BLACK, background: Color::WHITE, }) } fn style(&self, class: &Self::Class<'_>) -> iced_widget::qr_code::Style { class(self) } } impl combo_box::Catalog for Theme {} impl Base for Theme { fn default(preference: iced::theme::Mode) -> Self { match preference { iced::theme::Mode::Light => Theme::light(), iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(), } } fn mode(&self) -> iced::theme::Mode { if self.theme_type.is_dark() { iced::theme::Mode::Dark } else { iced::theme::Mode::Light } } fn base(&self) -> iced::theme::Style { iced::theme::Style { background_color: self.cosmic().bg_color().into(), text_color: self.cosmic().on_bg_color().into(), icon_color: self.cosmic().on_bg_color().into(), } } fn palette(&self) -> Option { Some(iced::theme::Palette { primary: self.cosmic().accent.base.into(), success: self.cosmic().success.base.into(), warning: self.cosmic().warning.base.into(), danger: self.cosmic().destructive.base.into(), background: iced::Color::from(self.cosmic().bg_color()), text: iced::Color::from(self.cosmic().on_bg_color()), }) } fn name(&self) -> &str { match &self.theme_type { crate::theme::ThemeType::Dark => "Cosmic Dark Theme", crate::theme::ThemeType::Light => "Cosmic Light Theme", crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, } } }