settings_ui_core.rs

  1use std::any::TypeId;
  2
  3use anyhow::Context as _;
  4use fs::Fs;
  5use gpui::{AnyElement, App, AppContext as _, ReadGlobal as _, Window};
  6use smallvec::SmallVec;
  7
  8use crate::SettingsStore;
  9
 10pub trait SettingsUi {
 11    fn settings_ui_item() -> SettingsUiItem {
 12        // todo(settings_ui): remove this default impl, only entry should have a default impl
 13        // because it's expected that the macro or custom impl use the item and the known paths to create the entry
 14        SettingsUiItem::None
 15    }
 16
 17    fn settings_ui_entry() -> SettingsUiEntry {
 18        SettingsUiEntry {
 19            path: None,
 20            title: "None entry",
 21            item: SettingsUiItem::None,
 22        }
 23    }
 24}
 25
 26pub struct SettingsUiEntry {
 27    /// The path in the settings JSON file for this setting. Relative to parent
 28    /// None implies `#[serde(flatten)]` or `Settings::KEY.is_none()` for top level settings
 29    pub path: Option<&'static str>,
 30    /// What is displayed for the text for this entry
 31    pub title: &'static str,
 32    pub item: SettingsUiItem,
 33}
 34
 35pub enum SettingsUiItemSingle {
 36    SwitchField,
 37    /// A numeric stepper for a specific type of number
 38    NumericStepper(NumType),
 39    ToggleGroup {
 40        /// Must be the same length as `labels`
 41        variants: &'static [&'static str],
 42        /// Must be the same length as `variants`
 43        labels: &'static [&'static str],
 44    },
 45    /// This should be used when toggle group size > 6
 46    DropDown {
 47        /// Must be the same length as `labels`
 48        variants: &'static [&'static str],
 49        /// Must be the same length as `variants`
 50        labels: &'static [&'static str],
 51    },
 52    Custom(Box<dyn Fn(SettingsValue<serde_json::Value>, &mut Window, &mut App) -> AnyElement>),
 53}
 54
 55pub struct SettingsValue<T> {
 56    pub title: &'static str,
 57    pub path: SmallVec<[&'static str; 1]>,
 58    pub value: Option<T>,
 59    pub default_value: T,
 60}
 61
 62impl<T> SettingsValue<T> {
 63    pub fn read(&self) -> &T {
 64        match &self.value {
 65            Some(value) => value,
 66            None => &self.default_value,
 67        }
 68    }
 69}
 70
 71impl SettingsValue<serde_json::Value> {
 72    pub fn write_value(path: &SmallVec<[&'static str; 1]>, value: serde_json::Value, cx: &mut App) {
 73        let settings_store = SettingsStore::global(cx);
 74        let fs = <dyn Fs>::global(cx);
 75
 76        let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value);
 77        let path = path.clone();
 78        cx.background_spawn(async move {
 79            rx.await?
 80                .with_context(|| format!("Failed to update setting at path `{:?}`", path.join(".")))
 81        })
 82        .detach_and_log_err(cx);
 83    }
 84}
 85
 86impl<T: serde::Serialize> SettingsValue<T> {
 87    pub fn write(
 88        path: &SmallVec<[&'static str; 1]>,
 89        value: T,
 90        cx: &mut App,
 91    ) -> Result<(), serde_json::Error> {
 92        SettingsValue::write_value(path, serde_json::to_value(value)?, cx);
 93        Ok(())
 94    }
 95}
 96
 97pub struct SettingsUiItemDynamic {
 98    pub options: Vec<SettingsUiEntry>,
 99    pub determine_option: fn(&serde_json::Value, &App) -> usize,
100}
101
102pub struct SettingsUiItemGroup {
103    pub items: Vec<SettingsUiEntry>,
104}
105
106pub enum SettingsUiItem {
107    Group(SettingsUiItemGroup),
108    Single(SettingsUiItemSingle),
109    Dynamic(SettingsUiItemDynamic),
110    None,
111}
112
113impl SettingsUi for bool {
114    fn settings_ui_item() -> SettingsUiItem {
115        SettingsUiItem::Single(SettingsUiItemSingle::SwitchField)
116    }
117}
118
119impl SettingsUi for Option<bool> {
120    fn settings_ui_item() -> SettingsUiItem {
121        SettingsUiItem::Single(SettingsUiItemSingle::SwitchField)
122    }
123}
124
125#[repr(u8)]
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum NumType {
128    U64 = 0,
129    U32 = 1,
130    F32 = 2,
131}
132pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> =
133    std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_name));
134pub static NUM_TYPE_IDS: std::sync::LazyLock<[TypeId; NumType::COUNT]> =
135    std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_id));
136
137impl NumType {
138    const COUNT: usize = 3;
139    const ALL: [NumType; Self::COUNT] = [NumType::U64, NumType::U32, NumType::F32];
140
141    pub fn type_id(self) -> TypeId {
142        match self {
143            NumType::U64 => TypeId::of::<u64>(),
144            NumType::U32 => TypeId::of::<u32>(),
145            NumType::F32 => TypeId::of::<f32>(),
146        }
147    }
148
149    pub fn type_name(self) -> &'static str {
150        match self {
151            NumType::U64 => std::any::type_name::<u64>(),
152            NumType::U32 => std::any::type_name::<u32>(),
153            NumType::F32 => std::any::type_name::<f32>(),
154        }
155    }
156}
157
158macro_rules! numeric_stepper_for_num_type {
159    ($type:ty, $num_type:ident) => {
160        impl SettingsUi for $type {
161            fn settings_ui_item() -> SettingsUiItem {
162                SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type))
163            }
164        }
165
166        impl SettingsUi for Option<$type> {
167            fn settings_ui_item() -> SettingsUiItem {
168                SettingsUiItem::Single(SettingsUiItemSingle::NumericStepper(NumType::$num_type))
169            }
170        }
171    };
172}
173
174numeric_stepper_for_num_type!(u64, U64);
175numeric_stepper_for_num_type!(u32, U32);
176// todo(settings_ui) is there a better ui for f32?
177numeric_stepper_for_num_type!(f32, F32);