1use std::rc::Rc;
2
3use gpui::{
4 AnyElement, App, Entity, InteractiveElement as _, IntoElement, ListAlignment, ListOffset,
5 ListState, 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, query: &str, cx: &App) -> bool {
112 self.resettable
113 && self
114 .groups
115 .iter()
116 .any(|group| group.is_resettable(query, cx))
117 }
118
119 fn reset_all(&self, query: &str, window: &mut Window, cx: &mut App) {
120 for group in &self.groups {
121 group.reset(query, window, cx);
122 }
123 }
124
125 fn group_at_scroll_top(
131 &self,
132 group_indices: &[usize],
133 selected_group_ix: Option<usize>,
134 scroll_top: ListOffset,
135 is_scrolled_to_end: bool,
136 ) -> Option<usize> {
137 let selected_pos =
138 selected_group_ix.and_then(|ix| group_indices.iter().position(|&g| g == ix));
139 if is_scrolled_to_end && selected_pos.is_some_and(|pos| pos > scroll_top.item_ix) {
140 return selected_group_ix;
141 }
142
143 let top_pos = if is_scrolled_to_end {
144 group_indices.len().saturating_sub(1)
145 } else {
146 scroll_top.item_ix
147 };
148 group_indices
149 .iter()
150 .take(top_pos + 1)
151 .rev()
152 .copied()
153 .find(|&ix| self.groups[ix].title.is_some())
154 }
155
156 pub(super) fn render(
157 &self,
158 ix: usize,
159 group_indices: &[usize],
160 state: &Entity<SettingsState>,
161 options: &RenderOptions,
162 window: &mut Window,
163 cx: &mut App,
164 ) -> impl IntoElement {
165 let search_input = state.read(cx).search_input.clone();
166 let query = search_input.read(cx).value();
167 let groups = group_indices
168 .iter()
169 .map(|&ix| (ix, self.groups[ix].clone()))
170 .collect::<Vec<_>>();
171 let groups_count = groups.len();
172
173 let page_state = window.use_keyed_state(
174 SharedString::from(format!("list-state:{}", ix)),
175 cx,
176 |_, _| PageState {
177 list: ListState::new(groups_count, ListAlignment::Top, px(100.)),
178 query: query.clone(),
179 groups: group_indices.to_vec(),
180 scroll_top: ListOffset::default(),
181 scrolled_programmatically: false,
182 },
183 );
184 let changed =
185 page_state.read(cx).query != query || page_state.read(cx).groups != group_indices;
186 let list_state = page_state.read(cx).list.clone();
187 if changed {
188 page_state.update(cx, |state, _| {
189 state.list.reset(groups_count);
190 state.query = query.clone();
191 state.groups = group_indices.to_vec();
192 });
193 }
194
195 let deferred_scroll_group_ix = state.read(cx).deferred_scroll_group_ix;
196 let scroll_group_ix = deferred_scroll_group_ix.or_else(|| {
197 changed
198 .then_some(state.read(cx).selected_index.group_ix)
199 .flatten()
200 });
201 if deferred_scroll_group_ix.is_some() {
202 state.update(cx, |state, _| state.deferred_scroll_group_ix = None);
203 }
204 if let Some(group_ix) = scroll_group_ix
205 && let Some(visible_ix) = group_indices.iter().position(|&ix| ix == group_ix)
206 {
207 list_state.scroll_to(ListOffset {
210 item_ix: visible_ix,
211 offset_in_item: px(0.),
212 });
213 }
214
215 let scroll_top = list_state.logical_scroll_top();
217 let programmatic = changed || scroll_group_ix.is_some();
218 let scrolled = !programmatic
219 && !page_state.read(cx).scrolled_programmatically
220 && page_state.read(cx).scroll_top != scroll_top;
221 page_state.update(cx, |state, _| {
222 state.scroll_top = scroll_top;
223 state.scrolled_programmatically = programmatic;
224 });
225 if scrolled {
226 let selected_group_ix = state.read(cx).selected_index.group_ix;
227 let group_ix = self.group_at_scroll_top(
228 group_indices,
229 selected_group_ix,
230 scroll_top,
231 list_state.is_scrolled_to_end() == Some(true),
232 );
233 if group_ix != selected_group_ix {
234 state.update(cx, |state, _| state.selected_index.group_ix = group_ix);
235 window.refresh();
237 }
238 }
239
240 v_flex()
241 .id(ix)
242 .size_full()
243 .child(
244 v_flex()
245 .p_4()
246 .gap_3()
247 .border_b_1()
248 .border_color(cx.theme().border)
249 .refine_style(&self.header_style)
250 .child(
251 h_flex()
252 .justify_between()
253 .child(
254 h_flex()
255 .gap_1()
256 .child(self.title.clone())
257 .when_some(self.title_suffix.clone(), |this, suffix| {
258 this.child(suffix(window, cx))
259 }),
260 )
261 .when(self.is_resettable(&query, cx), |this| {
262 this.child(
263 Button::new("reset")
264 .icon(IconName::Undo2)
265 .ghost()
266 .small()
267 .tooltip(t!("Settings.Reset All"))
268 .on_click({
269 let page = self.clone();
270 let query = query.clone();
271 move |_, window, cx| {
272 page.reset_all(&query, window, cx);
273 }
274 }),
275 )
276 }),
277 )
278 .when_some(self.description.clone(), |this, description| {
279 this.child(
280 Label::new(description)
281 .text_sm()
282 .text_color(cx.theme().muted_foreground),
283 )
284 }),
285 )
286 .child(
287 div()
288 .relative()
289 .flex_1()
290 .w_full()
291 .child(gpui_base::ScrollBounce::new(
292 "bounce",
293 &list_state,
294 list(list_state.clone(), {
295 let query = query.clone();
296 let options = *options;
297 move |visible_ix, window, cx| {
298 let (group_ix, group) = groups[visible_ix].clone();
299 div()
302 .w_full()
303 .px_4()
304 .child(group.py_4().render(
305 &query,
306 &options.with_page_ix(ix).with_group_ix(group_ix),
307 window,
308 cx,
309 ))
310 .into_any_element()
311 }
312 })
313 .size_full(),
314 ))
315 .vertical_scrollbar(&list_state),
316 )
317 }
318}
319
320struct PageState {
321 list: ListState,
322 query: SharedString,
323 groups: Vec<usize>,
324 scroll_top: ListOffset,
326 scrolled_programmatically: bool,
329}