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, 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 pub(super) fn render(
126 &self,
127 ix: usize,
128 group_indices: &[usize],
129 state: &Entity<SettingsState>,
130 options: &RenderOptions,
131 window: &mut Window,
132 cx: &mut App,
133 ) -> impl IntoElement {
134 let search_input = state.read(cx).search_input.clone();
135 let query = search_input.read(cx).value();
136 let groups = group_indices
137 .iter()
138 .map(|&ix| (ix, self.groups[ix].clone()))
139 .collect::<Vec<_>>();
140 let groups_count = groups.len();
141
142 let page_state = window.use_keyed_state(
143 SharedString::from(format!("list-state:{}", ix)),
144 cx,
145 |_, _| PageState {
146 list: ListState::new(groups_count, ListAlignment::Top, px(100.)),
147 query: query.clone(),
148 groups: group_indices.to_vec(),
149 },
150 );
151 let changed =
152 page_state.read(cx).query != query || page_state.read(cx).groups != group_indices;
153 let list_state = page_state.read(cx).list.clone();
154 if changed {
155 page_state.update(cx, |state, _| {
156 state.list.reset(groups_count);
157 state.query = query.clone();
158 state.groups = group_indices.to_vec();
159 });
160 }
161
162 let deferred_scroll_group_ix = state.read(cx).deferred_scroll_group_ix;
163 let scroll_group_ix = deferred_scroll_group_ix.or_else(|| {
164 changed
165 .then_some(state.read(cx).selected_index.group_ix)
166 .flatten()
167 });
168 if deferred_scroll_group_ix.is_some() {
169 state.update(cx, |state, _| state.deferred_scroll_group_ix = None);
170 }
171 if let Some(group_ix) = scroll_group_ix
172 && let Some(visible_ix) = group_indices.iter().position(|&ix| ix == group_ix)
173 {
174 list_state.scroll_to_reveal_item(visible_ix);
175 }
176
177 v_flex()
178 .id(ix)
179 .size_full()
180 .child(
181 v_flex()
182 .p_4()
183 .gap_3()
184 .border_b_1()
185 .border_color(cx.theme().border)
186 .refine_style(&self.header_style)
187 .child(
188 h_flex()
189 .justify_between()
190 .child(
191 h_flex()
192 .gap_1()
193 .child(self.title.clone())
194 .when_some(self.title_suffix.clone(), |this, suffix| {
195 this.child(suffix(window, cx))
196 }),
197 )
198 .when(self.is_resettable(&query, cx), |this| {
199 this.child(
200 Button::new("reset")
201 .icon(IconName::Undo2)
202 .ghost()
203 .small()
204 .tooltip(t!("Settings.Reset All"))
205 .on_click({
206 let page = self.clone();
207 let query = query.clone();
208 move |_, window, cx| {
209 page.reset_all(&query, window, cx);
210 }
211 }),
212 )
213 }),
214 )
215 .when_some(self.description.clone(), |this, description| {
216 this.child(
217 Label::new(description)
218 .text_sm()
219 .text_color(cx.theme().muted_foreground),
220 )
221 }),
222 )
223 .child(
224 div()
225 .px_4()
226 .relative()
227 .flex_1()
228 .w_full()
229 .child(
230 list(list_state.clone(), {
231 let query = query.clone();
232 let options = *options;
233 move |visible_ix, window, cx| {
234 let (group_ix, group) = groups[visible_ix].clone();
235 group
236 .py_4()
237 .render(
238 &query,
239 &options.with_page_ix(ix).with_group_ix(group_ix),
240 window,
241 cx,
242 )
243 .into_any_element()
244 }
245 })
246 .size_full(),
247 )
248 .vertical_scrollbar(&list_state),
249 )
250 }
251}
252
253struct PageState {
254 list: ListState,
255 query: SharedString,
256 groups: Vec<usize>,
257}