libcosmic/cosmic-theme/src/model/theme.rs
2026-04-15 15:06:07 -04:00

1628 lines
48 KiB
Rust

use crate::{
Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE,
LIGHT_PALETTE, NAME, Spacing, ThemeMode,
color::{ColorRepr, ColorReprOption, color_serde, color_serde::option as color_serde_option},
composite::over,
steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps},
};
use cosmic_config::{Config, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry};
use palette::{
IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb,
};
use serde::{Deserialize, Serialize};
use std::{default, num::NonZeroUsize};
/// ID for the current dark `ThemeBuilder` config
pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder";
/// ID for the current dark Theme config
pub const DARK_THEME_ID: &str = "com.system76.CosmicTheme.Dark";
/// ID for the current light `ThemeBuilder`` config
pub const LIGHT_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Light.Builder";
/// ID for the current light Theme config
pub const LIGHT_THEME_ID: &str = "com.system76.CosmicTheme.Light";
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
/// Theme layer type
pub enum Layer {
/// Background layer
#[default]
Background,
/// Primary Layer
Primary,
/// Secondary Layer
Secondary,
}
#[must_use]
/// Cosmic Theme data structure with all colors and its name
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, CosmicConfigEntry)]
#[version = 2]
pub struct Theme {
/// name of the theme
pub name: String,
/// background element colors
pub(crate) background: Container,
/// primary element colors
pub(crate) primary: Container,
/// secondary element colors
pub(crate) secondary: Container,
/// background element colors
pub(crate) transparent_background: Container,
/// primary element colors
pub(crate) transparent_primary: Container,
/// secondary element colors
pub(crate) transparent_secondary: Container,
/// button component styling
pub button: Component,
/// accent element colors
pub accent: Component,
/// suggested element colors
pub success: Component,
/// destructive element colors
pub destructive: Component,
/// warning element colors
pub warning: Component,
/// accent button element colors
pub accent_button: Component,
/// suggested button element colors
pub success_button: Component,
/// destructive button element colors
pub destructive_button: Component,
/// warning button element colors
pub warning_button: Component,
/// icon button element colors
pub icon_button: Component,
/// link button element colors
pub link_button: Component,
/// text button element colors
pub text_button: Component,
/// palette
pub palette: CosmicPaletteInner,
/// spacing
pub spacing: Spacing,
/// corner radii
pub corner_radii: CornerRadii,
/// is dark
pub is_dark: bool,
/// is high contrast
pub is_high_contrast: bool,
/// cosmic-comp window gaps size (outer, inner)
pub gaps: (u32, u32),
/// cosmic-comp active hint window outline width
pub active_hint: u32,
/// cosmic-comp custom window hint color
pub window_hint: Option<Srgb>,
/// enables blurred transparency
pub frosted: BlurStrength,
/// frosted windows
pub frosted_windows: bool,
/// frosted system interface
pub frosted_system_interface: bool,
/// frosted panel
pub frosted_panel: bool,
/// frosted applet popups
pub frosted_applets: bool,
/// shade color for dialogs
#[serde(with = "color_serde")]
#[cosmic_config_entry(with = ColorRepr)]
pub shade: Srgba,
/// accent text colors
/// If None, accent base color is the accent text color.
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub accent_text: Option<Srgba>,
/// control tint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub control_tint: Option<Srgb>,
/// text tint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub text_tint: Option<Srgb>,
}
impl Default for Theme {
#[inline]
fn default() -> Self {
Self::preferred_theme()
}
}
/// Trait for layered themes
pub trait LayeredTheme {
/// Set the layer of the theme
fn set_layer(&mut self, layer: Layer);
}
impl Theme {
#[must_use]
/// id of the theme
pub fn id() -> &'static str {
NAME
}
#[inline]
/// Get the config for the current dark theme
pub fn dark_config() -> Result<Config, cosmic_config::Error> {
Config::new(DARK_THEME_ID, Self::VERSION)
}
#[inline]
/// Get the config for the current light theme
pub fn light_config() -> Result<Config, cosmic_config::Error> {
Config::new(LIGHT_THEME_ID, Self::VERSION)
}
#[inline]
/// get the built in light theme
pub fn light_default() -> Self {
LIGHT_PALETTE.clone().into()
}
#[inline]
/// get the built in dark theme
pub fn dark_default() -> Self {
DARK_PALETTE.clone().into()
}
#[inline]
/// get the built in high contrast dark theme
pub fn high_contrast_dark_default() -> Self {
CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into()
}
#[inline]
/// get the built in high contrast light theme
pub fn high_contrast_light_default() -> Self {
CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into()
}
#[inline]
/// Convert the theme to a high-contrast variant
pub fn to_high_contrast(&self) -> Self {
todo!();
}
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent background based on whether blur is active
pub fn background(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_background
} else {
&self.background
}
}
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent primary based on whether blur is active
pub fn primary(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_primary
} else {
&self.primary
}
}
#[allow(clippy::doc_markdown)]
#[inline]
/// get opaque or transparent secondary based on whether blur is active
pub fn secondary(&self, transparent: bool) -> &Container {
if transparent {
&self.transparent_secondary
} else {
&self.secondary
}
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_0 color
pub fn control_0(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_0)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_1 color
pub fn control_1(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_1)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_2 color
pub fn control_2(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_2)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_3(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_3)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_4(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_4)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_5(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_5)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_6(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_6)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_7(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_7)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_8(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_8)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_9(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_9)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get control_3 color
pub fn control_10(&self) -> Srgba {
self.tint_neutral(self.palette.neutral_10)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @accent_color
fn tint_neutral(&self, neutral: Srgba) -> Srgba {
let Some(tint) = self.control_tint else {
return neutral;
};
let mut oklch_neutral: Oklcha = neutral.into_color();
let oklch_tint: Oklcha = tint.into_color();
oklch_neutral.hue = oklch_tint.hue;
oklch_neutral.chroma = oklch_tint.chroma;
oklch_neutral.into_color()
}
// TODO convenient getter functions for each named color variable
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @accent_color
pub fn accent_color(&self) -> Srgba {
self.accent.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @success_color
pub fn success_color(&self) -> Srgba {
self.success.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @destructive_color
pub fn destructive_color(&self) -> Srgba {
self.destructive.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @warning_color
pub fn warning_color(&self) -> Srgba {
self.warning.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @small_widget_divider
pub fn small_widget_divider(&self) -> Srgba {
self.palette.neutral_9.with_alpha(0.2)
}
// Containers
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @bg_color
pub fn bg_color(&self) -> Srgba {
self.background.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @bg_component_color
pub fn bg_component_color(&self) -> Srgba {
self.background.component.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @primary_container_color
pub fn primary_container_color(&self) -> Srgba {
self.primary.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @primary_component_color
pub fn primary_component_color(&self) -> Srgba {
self.primary.component.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @secondary_container_color
pub fn secondary_container_color(&self) -> Srgba {
self.secondary.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @secondary_component_color
pub fn secondary_component_color(&self) -> Srgba {
self.secondary.component.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @button_bg_color
pub fn button_bg_color(&self) -> Srgba {
self.button.base
}
// Text
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_bg_color
pub fn on_bg_color(&self) -> Srgba {
self.background.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_bg_component_color
pub fn on_bg_component_color(&self) -> Srgba {
self.background.component.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_primary_color
pub fn on_primary_container_color(&self) -> Srgba {
self.primary.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_primary_component_color
pub fn on_primary_component_color(&self) -> Srgba {
self.primary.component.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_secondary_color
pub fn on_secondary_container_color(&self) -> Srgba {
self.secondary.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_secondary_component_color
pub fn on_secondary_component_color(&self) -> Srgba {
self.secondary.component.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @accent_text_color
pub fn accent_text_color(&self) -> Srgba {
self.accent_text.unwrap_or(self.accent.base)
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @success_text_color
pub fn success_text_color(&self) -> Srgba {
self.success.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @warning_text_color
pub fn warning_text_color(&self) -> Srgba {
self.warning.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @destructive_text_color
pub fn destructive_text_color(&self) -> Srgba {
self.destructive.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_accent_color
pub fn on_accent_color(&self) -> Srgba {
self.accent.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_success_color
pub fn on_success_color(&self) -> Srgba {
self.success.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_warning_color
pub fn on_warning_color(&self) -> Srgba {
self.warning.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @on_destructive_color
pub fn on_destructive_color(&self) -> Srgba {
self.destructive.on
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @button_color
pub fn button_color(&self) -> Srgba {
self.button.on
}
// Borders and Dividers
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @bg_divider
pub fn bg_divider(&self) -> Srgba {
self.background.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @bg_component_divider
pub fn bg_component_divider(&self) -> Srgba {
self.background.component.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @primary_container_divider
pub fn primary_container_divider(&self) -> Srgba {
self.primary.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @primary_component_divider
pub fn primary_component_divider(&self) -> Srgba {
self.primary.component.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @secondary_container_divider
pub fn secondary_container_divider(&self) -> Srgba {
self.secondary.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @button_divider
pub fn button_divider(&self) -> Srgba {
self.button.divider
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @window_header_bg
pub fn window_header_bg(&self) -> Srgba {
self.background.base
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_none
pub fn space_none(&self) -> u16 {
self.spacing.space_none
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xxxs
pub fn space_xxxs(&self) -> u16 {
self.spacing.space_xxxs
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xxs
pub fn space_xxs(&self) -> u16 {
self.spacing.space_xxs
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xs
pub fn space_xs(&self) -> u16 {
self.spacing.space_xs
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_s
pub fn space_s(&self) -> u16 {
self.spacing.space_s
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_m
pub fn space_m(&self) -> u16 {
self.spacing.space_m
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_l
pub fn space_l(&self) -> u16 {
self.spacing.space_l
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xl
pub fn space_xl(&self) -> u16 {
self.spacing.space_xl
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xxl
pub fn space_xxl(&self) -> u16 {
self.spacing.space_xxl
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @space_xxxl
pub fn space_xxxl(&self) -> u16 {
self.spacing.space_xxxl
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_0
pub fn radius_0(&self) -> [f32; 4] {
self.corner_radii.radius_0
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_xs
pub fn radius_xs(&self) -> [f32; 4] {
self.corner_radii.radius_xs
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_s
pub fn radius_s(&self) -> [f32; 4] {
self.corner_radii.radius_s
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_m
pub fn radius_m(&self) -> [f32; 4] {
self.corner_radii.radius_m
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_l
pub fn radius_l(&self) -> [f32; 4] {
self.corner_radii.radius_l
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @radius_xl
pub fn radius_xl(&self) -> [f32; 4] {
self.corner_radii.radius_xl
}
#[must_use]
#[allow(clippy::doc_markdown)]
#[inline]
/// get @shade_color
pub fn shade_color(&self) -> Srgba {
self.shade
}
/// Get the active theme based on the current theme mode.
pub fn get_active() -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
(|| {
(if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? {
Self::dark_config
} else {
Self::light_config
})()
})()
.map_err(|error| (vec![error], Self::default()))
.and_then(|theme_config| Self::get_entry(&theme_config))
}
#[must_use]
/// Rebuild the current theme with the provided accent
pub fn with_accent(&self, c: Srgba) -> Self {
let mut oklcha: Oklcha = c.into_color();
let cur_oklcha: Oklcha = self.accent_color().into_color();
oklcha.l = cur_oklcha.l;
let adjusted_c: Srgb = oklcha.into_color();
let is_dark = self.is_dark;
let mut builder = if is_dark {
ThemeBuilder::dark_config()
.ok()
.and_then(|h| ThemeBuilder::get_entry(&h).ok())
.unwrap_or_else(ThemeBuilder::dark)
} else {
ThemeBuilder::light_config()
.ok()
.and_then(|h| ThemeBuilder::get_entry(&h).ok())
.unwrap_or_else(ThemeBuilder::light)
};
builder = builder.accent(adjusted_c);
builder.build()
}
/// choose default color palette based on preferred GTK color scheme
pub fn gtk_prefer_colorscheme() -> Self {
let gsettings = "/usr/bin/gsettings";
let cmd = std::process::Command::new(gsettings)
.arg("get")
.arg("org.gnome.desktop.interface")
.arg("color-scheme")
.output();
if let Ok(cmd) = cmd {
let color_scheme = String::from_utf8_lossy(&cmd.stdout);
if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") {
return Self::light_default();
}
}
Self::dark_default()
}
/// check current desktop environment and preferred color scheme and set it as default
pub fn preferred_theme() -> Self {
let current_desktop = std::env::var("XDG_CURRENT_DESKTOP");
if let Ok(desktop) = current_desktop
&& desktop.trim().to_lowercase().contains("gnome")
{
return Self::gtk_prefer_colorscheme();
}
Self::dark_default()
}
}
impl From<CosmicPalette> for Theme {
fn from(p: CosmicPalette) -> Self {
ThemeBuilder::palette(p).build()
}
}
#[must_use]
/// Helper for building customized themes
#[derive(Clone, Debug, Serialize, Deserialize, CosmicConfigEntry, PartialEq)]
#[version = 2]
pub struct ThemeBuilder {
/// override the palette for the builder
pub palette: CosmicPalette,
/// override spacing for the builder
pub spacing: Spacing,
/// override corner radii for the builder
pub corner_radii: CornerRadii,
/// override neutral_tint for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub neutral_tint: Option<Srgb>,
/// override bg_color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub bg_color: Option<Srgba>,
/// override the primary container bg color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub primary_container_bg: Option<Srgba>,
/// override the secontary container bg color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub secondary_container_bg: Option<Srgba>,
/// override the text tint for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub text_tint: Option<Srgb>,
/// override the accent color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub accent: Option<Srgb>,
/// override the success color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub success: Option<Srgb>,
/// override the warning color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub warning: Option<Srgb>,
/// override the destructive color for the builder
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub destructive: Option<Srgb>,
/// enabled blurred transparency
pub frosted: BlurStrength,
/// cosmic-comp window gaps size (outer, inner)
pub gaps: (u32, u32),
/// cosmic-comp active hint window outline width
pub active_hint: u32,
/// cosmic-comp custom window hint color
#[serde(with = "color_serde_option")]
#[cosmic_config_entry(with = ColorReprOption)]
pub window_hint: Option<Srgb>,
/// frosted windows
pub frosted_windows: bool,
/// frosted system interface
pub frosted_system_interface: bool,
/// frosted panel
pub frosted_panel: bool,
/// frosted applet popups
pub frosted_applets: bool,
}
impl Default for ThemeBuilder {
fn default() -> Self {
Self {
palette: DARK_PALETTE.to_owned(),
spacing: Spacing::default(),
corner_radii: CornerRadii::default(),
neutral_tint: Default::default(),
text_tint: Default::default(),
bg_color: Default::default(),
primary_container_bg: Default::default(),
secondary_container_bg: Default::default(),
accent: Default::default(),
success: Default::default(),
warning: Default::default(),
destructive: Default::default(),
frosted: BlurStrength::default(),
// cosmic-comp theme settings
gaps: (0, 8),
active_hint: 3,
window_hint: None,
frosted_windows: false,
frosted_system_interface: false,
frosted_panel: false,
frosted_applets: false,
}
}
}
impl ThemeBuilder {
#[inline]
/// Get a builder that is initialized with the default dark theme
pub fn dark() -> Self {
Self {
palette: DARK_PALETTE.to_owned(),
..Default::default()
}
}
#[inline]
/// Get a builder that is initialized with the default light theme
pub fn light() -> Self {
Self {
palette: LIGHT_PALETTE.to_owned(),
..Default::default()
}
}
#[inline]
/// Get a builder that is initialized with the default dark high contrast theme
pub fn dark_high_contrast() -> Self {
let palette: CosmicPalette = DARK_PALETTE.to_owned();
Self {
palette: CosmicPalette::HighContrastDark(palette.inner()),
..Default::default()
}
}
#[inline]
/// Get a builder that is initialized with the default light high contrast theme
pub fn light_high_contrast() -> Self {
let palette: CosmicPalette = LIGHT_PALETTE.to_owned();
Self {
palette: CosmicPalette::HighContrastLight(palette.inner()),
..Default::default()
}
}
#[inline]
/// Get a builder that is initialized with the provided palette
pub fn palette(palette: CosmicPalette) -> Self {
Self {
palette,
..Default::default()
}
}
#[inline]
/// set the spacing of the builder
pub fn spacing(mut self, spacing: Spacing) -> Self {
self.spacing = spacing;
self
}
#[inline]
/// set the corner radii of the builder
pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self {
self.corner_radii = corner_radii;
self
}
#[inline]
/// apply a neutral tint to the palette
pub fn neutral_tint(mut self, tint: Srgb) -> Self {
self.neutral_tint = Some(tint);
self
}
#[inline]
/// apply a text tint to the palette
pub fn text_tint(mut self, tint: Srgb) -> Self {
self.text_tint = Some(tint);
self
}
#[inline]
/// apply a background color to the palette
pub fn bg_color(mut self, c: Srgba) -> Self {
self.bg_color = Some(c);
self
}
#[inline]
/// apply a primary container background color to the palette
pub fn primary_container_bg(mut self, c: Srgba) -> Self {
self.primary_container_bg = Some(c);
self
}
#[inline]
/// apply a accent color to the palette
pub fn accent(mut self, c: Srgb) -> Self {
self.accent = Some(c);
self
}
#[inline]
/// apply a success color to the palette
pub fn success(mut self, c: Srgb) -> Self {
self.success = Some(c);
self
}
#[inline]
/// apply a warning color to the palette
pub fn warning(mut self, c: Srgb) -> Self {
self.warning = Some(c);
self
}
#[inline]
/// apply a destructive color to the palette
pub fn destructive(mut self, c: Srgb) -> Self {
self.destructive = Some(c);
self
}
#[allow(clippy::too_many_lines)]
/// build the theme
pub fn build(self) -> Theme {
let Self {
palette,
spacing,
corner_radii,
neutral_tint,
text_tint,
bg_color,
primary_container_bg,
secondary_container_bg,
accent,
success,
warning,
destructive,
gaps,
active_hint,
window_hint,
frosted,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
} = self;
let container_alpha = frosted.alpha();
let is_dark = palette.is_dark();
let is_high_contrast = palette.is_high_contrast();
let accent = if let Some(accent) = accent {
accent.into_color()
} else {
palette.as_ref().accent_blue
};
let success = if let Some(success) = success {
success.into_color()
} else {
palette.as_ref().bright_green
};
let warning = if let Some(warning) = warning {
warning.into_color()
} else {
palette.as_ref().bright_orange
};
let destructive = if let Some(destructive) = destructive {
destructive.into_color()
} else {
palette.as_ref().bright_red
};
let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap()));
let mut control_steps_array = if let Some(neutral_tint) = neutral_tint {
steps(neutral_tint, NonZeroUsize::new(11).unwrap())
} else {
steps(palette.as_ref().neutral_2, NonZeroUsize::new(11).unwrap())
};
if !is_dark {
control_steps_array.reverse();
}
let p_ref = palette.as_ref();
let neutral_steps = steps(
neutral_tint.unwrap_or(Rgb::new(0.0, 0.0, 0.0)),
NonZeroUsize::new(100).unwrap(),
);
let bg = if let Some(bg_color) = bg_color {
bg_color
} else {
p_ref.gray_1
};
let step_array = steps(bg, NonZeroUsize::new(100).unwrap());
let bg_index = color_index(bg, step_array.len());
let mut transparent_bg = bg;
transparent_bg.alpha = container_alpha;
let transparent_bg_steps_array = steps(transparent_bg, NonZeroUsize::new(100).unwrap());
let mut component_hovered_overlay = if bg_index < 91 {
control_steps_array[10]
} else {
control_steps_array[0]
};
component_hovered_overlay.alpha = 0.1;
let mut component_pressed_overlay = component_hovered_overlay;
component_pressed_overlay.alpha = 0.2;
// Standard button background is neutral 7 with 25% opacity
let button_bg = control_steps_array[7].with_alpha(0.25);
let (button_hovered_overlay, button_pressed_overlay) = (
control_steps_array[5].with_alpha(0.2),
control_steps_array[2].with_alpha(0.5),
);
let bg_component = get_surface_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2);
let on_bg_component = get_text(
color_index(bg_component, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
);
let transparent_bg_component = get_surface_color(
bg_index,
8,
&transparent_bg_steps_array,
is_dark,
&p_ref.neutral_2,
);
let on_transparent_bg_component = get_text(
color_index(transparent_bg_component, transparent_bg_steps_array.len()),
&transparent_bg_steps_array,
&control_steps_array[8],
text_steps_array.as_deref(),
);
let primary = {
let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg {
primary_container_bg_color
} else {
get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
};
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index: usize = color_index(container_bg, step_array.len());
let component_base =
get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]);
component_hovered_overlay = if base_index < 91 {
control_steps_array[10]
} else {
control_steps_array[0]
};
component_hovered_overlay.alpha = 0.1;
component_pressed_overlay = component_hovered_overlay;
component_pressed_overlay.alpha = 0.2;
Container::new(
Component::component(
component_base,
accent,
get_text(
color_index(component_base, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
component_hovered_overlay,
component_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
};
let transparent_primary = {
let mut container_bg = if let Some(primary_container_bg_color) = primary_container_bg {
primary_container_bg_color
} else {
get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
};
container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0);
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index: usize = color_index(container_bg, step_array.len());
let component_base =
get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]);
Container::new(
Component::component(
component_base,
accent,
get_text(
color_index(component_base, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
Srgba::new(0., 0., 0., 0.0),
Srgba::new(0., 0., 0., 0.0),
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
};
let accent_text = if is_dark {
(primary.base.relative_contrast(accent.color) < 4.).then(|| {
let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
let primary_color_index = color_index(primary.base, 100);
let steps = if is_high_contrast { 60 } else { 50 };
let accent_text = get_surface_color(
primary_color_index,
steps,
&step_array,
is_dark,
&Srgba::new(1., 1., 1., 1.),
);
if primary.base.relative_contrast(accent_text.color) < 4. {
Srgba::new(1., 1., 1., 1.)
} else {
accent_text
}
})
} else {
let darkest = if bg.relative_luminance().luma < primary.base.relative_luminance().luma {
bg
} else {
primary.base
};
(darkest.relative_contrast(accent.color) < 4.).then(|| {
let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
let primary_color_index = color_index(darkest, 100);
let steps = if is_high_contrast { 60 } else { 50 };
let accent_text = get_surface_color(
primary_color_index,
steps,
&step_array,
is_dark,
&Srgba::new(1., 1., 1., 1.),
);
if darkest.relative_contrast(accent_text.color) < 4. {
Srgba::new(0., 0., 0., 1.)
} else {
accent_text
}
})
};
let mut theme: Theme = Theme {
name: palette.name().to_string(),
shade: if palette.is_dark() {
Srgba::new(0., 0., 0., 0.32)
} else {
Srgba::new(0., 0., 0., 0.08)
},
background: Container::new(
Component::component(
bg_component,
accent,
on_bg_component,
component_hovered_overlay,
component_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
bg,
get_text(
bg_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
),
primary,
secondary: {
let container_bg = if let Some(secondary_container_bg) = secondary_container_bg {
secondary_container_bg
} else {
get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
};
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index = color_index(container_bg, step_array.len());
let secondary_component =
get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]);
component_hovered_overlay = if base_index < 91 {
control_steps_array[10]
} else {
control_steps_array[0]
};
component_hovered_overlay.alpha = 0.1;
component_pressed_overlay = component_hovered_overlay;
component_pressed_overlay.alpha = 0.2;
Container::new(
Component::component(
secondary_component,
accent,
get_text(
color_index(secondary_component, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
component_hovered_overlay,
component_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
},
accent: Component::colored_component(
accent,
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
accent_button: Component::colored_button(
accent,
control_steps_array[1],
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
button: Component::component(
button_bg,
accent,
on_bg_component,
button_hovered_overlay,
button_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
destructive: Component::colored_component(
destructive,
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
destructive_button: Component::colored_button(
destructive,
control_steps_array[1],
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
icon_button: Component::component(
Srgba::new(0.0, 0.0, 0.0, 0.0),
accent,
control_steps_array[8],
button_hovered_overlay,
button_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
link_button: {
let mut component = Component::component(
Srgba::new(0.0, 0.0, 0.0, 0.0),
accent,
accent_text.unwrap_or(accent),
Srgba::new(0.0, 0.0, 0.0, 0.0),
Srgba::new(0.0, 0.0, 0.0, 0.0),
is_high_contrast,
control_steps_array[8],
);
component.on_disabled = over(component.on.with_alpha(0.5), component.base);
component
},
success: Component::colored_component(
success,
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
success_button: Component::colored_button(
success,
control_steps_array[1],
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
text_button: Component::component(
Srgba::new(0.0, 0.0, 0.0, 0.0),
accent,
accent_text.unwrap_or(accent),
button_hovered_overlay,
button_pressed_overlay,
is_high_contrast,
control_steps_array[8],
),
warning: Component::colored_component(
warning,
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
warning_button: Component::colored_button(
warning,
control_steps_array[10],
control_steps_array[0],
accent,
button_hovered_overlay,
button_pressed_overlay,
),
palette: palette.inner(),
spacing,
corner_radii,
is_dark,
is_high_contrast,
gaps,
active_hint,
window_hint,
frosted,
accent_text,
control_tint: neutral_tint,
text_tint,
frosted_windows,
frosted_system_interface,
frosted_panel,
frosted_applets,
transparent_background: Container::new(
Component::component(
transparent_bg_component,
accent,
on_transparent_bg_component,
Srgba::new(0., 0., 0., 0.0),
Srgba::new(0., 0., 0., 0.0),
is_high_contrast,
control_steps_array[8],
),
transparent_bg,
get_text(
bg_index,
&transparent_bg_steps_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
),
transparent_primary,
transparent_secondary: {
let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg
{
secondary_container_bg
} else {
get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
};
container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0);
let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
let base_index = color_index(container_bg, step_array.len());
let secondary_component =
get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]);
Container::new(
Component::component(
secondary_component,
accent,
get_text(
color_index(secondary_component, step_array.len()),
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
Srgba::new(0., 0., 0., 0.0),
Srgba::new(0., 0., 0., 0.0),
is_high_contrast,
control_steps_array[8],
),
container_bg,
get_text(
base_index,
&step_array,
&control_steps_array[8],
text_steps_array.as_deref(),
),
get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
is_high_contrast,
)
},
};
theme.spacing = spacing;
theme.corner_radii = corner_radii;
theme
}
#[inline]
/// Get the builder for the dark config
pub fn dark_config() -> Result<Config, cosmic_config::Error> {
Config::new(DARK_THEME_BUILDER_ID, Self::VERSION)
}
#[inline]
/// Get the builder for the light config
pub fn light_config() -> Result<Config, cosmic_config::Error> {
Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION)
}
}
/// Actual blur radius is decided by cosmic-comp,
/// but this represents the strength of the blur effect.
#[allow(missing_docs)]
#[repr(u8)]
#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum BlurStrength {
ExtremelyLow,
ExtremelyLow2,
VeryLow,
VeryLow2,
Low,
Low2,
#[default]
Medium,
Medium2,
High,
High2,
VeryHigh,
VeryHigh2,
ExtremelyHigh,
ExtremelyHigh2,
}
impl BlurStrength {
/// Get the alpha value corresponding to the blur strength
/// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows:
pub fn alpha(&self) -> f32 {
match self {
Self::ExtremelyLow => 0.90,
Self::ExtremelyLow2 => 0.85,
Self::VeryLow => 0.8,
Self::VeryLow2 => 0.75,
Self::Low => 0.7,
Self::Low2 => 0.65,
Self::Medium => 0.6,
Self::Medium2 => 0.55,
Self::High => 0.5,
Self::High2 => 0.45,
Self::VeryHigh => 0.4,
Self::VeryHigh2 => 0.35,
Self::ExtremelyHigh => 0.25,
Self::ExtremelyHigh2 => 0.2,
}
}
}
impl TryFrom<u8> for BlurStrength {
type Error = ();
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(BlurStrength::ExtremelyLow),
1 => Ok(BlurStrength::ExtremelyLow2),
2 => Ok(BlurStrength::VeryLow),
3 => Ok(BlurStrength::VeryLow2),
4 => Ok(BlurStrength::Low),
5 => Ok(BlurStrength::Low2),
6 => Ok(BlurStrength::Medium),
7 => Ok(BlurStrength::Medium2),
8 => Ok(BlurStrength::High),
9 => Ok(BlurStrength::High2),
10 => Ok(BlurStrength::VeryHigh),
11 => Ok(BlurStrength::VeryHigh2),
12 => Ok(BlurStrength::ExtremelyHigh),
13 => Ok(BlurStrength::ExtremelyHigh2),
_ => Err(()),
}
}
}