settings.rs

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