settings.rs

  1use crate::{Theme, ThemeRegistry};
  2use anyhow::Result;
  3use gpui::{px, AppContext, Font, FontFeatures, FontStyle, FontWeight, Pixels};
  4use schemars::{
  5    gen::SchemaGenerator,
  6    schema::{InstanceType, Schema, SchemaObject},
  7    JsonSchema,
  8};
  9use serde::{Deserialize, Serialize};
 10use serde_json::Value;
 11use settings::{Settings, SettingsJsonSchemaParams};
 12use std::sync::Arc;
 13use util::ResultExt as _;
 14
 15const MIN_FONT_SIZE: Pixels = px(6.0);
 16const MIN_LINE_HEIGHT: f32 = 1.0;
 17
 18#[derive(Clone)]
 19pub struct ThemeSettings {
 20    pub ui_font_size: Pixels,
 21    pub buffer_font: Font,
 22    pub buffer_font_size: Pixels,
 23    pub buffer_line_height: BufferLineHeight,
 24    pub active_theme: Arc<Theme>,
 25}
 26
 27#[derive(Default)]
 28pub struct AdjustedBufferFontSize(Option<Pixels>);
 29
 30#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
 31pub struct ThemeSettingsContent {
 32    #[serde(default)]
 33    pub ui_font_size: Option<f32>,
 34    #[serde(default)]
 35    pub buffer_font_family: Option<String>,
 36    #[serde(default)]
 37    pub buffer_font_size: Option<f32>,
 38    #[serde(default)]
 39    pub buffer_line_height: Option<BufferLineHeight>,
 40    #[serde(default)]
 41    pub buffer_font_features: Option<FontFeatures>,
 42    #[serde(default)]
 43    pub theme: Option<String>,
 44}
 45
 46#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
 47#[serde(rename_all = "snake_case")]
 48pub enum BufferLineHeight {
 49    #[default]
 50    Comfortable,
 51    Standard,
 52    Custom(f32),
 53}
 54
 55impl BufferLineHeight {
 56    pub fn value(&self) -> f32 {
 57        match self {
 58            BufferLineHeight::Comfortable => 1.618,
 59            BufferLineHeight::Standard => 1.3,
 60            BufferLineHeight::Custom(line_height) => *line_height,
 61        }
 62    }
 63}
 64
 65impl ThemeSettings {
 66    pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels {
 67        let font_size = *cx
 68            .default_global::<AdjustedBufferFontSize>()
 69            .0
 70            .get_or_insert(self.buffer_font_size.into());
 71        font_size.max(MIN_FONT_SIZE)
 72    }
 73
 74    pub fn line_height(&self) -> f32 {
 75        f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT)
 76    }
 77}
 78
 79pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels {
 80    if let Some(adjusted_size) = cx.default_global::<AdjustedBufferFontSize>().0 {
 81        let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
 82        let delta = adjusted_size - buffer_font_size;
 83        size + delta
 84    } else {
 85        size
 86    }
 87    .max(MIN_FONT_SIZE)
 88}
 89
 90pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) {
 91    let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size;
 92    let adjusted_size = cx
 93        .default_global::<AdjustedBufferFontSize>()
 94        .0
 95        .get_or_insert(buffer_font_size);
 96    f(adjusted_size);
 97    *adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size);
 98    cx.refresh();
 99}
100
101pub fn reset_font_size(cx: &mut AppContext) {
102    if cx.has_global::<AdjustedBufferFontSize>() {
103        cx.global_mut::<AdjustedBufferFontSize>().0 = None;
104        cx.refresh();
105    }
106}
107
108impl settings::Settings for ThemeSettings {
109    const KEY: Option<&'static str> = None;
110
111    type FileContent = ThemeSettingsContent;
112
113    fn load(
114        defaults: &Self::FileContent,
115        user_values: &[&Self::FileContent],
116        cx: &mut AppContext,
117    ) -> Result<Self> {
118        let themes = cx.default_global::<Arc<ThemeRegistry>>();
119
120        let mut this = Self {
121            ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
122            buffer_font: Font {
123                family: defaults.buffer_font_family.clone().unwrap().into(),
124                features: defaults.buffer_font_features.clone().unwrap(),
125                weight: FontWeight::default(),
126                style: FontStyle::default(),
127            },
128            buffer_font_size: defaults.buffer_font_size.unwrap().into(),
129            buffer_line_height: defaults.buffer_line_height.unwrap(),
130            active_theme: themes
131                .get(defaults.theme.as_ref().unwrap())
132                .or(themes.get("Zed Pro Moonlight"))
133                .unwrap(),
134        };
135
136        for value in user_values.into_iter().copied().cloned() {
137            if let Some(value) = value.buffer_font_family {
138                this.buffer_font.family = value.into();
139            }
140            if let Some(value) = value.buffer_font_features {
141                this.buffer_font.features = value;
142            }
143
144            if let Some(value) = &value.theme {
145                if let Some(theme) = themes.get(value).log_err() {
146                    this.active_theme = theme;
147                }
148            }
149
150            merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));
151            merge(
152                &mut this.buffer_font_size,
153                value.buffer_font_size.map(Into::into),
154            );
155            merge(&mut this.buffer_line_height, value.buffer_line_height);
156        }
157
158        Ok(this)
159    }
160
161    fn json_schema(
162        generator: &mut SchemaGenerator,
163        params: &SettingsJsonSchemaParams,
164        cx: &AppContext,
165    ) -> schemars::schema::RootSchema {
166        let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
167        let theme_names = cx
168            .global::<Arc<ThemeRegistry>>()
169            .list_names(params.staff_mode)
170            .map(|theme_name| Value::String(theme_name.to_string()))
171            .collect();
172
173        let theme_name_schema = SchemaObject {
174            instance_type: Some(InstanceType::String.into()),
175            enum_values: Some(theme_names),
176            ..Default::default()
177        };
178
179        root_schema
180            .definitions
181            .extend([("ThemeName".into(), theme_name_schema.into())]);
182
183        root_schema
184            .schema
185            .object
186            .as_mut()
187            .unwrap()
188            .properties
189            .extend([(
190                "theme".to_owned(),
191                Schema::new_ref("#/definitions/ThemeName".into()),
192            )]);
193
194        root_schema
195    }
196}
197
198fn merge<T: Copy>(target: &mut T, value: Option<T>) {
199    if let Some(value) = value {
200        *target = value;
201    }
202}