242 lines
7.1 KiB
Rust
242 lines
7.1 KiB
Rust
//! Sparse theme definitions: the shared schema for bundled presets and
|
|
//! user theme files. Any field may be omitted; omitted fields fall through
|
|
//! to the system theme, then to the neutral presets.
|
|
|
|
use gpui::{Hsla, Rgba};
|
|
use serde::Deserialize;
|
|
|
|
use crate::ThemeVariant;
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Deserialize)]
|
|
pub struct ThemeDefinition {
|
|
pub mode: Option<ThemeVariant>,
|
|
pub control_radius: Option<f32>,
|
|
pub overlay_radius: Option<f32>,
|
|
pub sm_radius: Option<f32>,
|
|
pub accent: Option<String>,
|
|
pub accent_foreground: Option<String>,
|
|
pub background: Option<String>,
|
|
pub foreground: Option<String>,
|
|
pub surface: Option<String>,
|
|
pub surface_foreground: Option<String>,
|
|
pub primary: Option<String>,
|
|
pub primary_foreground: Option<String>,
|
|
pub secondary: Option<String>,
|
|
pub secondary_foreground: Option<String>,
|
|
pub muted: Option<String>,
|
|
pub muted_hover: Option<String>,
|
|
pub muted_foreground: Option<String>,
|
|
pub destructive: Option<String>,
|
|
pub destructive_foreground: Option<String>,
|
|
pub success: Option<String>,
|
|
pub success_foreground: Option<String>,
|
|
pub warning: Option<String>,
|
|
pub warning_foreground: Option<String>,
|
|
pub info: Option<String>,
|
|
pub info_foreground: Option<String>,
|
|
pub border: Option<String>,
|
|
pub input: Option<String>,
|
|
pub ring: Option<String>,
|
|
pub selection: Option<String>,
|
|
pub overlay: Option<String>,
|
|
pub scrollbar_thumb: Option<String>,
|
|
}
|
|
|
|
impl ThemeDefinition {
|
|
pub fn parse_toml(source: &str) -> anyhow::Result<Self> {
|
|
Ok(toml::from_str(source)?)
|
|
}
|
|
|
|
pub fn accent(&self) -> Option<Hsla> {
|
|
parse_hex(self.accent.as_deref()?)
|
|
}
|
|
|
|
pub fn accent_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.accent_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn background(&self) -> Option<Hsla> {
|
|
parse_hex(self.background.as_deref()?)
|
|
}
|
|
|
|
pub fn foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn surface(&self) -> Option<Hsla> {
|
|
parse_hex(self.surface.as_deref()?)
|
|
}
|
|
|
|
pub fn surface_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.surface_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn primary(&self) -> Option<Hsla> {
|
|
parse_hex(self.primary.as_deref()?)
|
|
}
|
|
|
|
pub fn primary_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.primary_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn secondary(&self) -> Option<Hsla> {
|
|
parse_hex(self.secondary.as_deref()?)
|
|
}
|
|
|
|
pub fn secondary_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.secondary_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn muted(&self) -> Option<Hsla> {
|
|
parse_hex(self.muted.as_deref()?)
|
|
}
|
|
|
|
pub fn muted_hover(&self) -> Option<Hsla> {
|
|
parse_hex(self.muted_hover.as_deref()?)
|
|
}
|
|
|
|
pub fn muted_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.muted_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn destructive(&self) -> Option<Hsla> {
|
|
parse_hex(self.destructive.as_deref()?)
|
|
}
|
|
|
|
pub fn destructive_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.destructive_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn success(&self) -> Option<Hsla> {
|
|
parse_hex(self.success.as_deref()?)
|
|
}
|
|
|
|
pub fn success_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.success_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn warning(&self) -> Option<Hsla> {
|
|
parse_hex(self.warning.as_deref()?)
|
|
}
|
|
|
|
pub fn warning_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.warning_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn info(&self) -> Option<Hsla> {
|
|
parse_hex(self.info.as_deref()?)
|
|
}
|
|
|
|
pub fn info_foreground(&self) -> Option<Hsla> {
|
|
parse_hex(self.info_foreground.as_deref()?)
|
|
}
|
|
|
|
pub fn border(&self) -> Option<Hsla> {
|
|
parse_hex(self.border.as_deref()?)
|
|
}
|
|
|
|
pub fn input(&self) -> Option<Hsla> {
|
|
parse_hex(self.input.as_deref()?)
|
|
}
|
|
|
|
pub fn ring(&self) -> Option<Hsla> {
|
|
parse_hex(self.ring.as_deref()?)
|
|
}
|
|
|
|
pub fn selection(&self) -> Option<Hsla> {
|
|
parse_hex(self.selection.as_deref()?)
|
|
}
|
|
|
|
pub fn overlay(&self) -> Option<Hsla> {
|
|
parse_hex(self.overlay.as_deref()?)
|
|
}
|
|
|
|
pub fn scrollbar_thumb(&self) -> Option<Hsla> {
|
|
parse_hex(self.scrollbar_thumb.as_deref()?)
|
|
}
|
|
}
|
|
|
|
/// Parses `#rgb` or `#rrggbb` into an opaque color.
|
|
pub(crate) fn parse_hex(value: &str) -> Option<Hsla> {
|
|
let digits = value.strip_prefix('#')?;
|
|
let expand = |channels: [f32; 3]| Some(expand_rgb([channels[0], channels[1], channels[2]]));
|
|
|
|
match digits.len() {
|
|
3 => {
|
|
let mut channels = [0u8; 3];
|
|
for (i, byte) in channels.iter_mut().enumerate() {
|
|
*byte = u8::from_str_radix(&digits[i..i + 1], 16).ok()?;
|
|
}
|
|
expand([
|
|
channels[0] as f32 / 15.0,
|
|
channels[1] as f32 / 15.0,
|
|
channels[2] as f32 / 15.0,
|
|
])
|
|
}
|
|
6 => {
|
|
let mut channels = [0u8; 3];
|
|
for (i, byte) in channels.iter_mut().enumerate() {
|
|
*byte = u8::from_str_radix(&digits[i * 2..i * 2 + 2], 16).ok()?;
|
|
}
|
|
expand([
|
|
channels[0] as f32 / 255.0,
|
|
channels[1] as f32 / 255.0,
|
|
channels[2] as f32 / 255.0,
|
|
])
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn expand_rgb([r, g, b]: [f32; 3]) -> Hsla {
|
|
Hsla::from(Rgba { r, g, b, a: 1.0 })
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parses_six_and_three_digit_hex() {
|
|
let Rgba { r, g, b, a } = Rgba::from(parse_hex("#7aa2f7").unwrap());
|
|
assert!((r - 0x7a as f32 / 255.0).abs() < 0.001);
|
|
assert!((g - 0xa2 as f32 / 255.0).abs() < 0.001);
|
|
assert!((b - 0xf7 as f32 / 255.0).abs() < 0.001);
|
|
assert_eq!(a, 1.0);
|
|
|
|
// Compare in RGB space: Rgba -> Hsla -> Rgba is lossy at low saturation.
|
|
let short = Rgba::from(parse_hex("#f00").unwrap());
|
|
assert!((short.r - 1.0).abs() < 0.001);
|
|
assert!(short.g.abs() < 0.001);
|
|
assert!(short.b.abs() < 0.001);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_malformed_values() {
|
|
assert!(parse_hex("").is_none());
|
|
assert!(parse_hex("#12").is_none());
|
|
assert!(parse_hex("#12345").is_none());
|
|
assert!(parse_hex("#zzzzzz").is_none());
|
|
assert!(parse_hex("7aa2f7").is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_a_full_definition() {
|
|
let def = ThemeDefinition::parse_toml(
|
|
"mode = \"dark\"\naccent = \"#7aa2f7\"\nbackground = \"#1a1b26\"\ninfo = \"#38bdf8\"\noverlay_radius = 15.0\nsm_radius = 6.0\n",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(def.mode, Some(crate::ThemeVariant::Dark));
|
|
assert!(def.accent().is_some());
|
|
assert!(def.info().is_some());
|
|
assert!(def.foreground().is_none());
|
|
assert_eq!(def.overlay_radius, Some(15.0));
|
|
assert_eq!(def.sm_radius, Some(6.0));
|
|
}
|
|
|
|
#[test]
|
|
fn an_invalid_mode_fails_the_parse_instead_of_being_ignored() {
|
|
assert!(ThemeDefinition::parse_toml("mode = \"blue\"\n").is_err());
|
|
}
|
|
}
|