1use gpui::{
2 AnyElement, App, Axis, Div, InteractiveElement as _, IntoElement, ParentElement, SharedString,
3 Stateful, Styled, Window, div, prelude::FluentBuilder as _,
4};
5use std::{any::TypeId, ops::Deref, rc::Rc};
6
7use crate::{
8 ActiveTheme as _, AxisExt, StyledExt as _,
9 label::Label,
10 setting::{
11 AnySettingField, ElementField, RenderOptions,
12 fields::{
13 BoolField, DropdownField, NumberField, ResetHandler, SettingFieldRender, StringField,
14 },
15 },
16 text::Text,
17 v_flex,
18};
19
20#[derive(Clone)]
22pub enum SettingItem {
23 Item {
25 title: SharedString,
26 description: Option<Text>,
27 keywords: Vec<SharedString>,
28 layout: Axis,
29 disabled: bool,
30 field: Rc<dyn AnySettingField>,
31 },
32 Element {
34 disabled: bool,
35 keywords: Vec<SharedString>,
36 reset_handler: Option<ResetHandler>,
40 render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
41 },
42}
43
44impl SettingItem {
45 pub fn new<F>(title: impl Into<SharedString>, field: F) -> Self
47 where
48 F: AnySettingField + 'static,
49 {
50 SettingItem::Item {
51 title: title.into(),
52 description: None,
53 layout: Axis::Horizontal,
54 disabled: false,
55 keywords: Vec::new(),
56 field: Rc::new(field),
57 }
58 }
59
60 pub fn render<R, E>(render: R) -> Self
62 where
63 E: IntoElement,
64 R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
65 {
66 SettingItem::Element {
67 disabled: false,
68 keywords: Vec::new(),
69 reset_handler: None,
70 render: Rc::new(move |options, window, cx| {
71 render(options, window, cx).into_any_element()
72 }),
73 }
74 }
75
76 pub fn on_reset<D, R>(mut self, is_dirty: D, reset: R) -> Self
85 where
86 D: Fn(&App) -> bool + 'static,
87 R: Fn(&mut Window, &mut App) + 'static,
88 {
89 match &mut self {
90 SettingItem::Element { reset_handler, .. } => {
91 *reset_handler = Some((Rc::new(is_dirty), Rc::new(reset)));
92 }
93 SettingItem::Item { .. } => {
96 debug_assert!(
97 false,
98 "SettingItem::on_reset only applies to SettingItem::Element; \
99 use SettingField::default_value or SettingField::on_reset for a normal item"
100 );
101 }
102 }
103 self
104 }
105
106 pub fn keywords<I, S>(mut self, keywords: I) -> Self
112 where
113 I: IntoIterator<Item = S>,
114 S: Into<SharedString>,
115 {
116 let keywords: Vec<SharedString> = keywords.into_iter().map(Into::into).collect();
117 match &mut self {
118 SettingItem::Item { keywords: k, .. } => *k = keywords,
119 SettingItem::Element { keywords: k, .. } => *k = keywords,
120 }
121 self
122 }
123
124 pub fn disabled(mut self, disabled: bool) -> Self {
132 match &mut self {
133 SettingItem::Item { disabled: d, .. } => *d = disabled,
134 SettingItem::Element { disabled: d, .. } => *d = disabled,
135 }
136 self
137 }
138
139 pub fn description(mut self, description: impl Into<Text>) -> Self {
143 match &mut self {
144 SettingItem::Item { description: d, .. } => {
145 *d = Some(description.into());
146 }
147 SettingItem::Element { .. } => {}
148 }
149 self
150 }
151
152 pub fn layout(mut self, layout: Axis) -> Self {
156 match &mut self {
157 SettingItem::Item { layout: l, .. } => {
158 *l = layout;
159 }
160 SettingItem::Element { .. } => {}
161 }
162 self
163 }
164
165 pub(crate) fn is_match(&self, query: &str, cx: &App) -> bool {
166 match self {
167 SettingItem::Item {
168 title,
169 description,
170 keywords,
171 ..
172 } => {
173 let q = &query.to_lowercase();
174 title.to_lowercase().contains(q)
175 || description
176 .as_ref()
177 .map_or(false, |d| d.get_text(cx).to_lowercase().contains(q))
178 || keywords.iter().any(|s| s.to_lowercase().contains(q))
179 }
180 SettingItem::Element { keywords, .. } => {
182 let q = &query.to_lowercase();
183 query.is_empty() || keywords.iter().any(|s| s.to_lowercase().contains(q))
184 }
185 }
186 }
187
188 pub(crate) fn is_resettable(&self, cx: &App) -> bool {
189 match self {
190 SettingItem::Item { field, .. } => field.is_resettable(cx),
191 SettingItem::Element { reset_handler, .. } => reset_handler
192 .as_ref()
193 .is_some_and(|(is_dirty, _)| is_dirty(cx)),
194 }
195 }
196
197 pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
198 match self {
199 SettingItem::Item { field, .. } => field.reset(window, cx),
200 SettingItem::Element { reset_handler, .. } => {
201 if let Some((_, reset)) = reset_handler.as_ref() {
202 reset(window, cx);
203 }
204 }
205 }
206 }
207
208 fn render_field(
209 field: Rc<dyn AnySettingField>,
210 options: RenderOptions,
211 window: &mut Window,
212 cx: &mut App,
213 ) -> impl IntoElement {
214 let field_type = field.field_type();
215 let style = field.style().clone();
216 let type_id = field.deref().type_id();
217 let renderer: Box<dyn SettingFieldRender> = match type_id {
218 t if t == std::any::TypeId::of::<bool>() => {
219 Box::new(BoolField::new(field_type.is_switch()))
220 }
221 t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
222 Box::new(NumberField::new(field_type.number_input_options()))
223 }
224 t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
225 Box::new(StringField::<SharedString>::new())
226 }
227 t if t == TypeId::of::<String>() && field_type.is_input() => {
228 Box::new(StringField::<String>::new())
229 }
230 t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => {
231 Box::new(DropdownField::<SharedString>::new(
232 field_type.dropdown_options(),
233 field_type.dropdown_scrollable(),
234 ))
235 }
236 t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
237 Box::new(DropdownField::<String>::new(
238 field_type.dropdown_options(),
239 field_type.dropdown_scrollable(),
240 ))
241 }
242 _ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
243 _ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
244 };
245
246 renderer.render(field, &options, &style, window, cx)
247 }
248
249 pub(super) fn render_item(
250 self,
251 options: &RenderOptions,
252 window: &mut Window,
253 cx: &mut App,
254 ) -> Stateful<Div> {
255 div()
256 .id(SharedString::from(format!("item-{}", options.item_ix())))
257 .w_full()
258 .child(match self {
259 SettingItem::Item {
260 title,
261 description,
262 layout,
263 disabled,
264 field,
265 ..
266 } => {
267 let layout = if options.layout().is_vertical() {
268 Axis::Vertical
269 } else {
270 layout
271 };
272
273 div()
274 .w_full()
275 .when(disabled, |this| this.opacity(0.5))
276 .map(|this| {
277 if layout.is_horizontal() {
278 this.h_flex().justify_between().items_center()
279 } else {
280 this.v_flex()
281 }
282 })
283 .gap_3()
284 .child(
285 v_flex()
286 .map(|this| {
287 if layout.is_horizontal() {
288 this.flex_1().max_w_3_5()
289 } else {
290 this.w_full()
291 }
292 })
293 .child(Label::new(title).text_sm())
294 .when_some(description, |this, description| {
295 this.child(
296 div()
297 .size_full()
298 .text_sm()
299 .text_color(cx.theme().muted_foreground)
300 .child(description),
301 )
302 }),
303 )
304 .child(div().id("field").child(Self::render_field(
305 field,
306 options.with_layout(layout).with_disabled(disabled),
307 window,
308 cx,
309 )))
310 .into_any_element()
311 }
312 SettingItem::Element {
313 disabled, render, ..
314 } => div()
315 .w_full()
316 .when(disabled, |this| this.opacity(0.5))
317 .child((render)(&options.with_disabled(disabled), window, cx))
318 .into_any_element(),
319 })
320 }
321}