settings_ui_core.rs

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