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        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}