libcosmic-yoda/src/theme/style/iced.rs
Lionel DARNIS 4743bb8ec9 yoda: cargo clippy --fix on libcosmic-yoda (115→33 warnings)
Auto-applied clippy lint suggestions across 23 files: collapse
`else { if … }` ladders, drop unneeded `return` statements, remove
needless borrows that were immediately auto-dereffed, swap `iter().next()`
for `first()`, drop redundant `.into()` to the same type, and similar
mechanical cleanups. Behavior is unchanged; the diff is -67 lines net.

The 33 remaining warnings are architectural and need manual attention
(very-complex-type aliases, too-many-arguments on internal helpers,
redundant must_use, needs-is_empty, etc.).

Leyoda 2026 – GPLv3
2026-05-05 19:10:03 +02:00

1634 lines
58 KiB
Rust

// Copyright 2022 System76 <info@system76.com>
// 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<dyn Fn(&Theme) -> Appearance>),
}
impl Application {
pub fn custom<F: Fn(&Theme) -> 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<dyn Fn(&Theme, iced_button::Status) -> 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)]
#[derive(Default)]
pub enum Checkbox {
#[default]
Primary,
Secondary,
Success,
Danger,
}
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<dyn Fn(&Theme) -> 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<F: Fn(&Theme) -> 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<iced_container::StyleFn<'a, Theme>> 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<dyn Fn(&Theme) -> slider::Style>,
hovered: Rc<dyn Fn(&Theme) -> slider::Style>,
dragging: Rc<dyn Fn(&Theme) -> 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>() -> <Self as menu::Catalog>::Class<'a> {}
fn style(&self, _class: &<Self as menu::Catalog>::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>() -> <Self as pick_list::Catalog>::Class<'a> {}
fn style(
&self,
_class: &<Self as pick_list::Catalog>::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>() -> <Self as pane_grid::Catalog>::Class<'a> {}
fn style(&self, _class: &<Self as pane_grid::Catalog>::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<dyn Fn(&Theme) -> progress_bar::Style>),
}
impl ProgressBar {
pub fn custom<F: Fn(&Theme) -> 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<dyn Fn(&Theme) -> rule::Style>),
}
impl Rule {
pub fn custom<F: Fn(&Theme) -> 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,
},
};
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,
},
};
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<dyn Fn(&Theme) -> svg::Style>),
/// No filtering is applied
#[default]
Default,
}
impl Svg {
pub fn custom<F: Fn(&Theme) -> 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<Color> 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>() -> <Self as iced_container::Catalog>::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<iced::theme::Palette> {
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,
}
}
}