//! 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, pub control_radius: Option, pub accent: Option, pub accent_foreground: Option, pub background: Option, pub foreground: Option, pub surface: Option, pub surface_foreground: Option, pub primary: Option, pub primary_foreground: Option, pub secondary: Option, pub secondary_foreground: Option, pub muted: Option, pub muted_foreground: Option, pub destructive: Option, pub destructive_foreground: Option, pub border: Option, pub input: Option, pub ring: Option, pub selection: Option, } impl ThemeDefinition { pub fn parse_toml(source: &str) -> anyhow::Result { Ok(toml::from_str(source)?) } pub fn accent(&self) -> Option { parse_hex(self.accent.as_deref()?) } pub fn accent_foreground(&self) -> Option { parse_hex(self.accent_foreground.as_deref()?) } pub fn background(&self) -> Option { parse_hex(self.background.as_deref()?) } pub fn foreground(&self) -> Option { parse_hex(self.foreground.as_deref()?) } pub fn surface(&self) -> Option { parse_hex(self.surface.as_deref()?) } pub fn surface_foreground(&self) -> Option { parse_hex(self.surface_foreground.as_deref()?) } pub fn primary(&self) -> Option { parse_hex(self.primary.as_deref()?) } pub fn primary_foreground(&self) -> Option { parse_hex(self.primary_foreground.as_deref()?) } pub fn secondary(&self) -> Option { parse_hex(self.secondary.as_deref()?) } pub fn secondary_foreground(&self) -> Option { parse_hex(self.secondary_foreground.as_deref()?) } pub fn muted(&self) -> Option { parse_hex(self.muted.as_deref()?) } pub fn muted_foreground(&self) -> Option { parse_hex(self.muted_foreground.as_deref()?) } pub fn destructive(&self) -> Option { parse_hex(self.destructive.as_deref()?) } pub fn destructive_foreground(&self) -> Option { parse_hex(self.destructive_foreground.as_deref()?) } pub fn border(&self) -> Option { parse_hex(self.border.as_deref()?) } pub fn input(&self) -> Option { parse_hex(self.input.as_deref()?) } pub fn ring(&self) -> Option { parse_hex(self.ring.as_deref()?) } pub fn selection(&self) -> Option { parse_hex(self.selection.as_deref()?) } } /// Parses `#rgb` or `#rrggbb` into an opaque color. pub(crate) fn parse_hex(value: &str) -> Option { 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\"\n", ) .unwrap(); assert_eq!(def.mode, Some(crate::ThemeVariant::Dark)); assert!(def.accent().is_some()); assert!(def.foreground().is_none()); } #[test] fn an_invalid_mode_fails_the_parse_instead_of_being_ignored() { assert!(ThemeDefinition::parse_toml("mode = \"blue\"\n").is_err()); } }