Skip to main content

gpui_component/setting/
item.rs

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/// Setting item.
21#[derive(Clone)]
22pub enum SettingItem {
23    /// A normal setting item with a title, description, and field.
24    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    /// A full custom element to render.
33    Element {
34        disabled: bool,
35        keywords: Vec<SharedString>,
36        /// Optional custom reset behavior. The first closure reports whether
37        /// the item is "dirty" (controls reset button visibility), the second
38        /// performs the reset.
39        reset_handler: Option<ResetHandler>,
40        render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
41    },
42}
43
44impl SettingItem {
45    /// Create a new setting item.
46    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    /// Create a new custom element setting item with a render closure.
61    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    /// Provide custom reset behavior for a custom element item.
77    ///
78    /// Only applies to [`SettingItem::Element`] (created via
79    /// [`SettingItem::render`]). When set, the page-level reset button will
80    /// appear while `is_dirty` returns true, and clicking it invokes `reset`.
81    ///
82    /// - `is_dirty` reports whether the item differs from its default state.
83    /// - `reset` performs the reset.
84    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            // `on_reset` is meaningless for a value-bearing item: use the
94            // field's own `default_value` / `SettingField::on_reset` instead.
95            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    /// Set additional keywords used only for search matching (not rendered).
107    ///
108    /// For example, an item titled "Enable Two-factor auth" can be made
109    /// searchable via "MFA". This is also useful for custom elements that
110    /// have no title/description but should still show up in search results.
111    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    /// Set whether the setting item is disabled, default is false.
125    ///
126    /// A disabled item is rendered with reduced opacity. For
127    /// [`SettingItem::Item`] the underlying field is also rendered in a
128    /// non-interactive state. For [`SettingItem::Element`] the `disabled` flag
129    /// is forwarded via [`RenderOptions::disabled`] so the custom renderer can
130    /// disable its interactive controls.
131    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    /// Set the description of the setting item.
140    ///
141    /// Only applies to [`SettingItem::Item`].
142    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    /// Set the layout of the setting item.
153    ///
154    /// Only applies to [`SettingItem::Item`].
155    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            // We need to show all custom elements when not searching.
181            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        title: SharedString,
210        field: Rc<dyn AnySettingField>,
211        options: RenderOptions,
212        window: &mut Window,
213        cx: &mut App,
214    ) -> impl IntoElement {
215        let field_type = field.field_type();
216        let style = field.style().clone();
217        let type_id = field.deref().type_id();
218        let renderer: Box<dyn SettingFieldRender> = match type_id {
219            t if t == std::any::TypeId::of::<bool>() => {
220                Box::new(BoolField::new(field_type.is_switch(), title))
221            }
222            t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
223                Box::new(NumberField::new(field_type.number_input_options(), title))
224            }
225            t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
226                Box::new(StringField::<SharedString>::new(title))
227            }
228            t if t == TypeId::of::<String>() && field_type.is_input() => {
229                Box::new(StringField::<String>::new(title))
230            }
231            t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => {
232                Box::new(DropdownField::<SharedString>::new(
233                    field_type.dropdown_options(),
234                    field_type.dropdown_scrollable(),
235                    title,
236                ))
237            }
238            t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
239                Box::new(DropdownField::<String>::new(
240                    field_type.dropdown_options(),
241                    field_type.dropdown_scrollable(),
242                    title,
243                ))
244            }
245            _ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
246            _ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
247        };
248
249        renderer.render(field, &options, &style, window, cx)
250    }
251
252    pub(super) fn render_item(
253        self,
254        options: &RenderOptions,
255        window: &mut Window,
256        cx: &mut App,
257    ) -> Stateful<Div> {
258        div()
259            .id(SharedString::from(format!("item-{}", options.item_ix())))
260            .w_full()
261            .child(match self {
262                SettingItem::Item {
263                    title,
264                    description,
265                    layout,
266                    disabled,
267                    field,
268                    ..
269                } => {
270                    let layout = if options.layout().is_vertical() {
271                        Axis::Vertical
272                    } else {
273                        layout
274                    };
275
276                    div()
277                        .w_full()
278                        .when(disabled, |this| this.opacity(0.5))
279                        .map(|this| {
280                            if layout.is_horizontal() {
281                                this.h_flex().justify_between().items_center()
282                            } else {
283                                this.v_flex()
284                            }
285                        })
286                        .gap_3()
287                        .child(
288                            v_flex()
289                                .map(|this| {
290                                    if layout.is_horizontal() {
291                                        this.flex_1().max_w_3_5()
292                                    } else {
293                                        this.w_full()
294                                    }
295                                })
296                                .child(Label::new(title.clone()).text_sm())
297                                .when_some(description, |this, description| {
298                                    this.child(
299                                        div()
300                                            .size_full()
301                                            .text_sm()
302                                            .text_color(cx.theme().muted_foreground)
303                                            .child(description),
304                                    )
305                                }),
306                        )
307                        .child(div().id("field").child(Self::render_field(
308                            title,
309                            field,
310                            options.with_layout(layout).with_disabled(disabled),
311                            window,
312                            cx,
313                        )))
314                        .into_any_element()
315                }
316                SettingItem::Element {
317                    disabled, render, ..
318                } => div()
319                    .w_full()
320                    .when(disabled, |this| this.opacity(0.5))
321                    .child((render)(&options.with_disabled(disabled), window, cx))
322                    .into_any_element(),
323            })
324    }
325}