To reduce compile-times and avoid some overhead to binary size, this will modify some of our generic functions to use non-generic inner functions where possible. The inner functions are marked carefully with `#[inline(never)]` to prevent being inlined by LLVM at their callsites While looking for generic functions to optimize, I have also taken the opportunity to annotate public non-generic getters and setters with `#[inline]` to ensure that LLVM will inline them across crate boundaries. By default, only generic functions are automatically inlined, and only when enabling fat LTO are constant functions reliably inlined across crate boundaries.
1266 lines
36 KiB
Rust
1266 lines
36 KiB
Rust
use crate::{
|
|
composite::over,
|
|
steps::{color_index, get_index, get_small_widget_color, get_surface_color, get_text, steps},
|
|
Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, ThemeMode,
|
|
DARK_PALETTE, LIGHT_PALETTE, NAME,
|
|
};
|
|
use cosmic_config::{Config, CosmicConfigEntry};
|
|
use palette::{color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::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,
|
|
cosmic_config::cosmic_config_derive::CosmicConfigEntry,
|
|
)]
|
|
#[version = 1]
|
|
pub struct Theme {
|
|
/// name of the theme
|
|
pub name: String,
|
|
/// background element colors
|
|
pub background: Container,
|
|
/// primary element colors
|
|
pub primary: Container,
|
|
/// secondary element colors
|
|
pub secondary: Container,
|
|
/// 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,
|
|
/// button component styling
|
|
pub 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 is_frosted: bool,
|
|
/// shade color for dialogs
|
|
pub shade: Srgba,
|
|
/// accent text colors
|
|
/// If None, accent base color is the accent text color.
|
|
pub accent_text: Option<Srgba>,
|
|
}
|
|
|
|
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!();
|
|
}
|
|
|
|
// 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 {
|
|
let mut neutral_9 = self.palette.neutral_9;
|
|
neutral_9.alpha = 0.2;
|
|
neutral_9
|
|
}
|
|
|
|
// 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
|
|
pub fn get_active() -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
|
let config =
|
|
Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?;
|
|
let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?;
|
|
let config = if is_dark {
|
|
Self::dark_config()
|
|
} else {
|
|
Self::light_config()
|
|
}
|
|
.map_err(|e| (vec![e], Self::default()))?;
|
|
Self::get_entry(&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 {
|
|
if 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,
|
|
cosmic_config::cosmic_config_derive::CosmicConfigEntry,
|
|
PartialEq,
|
|
)]
|
|
#[version = 1]
|
|
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
|
|
pub neutral_tint: Option<Srgb>,
|
|
/// override bg_color for the builder
|
|
pub bg_color: Option<Srgba>,
|
|
/// override the primary container bg color for the builder
|
|
pub primary_container_bg: Option<Srgba>,
|
|
/// override the secontary container bg color for the builder
|
|
pub secondary_container_bg: Option<Srgba>,
|
|
/// override the text tint for the builder
|
|
pub text_tint: Option<Srgb>,
|
|
/// override the accent color for the builder
|
|
pub accent: Option<Srgb>,
|
|
/// override the success color for the builder
|
|
pub success: Option<Srgb>,
|
|
/// override the warning color for the builder
|
|
pub warning: Option<Srgb>,
|
|
/// override the destructive color for the builder
|
|
pub destructive: Option<Srgb>,
|
|
/// enabled blurred transparency
|
|
pub is_frosted: bool, // TODO handle
|
|
/// 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>,
|
|
}
|
|
|
|
impl Default for ThemeBuilder {
|
|
fn default() -> Self {
|
|
Self {
|
|
palette: DARK_PALETTE.to_owned().into(),
|
|
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(),
|
|
is_frosted: false,
|
|
// cosmic-comp theme settings
|
|
gaps: (0, 8),
|
|
active_hint: 3,
|
|
window_hint: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
mut 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,
|
|
is_frosted,
|
|
} = self;
|
|
|
|
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().accent_green
|
|
};
|
|
|
|
let warning = if let Some(warning) = warning {
|
|
warning.into_color()
|
|
} else {
|
|
palette.as_ref().accent_yellow
|
|
};
|
|
|
|
let destructive = if let Some(destructive) = destructive {
|
|
destructive.into_color()
|
|
} else {
|
|
palette.as_ref().accent_red
|
|
};
|
|
|
|
let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap()));
|
|
|
|
if let Some(neutral_tint) = neutral_tint {
|
|
let mut neutral_steps_arr = steps(neutral_tint, NonZeroUsize::new(11).unwrap());
|
|
if !is_dark {
|
|
neutral_steps_arr.reverse();
|
|
}
|
|
|
|
let p = palette.as_mut();
|
|
p.neutral_0 = neutral_steps_arr[0];
|
|
p.neutral_1 = neutral_steps_arr[1];
|
|
p.neutral_2 = neutral_steps_arr[2];
|
|
p.neutral_3 = neutral_steps_arr[3];
|
|
p.neutral_4 = neutral_steps_arr[4];
|
|
p.neutral_5 = neutral_steps_arr[5];
|
|
p.neutral_6 = neutral_steps_arr[6];
|
|
p.neutral_7 = neutral_steps_arr[7];
|
|
p.neutral_8 = neutral_steps_arr[8];
|
|
p.neutral_9 = neutral_steps_arr[9];
|
|
p.neutral_10 = neutral_steps_arr[10];
|
|
}
|
|
|
|
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 component_hovered_overlay = if bg_index < 91 {
|
|
p_ref.neutral_10
|
|
} else {
|
|
p_ref.neutral_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 = {
|
|
let mut color = p_ref.neutral_7;
|
|
color.alpha = 0.25;
|
|
color
|
|
};
|
|
|
|
let (mut button_hovered_overlay, mut button_pressed_overlay) =
|
|
(p_ref.neutral_5, p_ref.neutral_2);
|
|
button_hovered_overlay.alpha = 0.2;
|
|
button_pressed_overlay.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,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
);
|
|
|
|
let primary = {
|
|
let 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, &p_ref.neutral_1)
|
|
};
|
|
|
|
let base_index: usize = color_index(container_bg, step_array.len());
|
|
let component_base =
|
|
get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.neutral_3);
|
|
|
|
component_hovered_overlay = if base_index < 91 {
|
|
p_ref.neutral_10
|
|
} else {
|
|
p_ref.neutral_0
|
|
};
|
|
component_hovered_overlay.alpha = 0.1;
|
|
|
|
component_pressed_overlay = component_hovered_overlay;
|
|
component_pressed_overlay.alpha = 0.2;
|
|
|
|
let container = Container::new(
|
|
Component::component(
|
|
component_base,
|
|
accent,
|
|
get_text(
|
|
color_index(component_base, step_array.len()),
|
|
&step_array,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
),
|
|
component_hovered_overlay,
|
|
component_pressed_overlay,
|
|
is_high_contrast,
|
|
p_ref.neutral_8,
|
|
),
|
|
container_bg,
|
|
get_text(
|
|
base_index,
|
|
&step_array,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
),
|
|
get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6),
|
|
is_high_contrast,
|
|
);
|
|
|
|
container
|
|
};
|
|
|
|
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,
|
|
p_ref.neutral_8,
|
|
),
|
|
bg,
|
|
get_text(
|
|
bg_index,
|
|
&step_array,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
),
|
|
get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.neutral_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, &p_ref.neutral_2)
|
|
};
|
|
|
|
let base_index = color_index(container_bg, step_array.len());
|
|
let secondary_component =
|
|
get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.neutral_4);
|
|
|
|
component_hovered_overlay = if base_index < 91 {
|
|
p_ref.neutral_10
|
|
} else {
|
|
p_ref.neutral_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,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
),
|
|
component_hovered_overlay,
|
|
component_pressed_overlay,
|
|
is_high_contrast,
|
|
p_ref.neutral_8,
|
|
),
|
|
container_bg,
|
|
get_text(
|
|
base_index,
|
|
&step_array,
|
|
&p_ref.neutral_8,
|
|
text_steps_array.as_deref(),
|
|
),
|
|
get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6),
|
|
is_high_contrast,
|
|
)
|
|
},
|
|
accent: Component::colored_component(
|
|
accent,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
accent_button: Component::colored_button(
|
|
accent,
|
|
p_ref.neutral_1,
|
|
p_ref.neutral_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,
|
|
p_ref.neutral_8,
|
|
),
|
|
destructive: Component::colored_component(
|
|
destructive,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
destructive_button: Component::colored_button(
|
|
destructive,
|
|
p_ref.neutral_1,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
icon_button: Component::component(
|
|
Srgba::new(0.0, 0.0, 0.0, 0.0),
|
|
accent,
|
|
p_ref.neutral_8,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
is_high_contrast,
|
|
p_ref.neutral_8,
|
|
),
|
|
link_button: {
|
|
let mut component = Component::component(
|
|
Srgba::new(0.0, 0.0, 0.0, 0.0),
|
|
accent,
|
|
accent,
|
|
Srgba::new(0.0, 0.0, 0.0, 0.0),
|
|
Srgba::new(0.0, 0.0, 0.0, 0.0),
|
|
is_high_contrast,
|
|
p_ref.neutral_8,
|
|
);
|
|
|
|
let mut on_50 = component.on;
|
|
on_50.alpha = 0.5;
|
|
|
|
component.on_disabled = over(on_50, component.base);
|
|
component
|
|
},
|
|
success: Component::colored_component(
|
|
success,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
success_button: Component::colored_button(
|
|
success,
|
|
p_ref.neutral_1,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
text_button: Component::component(
|
|
Srgba::new(0.0, 0.0, 0.0, 0.0),
|
|
accent,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
is_high_contrast,
|
|
p_ref.neutral_8,
|
|
),
|
|
warning: Component::colored_component(
|
|
warning,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
warning_button: Component::colored_button(
|
|
warning,
|
|
p_ref.neutral_10,
|
|
p_ref.neutral_0,
|
|
accent,
|
|
button_hovered_overlay,
|
|
button_pressed_overlay,
|
|
),
|
|
palette: palette.inner(),
|
|
spacing,
|
|
corner_radii,
|
|
is_dark,
|
|
is_high_contrast,
|
|
gaps,
|
|
active_hint,
|
|
window_hint,
|
|
is_frosted,
|
|
accent_text,
|
|
};
|
|
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)
|
|
}
|
|
}
|