1use std::ops::Range;
2
3use crate::{
4 IconName, Sizable, Size, StyledExt,
5 group_box::GroupBoxVariant,
6 h_resizable,
7 input::{Input, InputState},
8 resizable_panel,
9 setting::{SettingGroup, SettingPage},
10 sidebar::{Sidebar, SidebarMenu, SidebarMenuItem},
11};
12use gpui::{
13 App, AppContext as _, Axis, ElementId, Entity, IntoElement, ParentElement as _, Pixels,
14 RenderOnce, StyleRefinement, Styled, Window, container_query, div, prelude::FluentBuilder as _,
15 px, relative,
16};
17use rust_i18n::t;
18
19const STACKED_LAYOUT_MAX_WIDTH: Pixels = px(480.);
20
21#[derive(IntoElement)]
34pub struct Settings {
35 id: ElementId,
36 pages: Vec<SettingPage>,
37 group_variant: GroupBoxVariant,
38 size: Size,
39 sidebar_width: Pixels,
40 sidebar_size_range: Range<Pixels>,
41 sidebar_style: StyleRefinement,
42 default_selected_index: SelectIndex,
43 header_style: StyleRefinement,
44}
45
46impl Settings {
47 pub fn new(id: impl Into<ElementId>) -> Self {
49 Self {
50 id: id.into(),
51 pages: vec![],
52 group_variant: GroupBoxVariant::default(),
53 size: Size::default(),
54 sidebar_width: px(250.0),
55 sidebar_size_range: px(160.0)..px(360.0),
56 sidebar_style: StyleRefinement::default(),
57 default_selected_index: SelectIndex::default(),
58 header_style: StyleRefinement::default(),
59 }
60 }
61
62 pub fn sidebar_width(mut self, width: impl Into<Pixels>) -> Self {
64 self.sidebar_width = width.into();
65 self
66 }
67
68 pub fn sidebar_size_range(mut self, range: impl Into<Range<Pixels>>) -> Self {
70 self.sidebar_size_range = range.into();
71 self
72 }
73
74 pub fn page(mut self, page: SettingPage) -> Self {
76 self.pages.push(page);
77 self
78 }
79
80 pub fn pages(mut self, pages: impl IntoIterator<Item = SettingPage>) -> Self {
82 self.pages.extend(pages);
83 self
84 }
85
86 pub fn with_group_variant(mut self, variant: GroupBoxVariant) -> Self {
90 self.group_variant = variant;
91 self
92 }
93
94 pub fn sidebar_style(mut self, style: &StyleRefinement) -> Self {
96 self.sidebar_style = style.clone();
97 self
98 }
99
100 pub fn default_selected_index(mut self, index: SelectIndex) -> Self {
102 self.default_selected_index = index;
103 self
104 }
105
106 pub fn header_style(mut self, style: &StyleRefinement) -> Self {
108 self.header_style = style.clone();
109 self
110 }
111
112 fn filtered_pages(&self, query: &str, cx: &App) -> Vec<SettingPage> {
113 self.pages
114 .iter()
115 .filter_map(|page| {
116 let filtered_groups: Vec<SettingGroup> = page
117 .groups
118 .iter()
119 .filter_map(|group| {
120 let mut group = group.clone();
121 group.items = group
122 .items
123 .iter()
124 .filter(|item| item.is_match(&query, cx))
125 .cloned()
126 .collect();
127 if group.items.is_empty() {
128 None
129 } else {
130 Some(group)
131 }
132 })
133 .collect();
134 let mut page = page.clone();
135 page.groups = filtered_groups;
136 if page.groups.is_empty() {
137 None
138 } else {
139 Some(page)
140 }
141 })
142 .collect()
143 }
144
145 fn render_active_page(
146 &self,
147 state: &Entity<SettingsState>,
148 pages: &Vec<SettingPage>,
149 options: &RenderOptions,
150 window: &mut Window,
151 cx: &mut App,
152 ) -> gpui::AnyElement {
153 let selected_index = state.read(cx).selected_index;
154
155 for (ix, page) in pages.into_iter().enumerate() {
156 if selected_index.page_ix == ix {
157 return page
158 .render(ix, state, &options, window, cx)
159 .into_any_element();
160 }
161 }
162
163 return div().into_any_element();
164 }
165
166 fn render_sidebar(
167 &self,
168 state: &Entity<SettingsState>,
169 pages: &Vec<SettingPage>,
170 _: &mut Window,
171 cx: &mut App,
172 ) -> impl IntoElement {
173 let selected_index = state.read(cx).selected_index;
174 let search_input = state.read(cx).search_input.clone();
175
176 Sidebar::new("settings-sidebar")
177 .w(relative(1.))
178 .border_0()
179 .refine_style(&self.sidebar_style)
180 .collapsible(false)
181 .collapsed(false)
182 .header(
183 div()
184 .w_full()
185 .refine_style(&self.header_style)
186 .child(Input::new(&search_input).prefix(IconName::Search)),
187 )
188 .child(
189 SidebarMenu::new().children(pages.iter().enumerate().map(|(page_ix, page)| {
190 let is_page_active =
191 selected_index.page_ix == page_ix && selected_index.group_ix.is_none();
192 SidebarMenuItem::new(page.title.clone())
193 .click_to_open(true)
194 .when_some(page.icon.clone(), |this, icon| this.icon(icon))
195 .default_open(page.default_open)
196 .active(is_page_active)
197 .on_click({
198 let state = state.clone();
199 move |_, _, cx| {
200 state.update(cx, |state, cx| {
201 state.selected_index = SelectIndex {
202 page_ix,
203 ..Default::default()
204 };
205 cx.notify();
206 })
207 }
208 })
209 .when(page.groups.len() > 1, |this| {
210 this.children(
211 page.groups
212 .iter()
213 .filter(|g| g.title.is_some())
214 .enumerate()
215 .map(|(group_ix, group)| {
216 let is_active = selected_index.page_ix == page_ix
217 && selected_index.group_ix == Some(group_ix);
218 let title = group.title.clone().unwrap_or_default();
219
220 SidebarMenuItem::new(title).active(is_active).on_click({
221 let state = state.clone();
222 move |_, _, cx| {
223 state.update(cx, |state, cx| {
224 state.selected_index = SelectIndex {
225 page_ix,
226 group_ix: Some(group_ix),
227 };
228 state.deferred_scroll_group_ix = Some(group_ix);
229 cx.notify();
230 })
231 }
232 })
233 }),
234 )
235 })
236 })),
237 )
238 }
239}
240
241impl Sizable for Settings {
242 fn with_size(mut self, size: impl Into<Size>) -> Self {
243 self.size = size.into();
244 self
245 }
246}
247
248pub(super) struct SettingsState {
249 pub(super) selected_index: SelectIndex,
250 pub(super) deferred_scroll_group_ix: Option<usize>,
252 pub(super) search_input: Entity<InputState>,
253}
254
255#[derive(Clone, Copy)]
265pub struct RenderOptions {
266 page_ix: usize,
267 group_ix: usize,
268 item_ix: usize,
269 size: Size,
270 group_variant: GroupBoxVariant,
271 layout: Axis,
272 disabled: bool,
273}
274
275impl RenderOptions {
276 pub fn new() -> Self {
277 Self {
278 page_ix: 0,
279 group_ix: 0,
280 item_ix: 0,
281 size: Size::default(),
282 group_variant: GroupBoxVariant::default(),
283 layout: Axis::Horizontal,
284 disabled: false,
285 }
286 }
287
288 pub fn with_page_ix(mut self, page_ix: usize) -> Self {
289 self.page_ix = page_ix;
290 self
291 }
292
293 pub fn with_group_ix(mut self, group_ix: usize) -> Self {
294 self.group_ix = group_ix;
295 self
296 }
297
298 pub fn with_item_ix(mut self, item_ix: usize) -> Self {
299 self.item_ix = item_ix;
300 self
301 }
302
303 pub fn with_size(mut self, size: Size) -> Self {
304 self.size = size;
305 self
306 }
307
308 pub fn with_group_variant(mut self, group_variant: GroupBoxVariant) -> Self {
309 self.group_variant = group_variant;
310 self
311 }
312
313 pub fn with_layout(mut self, layout: Axis) -> Self {
314 self.layout = layout;
315 self
316 }
317
318 pub fn with_disabled(mut self, disabled: bool) -> Self {
319 self.disabled = disabled;
320 self
321 }
322
323 pub fn page_ix(&self) -> usize {
324 self.page_ix
325 }
326
327 pub fn group_ix(&self) -> usize {
328 self.group_ix
329 }
330
331 pub fn item_ix(&self) -> usize {
332 self.item_ix
333 }
334
335 pub fn size(&self) -> Size {
336 self.size
337 }
338
339 pub fn group_variant(&self) -> GroupBoxVariant {
340 self.group_variant
341 }
342
343 pub fn layout(&self) -> Axis {
344 self.layout
345 }
346
347 pub fn is_disabled(&self) -> bool {
348 self.disabled
349 }
350}
351
352impl Default for RenderOptions {
353 fn default() -> Self {
354 Self::new()
355 }
356}
357
358#[derive(Clone, Copy, Default)]
359pub struct SelectIndex {
360 pub page_ix: usize,
361 pub group_ix: Option<usize>,
362}
363
364impl RenderOnce for Settings {
365 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
366 let state = window.use_keyed_state(self.id.clone(), cx, |window, cx| {
367 let search_input = cx.new(|cx| {
368 InputState::new(window, cx)
369 .placeholder(t!("Settings.search_placeholder"))
370 .default_value("")
371 });
372
373 SettingsState {
374 search_input,
375 selected_index: self.default_selected_index,
376 deferred_scroll_group_ix: None,
377 }
378 });
379
380 let query = state.read(cx).search_input.read(cx).value();
381 let filtered_pages = self.filtered_pages(&query, cx);
382 let options = RenderOptions::new()
383 .with_size(self.size)
384 .with_group_variant(self.group_variant);
385 let sidebar_size_range = self.sidebar_size_range.clone();
386 let sidebar = self
387 .render_sidebar(&state, &filtered_pages, window, cx)
388 .into_any_element();
389
390 h_resizable(self.id.clone())
391 .child(
392 resizable_panel()
393 .size(self.sidebar_width)
394 .size_range(sidebar_size_range)
395 .child(sidebar),
396 )
397 .child(
398 resizable_panel().child(container_query(move |size, window, cx| {
399 let options = options.with_layout(if size.width <= STACKED_LAYOUT_MAX_WIDTH {
400 Axis::Vertical
401 } else {
402 Axis::Horizontal
403 });
404 self.render_active_page(&state, &filtered_pages, &options, window, cx)
405 })),
406 )
407 }
408}