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::{BoolField, DropdownField, NumberField, SettingFieldRender, StringField},
13    },
14    text::Text,
15    v_flex,
16};
17
18/// Setting item.
19#[derive(Clone)]
20pub enum SettingItem {
21    /// A normal setting item with a title, description, and field.
22    Item {
23        title: SharedString,
24        description: Option<Text>,
25        layout: Axis,
26        field: Rc<dyn AnySettingField>,
27    },
28    /// A full custom element to render.
29    Element {
30        render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
31    },
32}
33
34impl SettingItem {
35    /// Create a new setting item.
36    pub fn new<F>(title: impl Into<SharedString>, field: F) -> Self
37    where
38        F: AnySettingField + 'static,
39    {
40        SettingItem::Item {
41            title: title.into(),
42            description: None,
43            layout: Axis::Horizontal,
44            field: Rc::new(field),
45        }
46    }
47
48    /// Create a new custom element setting item with a render closure.
49    pub fn render<R, E>(render: R) -> Self
50    where
51        E: IntoElement,
52        R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
53    {
54        SettingItem::Element {
55            render: Rc::new(move |options, window, cx| {
56                render(options, window, cx).into_any_element()
57            }),
58        }
59    }
60
61    /// Set the description of the setting item.
62    ///
63    /// Only applies to [`SettingItem::Item`].
64    pub fn description(mut self, description: impl Into<Text>) -> Self {
65        match &mut self {
66            SettingItem::Item { description: d, .. } => {
67                *d = Some(description.into());
68            }
69            SettingItem::Element { .. } => {}
70        }
71        self
72    }
73
74    /// Set the layout of the setting item.
75    ///
76    /// Only applies to [`SettingItem::Item`].
77    pub fn layout(mut self, layout: Axis) -> Self {
78        match &mut self {
79            SettingItem::Item { layout: l, .. } => {
80                *l = layout;
81            }
82            SettingItem::Element { .. } => {}
83        }
84        self
85    }
86
87    pub(crate) fn is_match(&self, query: &str) -> bool {
88        match self {
89            SettingItem::Item {
90                title, description, ..
91            } => {
92                title.to_lowercase().contains(&query.to_lowercase())
93                    || description.as_ref().map_or(false, |d| {
94                        d.as_str().to_lowercase().contains(&query.to_lowercase())
95                    })
96            }
97            // We need to show all custom elements when not searching.
98            SettingItem::Element { .. } => query.is_empty(),
99        }
100    }
101
102    pub(crate) fn is_resettable(&self, cx: &App) -> bool {
103        match self {
104            SettingItem::Item { field, .. } => field.is_resettable(cx),
105            SettingItem::Element { .. } => false,
106        }
107    }
108
109    pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
110        match self {
111            SettingItem::Item { field, .. } => field.reset(window, cx),
112            SettingItem::Element { .. } => {}
113        }
114    }
115
116    fn render_field(
117        field: Rc<dyn AnySettingField>,
118        options: RenderOptions,
119        window: &mut Window,
120        cx: &mut App,
121    ) -> impl IntoElement {
122        let field_type = field.field_type();
123        let style = field.style().clone();
124        let type_id = field.deref().type_id();
125        let renderer: Box<dyn SettingFieldRender> = match type_id {
126            t if t == std::any::TypeId::of::<bool>() => {
127                Box::new(BoolField::new(field_type.is_switch()))
128            }
129            t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
130                Box::new(NumberField::new(field_type.number_input_options()))
131            }
132            t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
133                Box::new(StringField::<SharedString>::new())
134            }
135            t if t == TypeId::of::<String>() && field_type.is_input() => {
136                Box::new(StringField::<String>::new())
137            }
138            t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => Box::new(
139                DropdownField::<SharedString>::new(field_type.dropdown_options()),
140            ),
141            t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
142                Box::new(DropdownField::<String>::new(field_type.dropdown_options()))
143            }
144            _ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
145            _ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
146        };
147
148        renderer.render(field, &options, &style, window, cx)
149    }
150
151    pub(super) fn render_item(
152        self,
153        options: &RenderOptions,
154        window: &mut Window,
155        cx: &mut App,
156    ) -> Stateful<Div> {
157        div()
158            .id(SharedString::from(format!("item-{}", options.item_ix)))
159            .w_full()
160            .child(match self {
161                SettingItem::Item {
162                    title,
163                    description,
164                    layout,
165                    field,
166                } => div()
167                    .w_full()
168                    .overflow_hidden()
169                    .map(|this| {
170                        if layout.is_horizontal() {
171                            this.h_flex().justify_between().items_start()
172                        } else {
173                            this.v_flex()
174                        }
175                    })
176                    .gap_3()
177                    .child(
178                        v_flex()
179                            .map(|this| {
180                                if layout.is_horizontal() {
181                                    this.flex_1().max_w_3_5()
182                                } else {
183                                    this.w_full()
184                                }
185                            })
186                            .gap_1()
187                            .child(Label::new(title.clone()).text_sm())
188                            .when_some(description.clone(), |this, description| {
189                                this.child(
190                                    div()
191                                        .size_full()
192                                        .text_sm()
193                                        .text_color(cx.theme().muted_foreground)
194                                        .child(description),
195                                )
196                            }),
197                    )
198                    .child(div().id("field").child(Self::render_field(
199                        field,
200                        RenderOptions { layout, ..*options },
201                        window,
202                        cx,
203                    )))
204                    .into_any_element(),
205                SettingItem::Element { render } => {
206                    (render)(&options, window, cx).into_any_element()
207                }
208            })
209    }
210}