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);