1use crate::{prelude::*, Checkbox, ListHeader};
2
3use super::DropdownMenu;
4
5#[derive(PartialEq, Clone, Eq, Debug)]
6pub enum ToggleType {
7 Checkbox,
8 // Switch,
9}
10
11impl From<ToggleType> for SettingType {
12 fn from(toggle_type: ToggleType) -> Self {
13 SettingType::Toggle(toggle_type)
14 }
15}
16
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum InputType {
19 Text,
20 Number,
21}
22
23impl From<InputType> for SettingType {
24 fn from(input_type: InputType) -> Self {
25 SettingType::Input(input_type)
26 }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum SecondarySettingType {
31 Dropdown,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum SettingType {
36 Toggle(ToggleType),
37 ToggleAnd(SecondarySettingType),
38 Input(InputType),
39 Dropdown,
40 Range,
41 Unsupported,
42}
43
44#[derive(Debug, Clone, IntoElement)]
45pub struct SettingsGroup {
46 pub name: String,
47 settings: Vec<SettingsItem>,
48}
49
50impl SettingsGroup {
51 pub fn new(name: impl Into<String>) -> Self {
52 Self {
53 name: name.into(),
54 settings: Vec::new(),
55 }
56 }
57
58 pub fn add_setting(mut self, setting: SettingsItem) -> Self {
59 self.settings.push(setting);
60 self
61 }
62}
63
64impl RenderOnce for SettingsGroup {
65 fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
66 let empty_message = format!("No settings available for {}", self.name);
67
68 let header = ListHeader::new(self.name);
69
70 let settings = self.settings.clone().into_iter();
71
72 v_flex()
73 .p_1()
74 .gap_2()
75 .child(header)
76 .when(self.settings.len() == 0, |this| {
77 this.child(Label::new(empty_message))
78 })
79 .children(settings)
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
84pub enum SettingLayout {
85 Stacked,
86 AutoWidth,
87 FullLine,
88 FullLineJustified,
89}
90
91#[derive(Debug, Clone, PartialEq)]
92pub struct SettingId(pub SharedString);
93
94impl From<SettingId> for ElementId {
95 fn from(id: SettingId) -> Self {
96 ElementId::Name(id.0)
97 }
98}
99
100impl From<&str> for SettingId {
101 fn from(id: &str) -> Self {
102 Self(id.to_string().into())
103 }
104}
105
106impl From<SharedString> for SettingId {
107 fn from(id: SharedString) -> Self {
108 Self(id)
109 }
110}
111
112#[derive(Debug, Clone, PartialEq)]
113pub struct SettingValue(pub SharedString);
114
115impl From<SharedString> for SettingValue {
116 fn from(value: SharedString) -> Self {
117 Self(value)
118 }
119}
120
121impl From<String> for SettingValue {
122 fn from(value: String) -> Self {
123 Self(value.into())
124 }
125}
126
127impl From<bool> for SettingValue {
128 fn from(value: bool) -> Self {
129 Self(value.to_string().into())
130 }
131}
132
133impl From<SettingValue> for bool {
134 fn from(value: SettingValue) -> Self {
135 value.0 == "true"
136 }
137}
138
139#[derive(Debug, Clone, IntoElement)]
140pub struct SettingsItem {
141 pub id: SettingId,
142 current_value: Option<SettingValue>,
143 disabled: bool,
144 hide_label: bool,
145 icon: Option<IconName>,
146 layout: SettingLayout,
147 name: SharedString,
148 // possible_values: Option<Vec<SettingValue>>,
149 setting_type: SettingType,
150 toggled: Option<bool>,
151}
152
153impl SettingsItem {
154 pub fn new(
155 id: impl Into<SettingId>,
156 name: SharedString,
157 setting_type: SettingType,
158 current_value: Option<SettingValue>,
159 ) -> Self {
160 let toggled = match setting_type {
161 SettingType::Toggle(_) | SettingType::ToggleAnd(_) => Some(false),
162 _ => None,
163 };
164
165 Self {
166 id: id.into(),
167 current_value,
168 disabled: false,
169 hide_label: false,
170 icon: None,
171 layout: SettingLayout::FullLine,
172 name,
173 // possible_values: None,
174 setting_type,
175 toggled,
176 }
177 }
178
179 pub fn layout(mut self, layout: SettingLayout) -> Self {
180 self.layout = layout;
181 self
182 }
183
184 pub fn toggled(mut self, toggled: bool) -> Self {
185 self.toggled = Some(toggled);
186 self
187 }
188
189 // pub fn hide_label(mut self, hide_label: bool) -> Self {
190 // self.hide_label = hide_label;
191 // self
192 // }
193
194 pub fn icon(mut self, icon: IconName) -> Self {
195 self.icon = Some(icon);
196 self
197 }
198
199 // pub fn disabled(mut self, disabled: bool) -> Self {
200 // self.disabled = disabled;
201 // self
202 // }
203}
204
205impl RenderOnce for SettingsItem {
206 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
207 let id: ElementId = self.id.clone().into();
208
209 // When the setting is disabled or toggled off, we don't want any secondary elements to be interactable
210 let secondary_element_disabled = self.disabled || self.toggled == Some(false);
211
212 let full_width = match self.layout {
213 SettingLayout::FullLine | SettingLayout::FullLineJustified => true,
214 _ => false,
215 };
216
217 let hide_label = self.hide_label || self.icon.is_some();
218
219 let justified = match (self.layout.clone(), self.setting_type.clone()) {
220 (_, SettingType::ToggleAnd(_)) => true,
221 (SettingLayout::FullLineJustified, _) => true,
222 _ => false,
223 };
224
225 let (setting_type, current_value) = (self.setting_type.clone(), self.current_value.clone());
226 let current_string = if let Some(current_value) = current_value.clone() {
227 Some(current_value.0)
228 } else {
229 None
230 };
231
232 let toggleable = match setting_type {
233 SettingType::Toggle(_) => true,
234 SettingType::ToggleAnd(_) => true,
235 _ => false,
236 };
237
238 let setting_element = match setting_type {
239 SettingType::Toggle(_) => None,
240 SettingType::ToggleAnd(secondary_setting_type) => match secondary_setting_type {
241 SecondarySettingType::Dropdown => Some(
242 DropdownMenu::new(id.clone(), &cx)
243 .current_item(current_string)
244 .disabled(secondary_element_disabled)
245 .into_any_element(),
246 ),
247 },
248 SettingType::Input(input_type) => match input_type {
249 InputType::Text => Some(div().child("text").into_any_element()),
250 InputType::Number => Some(div().child("number").into_any_element()),
251 },
252 SettingType::Dropdown => Some(
253 DropdownMenu::new(id.clone(), &cx)
254 .current_item(current_string)
255 .full_width(true)
256 .into_any_element(),
257 ),
258 SettingType::Range => Some(div().child("range").into_any_element()),
259 SettingType::Unsupported => None,
260 };
261
262 let checkbox = Checkbox::new(
263 ElementId::Name(format!("toggle-{}", self.id.0).to_string().into()),
264 self.toggled.into(),
265 )
266 .disabled(self.disabled);
267
268 let toggle_element = match (toggleable, self.setting_type.clone()) {
269 (true, SettingType::Toggle(toggle_type)) => match toggle_type {
270 ToggleType::Checkbox => Some(checkbox.into_any_element()),
271 },
272 (true, SettingType::ToggleAnd(_)) => Some(checkbox.into_any_element()),
273 (_, _) => None,
274 };
275
276 let item = if self.layout == SettingLayout::Stacked {
277 v_flex()
278 } else {
279 h_flex()
280 };
281
282 item.id(id)
283 .gap_2()
284 .w_full()
285 .when_some(self.icon, |this, icon| {
286 this.child(div().px_0p5().child(Icon::new(icon).color(Color::Muted)))
287 })
288 .children(toggle_element)
289 .children(if hide_label {
290 None
291 } else {
292 Some(Label::new(self.name.clone()))
293 })
294 .when(justified, |this| this.child(div().flex_1().size_full()))
295 .child(
296 h_flex()
297 .when(full_width, |this| this.w_full())
298 .when(self.layout == SettingLayout::FullLineJustified, |this| {
299 this.justify_end()
300 })
301 .children(setting_element),
302 )
303 // help flex along when full width is disabled
304 //
305 // this probably isn't needed, but fighting with flex to
306 // get this right without inspection tools will be a pain
307 .when(!full_width, |this| this.child(div().size_full().flex_1()))
308 }
309}
310
311pub struct SettingsMenu {
312 name: SharedString,
313 groups: Vec<SettingsGroup>,
314}
315
316impl SettingsMenu {
317 pub fn new(name: impl Into<SharedString>) -> Self {
318 Self {
319 name: name.into(),
320 groups: Vec::new(),
321 }
322 }
323
324 pub fn add_group(mut self, group: SettingsGroup) -> Self {
325 self.groups.push(group);
326 self
327 }
328}
329
330impl Render for SettingsMenu {
331 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
332 let is_empty = self.groups.is_empty();
333 v_flex()
334 .id(ElementId::Name(self.name.clone()))
335 .elevation_2(cx)
336 .min_w_56()
337 .max_w_96()
338 .max_h_2_3()
339 .px_2()
340 .when_else(
341 is_empty,
342 |empty| empty.py_1(),
343 |not_empty| not_empty.pt_0().pb_1(),
344 )
345 .gap_1()
346 .when(is_empty, |this| {
347 this.child(Label::new("No settings found").color(Color::Muted))
348 })
349 .children(self.groups.clone())
350 }
351}