Skip to main content

gpui_component/setting/
page.rs

1use std::rc::Rc;
2
3use gpui::{
4    AnyElement, App, Entity, InteractiveElement as _, IntoElement, ListAlignment, ListState,
5    ParentElement as _, SharedString, StyleRefinement, Styled, Window, div, list,
6    prelude::FluentBuilder as _, px,
7};
8use rust_i18n::t;
9
10use crate::{
11    ActiveTheme, Icon, IconName, Sizable, StyledExt,
12    button::{Button, ButtonVariants},
13    h_flex,
14    label::Label,
15    scroll::ScrollableElement,
16    setting::{RenderOptions, SettingGroup, settings::SettingsState},
17    v_flex,
18};
19
20/// A setting page that can contain multiple setting groups.
21#[derive(Clone)]
22pub struct SettingPage {
23    pub(super) icon: Option<Icon>,
24    resettable: bool,
25    pub(super) default_open: bool,
26    pub(super) title: SharedString,
27    pub(super) title_suffix: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,
28    pub(super) description: Option<SharedString>,
29    pub(super) groups: Vec<SettingGroup>,
30    pub(super) header_style: StyleRefinement,
31}
32
33impl SettingPage {
34    pub fn new(title: impl Into<SharedString>) -> Self {
35        Self {
36            icon: None,
37            resettable: true,
38            default_open: false,
39            title: title.into(),
40            title_suffix: None,
41            description: None,
42            groups: Vec::new(),
43            header_style: StyleRefinement::default(),
44        }
45    }
46
47    /// Set the title of the setting page.
48    pub fn title(mut self, title: impl Into<SharedString>) -> Self {
49        self.title = title.into();
50        self
51    }
52
53    /// Set a custom element to render after the title in the page header.
54    ///
55    /// For example, an info icon button that opens the help documentation.
56    pub fn title_suffix<F, E>(mut self, suffix: F) -> Self
57    where
58        E: IntoElement,
59        F: Fn(&mut Window, &mut App) -> E + 'static,
60    {
61        self.title_suffix = Some(Rc::new(move |window, cx| {
62            suffix(window, cx).into_any_element()
63        }));
64        self
65    }
66
67    /// Set the icon of the setting page.
68    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
69        self.icon = Some(icon.into());
70        self
71    }
72
73    /// Set the description of the setting page, default is None.
74    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
75        self.description = Some(description.into());
76        self
77    }
78
79    /// Set the default open state of the setting page, default is false.
80    pub fn default_open(mut self, default_open: bool) -> Self {
81        self.default_open = default_open;
82        self
83    }
84
85    /// Set whether the setting page is resettable, default is true.
86    ///
87    /// If true and the items in this page has changed, the reset button will appear.
88    pub fn resettable(mut self, resettable: bool) -> Self {
89        self.resettable = resettable;
90        self
91    }
92
93    /// Add a setting group to the page.
94    pub fn group(mut self, group: SettingGroup) -> Self {
95        self.groups.push(group);
96        self
97    }
98
99    /// Add multiple setting groups to the page.
100    pub fn groups(mut self, groups: impl IntoIterator<Item = SettingGroup>) -> Self {
101        self.groups.extend(groups);
102        self
103    }
104
105    /// Set the style refinement for the header of the setting page.
106    pub fn header_style(mut self, style: &StyleRefinement) -> Self {
107        self.header_style = style.clone();
108        self
109    }
110
111    fn is_resettable(&self, cx: &App) -> bool {
112        self.resettable && self.groups.iter().any(|group| group.is_resettable(cx))
113    }
114
115    fn reset_all(&self, window: &mut Window, cx: &mut App) {
116        for group in &self.groups {
117            group.reset(window, cx);
118        }
119    }
120
121    pub(super) fn render(
122        &self,
123        ix: usize,
124        state: &Entity<SettingsState>,
125        options: &RenderOptions,
126        window: &mut Window,
127        cx: &mut App,
128    ) -> impl IntoElement {
129        let search_input = state.read(cx).search_input.clone();
130        let query = search_input.read(cx).value();
131        let groups = self
132            .groups
133            .iter()
134            .filter(|group| group.is_match(&query, cx))
135            .cloned()
136            .collect::<Vec<_>>();
137        let groups_count = groups.len();
138
139        let list_state = window
140            .use_keyed_state(
141                SharedString::from(format!("list-state:{}", ix)),
142                cx,
143                |_, _| ListState::new(groups_count, ListAlignment::Top, px(100.)),
144            )
145            .read(cx)
146            .clone();
147
148        if list_state.item_count() != groups_count {
149            list_state.reset(groups_count);
150        }
151
152        let deferred_scroll_group_ix = state.read(cx).deferred_scroll_group_ix;
153        if let Some(ix) = deferred_scroll_group_ix {
154            state.update(cx, |state, _| {
155                state.deferred_scroll_group_ix = None;
156            });
157            list_state.scroll_to_reveal_item(ix);
158        }
159
160        v_flex()
161            .id(ix)
162            .size_full()
163            .child(
164                v_flex()
165                    .p_4()
166                    .gap_3()
167                    .border_b_1()
168                    .border_color(cx.theme().border)
169                    .refine_style(&self.header_style)
170                    .child(
171                        h_flex()
172                            .justify_between()
173                            .child(
174                                h_flex()
175                                    .gap_1()
176                                    .child(self.title.clone())
177                                    .when_some(self.title_suffix.clone(), |this, suffix| {
178                                        this.child(suffix(window, cx))
179                                    }),
180                            )
181                            .when(self.is_resettable(cx), |this| {
182                                this.child(
183                                    Button::new("reset")
184                                        .icon(IconName::Undo2)
185                                        .ghost()
186                                        .small()
187                                        .tooltip(t!("Settings.Reset All"))
188                                        .on_click({
189                                            let page = self.clone();
190                                            move |_, window, cx| {
191                                                page.reset_all(window, cx);
192                                            }
193                                        }),
194                                )
195                            }),
196                    )
197                    .when_some(self.description.clone(), |this, description| {
198                        this.child(
199                            Label::new(description)
200                                .text_sm()
201                                .text_color(cx.theme().muted_foreground),
202                        )
203                    }),
204            )
205            .child(
206                div()
207                    .px_4()
208                    .relative()
209                    .flex_1()
210                    .w_full()
211                    .child(
212                        list(list_state.clone(), {
213                            let query = query.clone();
214                            let options = *options;
215                            move |group_ix, window, cx| {
216                                let group = groups[group_ix].clone();
217                                group
218                                    .py_4()
219                                    .render(
220                                        &query,
221                                        &options.with_page_ix(ix).with_group_ix(group_ix),
222                                        window,
223                                        cx,
224                                    )
225                                    .into_any_element()
226                            }
227                        })
228                        .size_full(),
229                    )
230                    .vertical_scrollbar(&list_state),
231            )
232    }
233}