registry.rs

  1use std::collections::HashMap;
  2use std::sync::Arc;
  3
  4use anyhow::{anyhow, Context, Result};
  5use gpui::{AssetSource, HighlightStyle, SharedString};
  6use refineable::Refineable;
  7use util::ResultExt;
  8
  9use crate::{
 10    try_parse_color, Appearance, AppearanceContent, PlayerColor, PlayerColors, StatusColors,
 11    SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeContent, ThemeFamily, ThemeFamilyContent,
 12    ThemeStyles,
 13};
 14
 15#[derive(Debug, Clone)]
 16pub struct ThemeMeta {
 17    pub name: SharedString,
 18    pub appearance: Appearance,
 19}
 20
 21pub struct ThemeRegistry {
 22    assets: Box<dyn AssetSource>,
 23    themes: HashMap<SharedString, Arc<Theme>>,
 24}
 25
 26impl ThemeRegistry {
 27    pub fn new(assets: Box<dyn AssetSource>) -> Self {
 28        let mut registry = Self {
 29            assets,
 30            themes: HashMap::new(),
 31        };
 32
 33        // We're loading our new versions of the One themes by default, as
 34        // we need them to be loaded for tests.
 35        //
 36        // These themes will get overwritten when `load_user_themes` is called
 37        // when Zed starts, so the One variants used will be the ones ported from Zed1.
 38        registry.insert_theme_families([crate::one_themes::one_family()]);
 39
 40        registry
 41    }
 42
 43    fn insert_theme_families(&mut self, families: impl IntoIterator<Item = ThemeFamily>) {
 44        for family in families.into_iter() {
 45            self.insert_themes(family.themes);
 46        }
 47    }
 48
 49    fn insert_themes(&mut self, themes: impl IntoIterator<Item = Theme>) {
 50        for theme in themes.into_iter() {
 51            self.themes.insert(theme.name.clone(), Arc::new(theme));
 52        }
 53    }
 54
 55    #[allow(unused)]
 56    fn insert_user_theme_families(
 57        &mut self,
 58        families: impl IntoIterator<Item = ThemeFamilyContent>,
 59    ) {
 60        for family in families.into_iter() {
 61            self.insert_user_themes(family.themes);
 62        }
 63    }
 64
 65    #[allow(unused)]
 66    fn insert_user_themes(&mut self, themes: impl IntoIterator<Item = ThemeContent>) {
 67        self.insert_themes(themes.into_iter().map(|user_theme| {
 68            let mut theme_colors = match user_theme.appearance {
 69                AppearanceContent::Light => ThemeColors::light(),
 70                AppearanceContent::Dark => ThemeColors::dark(),
 71            };
 72            theme_colors.refine(&user_theme.style.theme_colors_refinement());
 73
 74            let mut status_colors = match user_theme.appearance {
 75                AppearanceContent::Light => StatusColors::light(),
 76                AppearanceContent::Dark => StatusColors::dark(),
 77            };
 78            status_colors.refine(&user_theme.style.status_colors_refinement());
 79
 80            let mut player_colors = match user_theme.appearance {
 81                AppearanceContent::Light => PlayerColors::light(),
 82                AppearanceContent::Dark => PlayerColors::dark(),
 83            };
 84            if !user_theme.style.players.is_empty() {
 85                player_colors = PlayerColors(
 86                    user_theme
 87                        .style
 88                        .players
 89                        .into_iter()
 90                        .map(|player| PlayerColor {
 91                            cursor: player
 92                                .cursor
 93                                .as_ref()
 94                                .and_then(|color| try_parse_color(&color).ok())
 95                                .unwrap_or_default(),
 96                            background: player
 97                                .background
 98                                .as_ref()
 99                                .and_then(|color| try_parse_color(&color).ok())
100                                .unwrap_or_default(),
101                            selection: player
102                                .selection
103                                .as_ref()
104                                .and_then(|color| try_parse_color(&color).ok())
105                                .unwrap_or_default(),
106                        })
107                        .collect(),
108                );
109            }
110
111            let mut syntax_colors = match user_theme.appearance {
112                AppearanceContent::Light => SyntaxTheme::light(),
113                AppearanceContent::Dark => SyntaxTheme::dark(),
114            };
115            if !user_theme.style.syntax.is_empty() {
116                syntax_colors.highlights = user_theme
117                    .style
118                    .syntax
119                    .iter()
120                    .map(|(syntax_token, highlight)| {
121                        (
122                            syntax_token.clone(),
123                            HighlightStyle {
124                                color: highlight
125                                    .color
126                                    .as_ref()
127                                    .and_then(|color| try_parse_color(&color).ok()),
128                                font_style: highlight.font_style.map(Into::into),
129                                font_weight: highlight.font_weight.map(Into::into),
130                                ..Default::default()
131                            },
132                        )
133                    })
134                    .collect::<Vec<_>>();
135            }
136
137            Theme {
138                id: uuid::Uuid::new_v4().to_string(),
139                name: user_theme.name.into(),
140                appearance: match user_theme.appearance {
141                    AppearanceContent::Light => Appearance::Light,
142                    AppearanceContent::Dark => Appearance::Dark,
143                },
144                styles: ThemeStyles {
145                    system: SystemColors::default(),
146                    colors: theme_colors,
147                    status: status_colors,
148                    player: player_colors,
149                    syntax: Arc::new(syntax_colors),
150                    accents: Vec::new(),
151                },
152            }
153        }));
154    }
155
156    pub fn clear(&mut self) {
157        self.themes.clear();
158    }
159
160    pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
161        self.themes.keys().cloned()
162    }
163
164    pub fn list(&self, _staff: bool) -> impl Iterator<Item = ThemeMeta> + '_ {
165        self.themes.values().map(|theme| ThemeMeta {
166            name: theme.name.clone(),
167            appearance: theme.appearance(),
168        })
169    }
170
171    pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
172        self.themes
173            .get(name)
174            .ok_or_else(|| anyhow!("theme not found: {}", name))
175            .cloned()
176    }
177
178    pub fn load_user_themes(&mut self) {
179        let theme_paths = self
180            .assets
181            .list("themes/")
182            .expect("failed to list theme assets")
183            .into_iter()
184            .filter(|path| path.ends_with(".json"));
185
186        for path in theme_paths {
187            let Some(theme) = self.assets.load(&path).log_err() else {
188                continue;
189            };
190
191            let Some(theme_family) = serde_json::from_slice(&theme)
192                .with_context(|| format!("failed to parse theme at path \"{path}\""))
193                .log_err()
194            else {
195                continue;
196            };
197
198            self.insert_user_theme_families([theme_family]);
199        }
200    }
201}
202
203impl Default for ThemeRegistry {
204    fn default() -> Self {
205        Self::new(Box::new(()))
206    }
207}