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#[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 pub fn title(mut self, title: impl Into<SharedString>) -> Self {
49 self.title = title.into();
50 self
51 }
52
53 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 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
69 self.icon = Some(icon.into());
70 self
71 }
72
73 pub fn description(mut self, description: impl Into<SharedString>) -> Self {
75 self.description = Some(description.into());
76 self
77 }
78
79 pub fn default_open(mut self, default_open: bool) -> Self {
81 self.default_open = default_open;
82 self
83 }
84
85 pub fn resettable(mut self, resettable: bool) -> Self {
89 self.resettable = resettable;
90 self
91 }
92
93 pub fn group(mut self, group: SettingGroup) -> Self {
95 self.groups.push(group);
96 self
97 }
98
99 pub fn groups(mut self, groups: impl IntoIterator<Item = SettingGroup>) -> Self {
101 self.groups.extend(groups);
102 self
103 }
104
105 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}