1use crate::{
2 IconName, Sizable, Size, StyledExt,
3 group_box::GroupBoxVariant,
4 input::{Input, InputState},
5 resizable::{h_resizable, resizable_panel},
6 setting::{SettingGroup, SettingPage},
7 sidebar::{Sidebar, SidebarMenu, SidebarMenuItem},
8};
9use gpui::{
10 App, AppContext as _, Axis, ElementId, Entity, IntoElement, ParentElement as _, Pixels,
11 RenderOnce, StyleRefinement, Styled, Window, div, prelude::FluentBuilder as _, px, relative,
12};
13use rust_i18n::t;
14
15#[derive(IntoElement)]
28pub struct Settings {
29 id: ElementId,
30 pages: Vec<SettingPage>,
31 group_variant: GroupBoxVariant,
32 size: Size,
33 sidebar_width: Pixels,
34 sidebar_style: StyleRefinement,
35}
36
37impl Settings {
38 pub fn new(id: impl Into<ElementId>) -> Self {
40 Self {
41 id: id.into(),
42 pages: vec![],
43 group_variant: GroupBoxVariant::default(),
44 size: Size::default(),
45 sidebar_width: px(250.0),
46 sidebar_style: StyleRefinement::default(),
47 }
48 }
49
50 pub fn sidebar_width(mut self, width: impl Into<Pixels>) -> Self {
52 self.sidebar_width = width.into();
53 self
54 }
55
56 pub fn page(mut self, page: SettingPage) -> Self {
58 self.pages.push(page);
59 self
60 }
61
62 pub fn pages(mut self, pages: impl IntoIterator<Item = SettingPage>) -> Self {
64 self.pages.extend(pages);
65 self
66 }
67
68 pub fn with_group_variant(mut self, variant: GroupBoxVariant) -> Self {
72 self.group_variant = variant;
73 self
74 }
75
76 pub fn sidebar_style(mut self, style: &StyleRefinement) -> Self {
78 self.sidebar_style = style.clone();
79 self
80 }
81
82 fn filtered_pages(&self, query: &str) -> Vec<SettingPage> {
83 self.pages
84 .iter()
85 .filter_map(|page| {
86 let filtered_groups: Vec<SettingGroup> = page
87 .groups
88 .iter()
89 .filter_map(|group| {
90 let mut group = group.clone();
91 group.items = group
92 .items
93 .iter()
94 .filter(|item| item.is_match(&query))
95 .cloned()
96 .collect();
97 if group.items.is_empty() {
98 None
99 } else {
100 Some(group)
101 }
102 })
103 .collect();
104 let mut page = page.clone();
105 page.groups = filtered_groups;
106 if page.groups.is_empty() {
107 None
108 } else {
109 Some(page)
110 }
111 })
112 .collect()
113 }
114
115 fn render_active_page(
116 &self,
117 state: &Entity<SettingsState>,
118 pages: &Vec<SettingPage>,
119 options: &RenderOptions,
120 window: &mut Window,
121 cx: &mut App,
122 ) -> impl IntoElement {
123 let selected_index = state.read(cx).selected_index;
124
125 for (ix, page) in pages.into_iter().enumerate() {
126 if selected_index.page_ix == ix {
127 return page
128 .render(ix, state, &options, window, cx)
129 .into_any_element();
130 }
131 }
132
133 return div().into_any_element();
134 }
135
136 fn render_sidebar(
137 &self,
138 state: &Entity<SettingsState>,
139 pages: &Vec<SettingPage>,
140 _: &mut Window,
141 cx: &mut App,
142 ) -> impl IntoElement {
143 let selected_index = state.read(cx).selected_index;
144 let search_input = state.read(cx).search_input.clone();
145
146 Sidebar::left()
147 .w(relative(1.))
148 .border_0()
149 .refine_style(&self.sidebar_style)
150 .collapsed(false)
151 .header(
152 div()
153 .w_full()
154 .child(Input::new(&search_input).prefix(IconName::Search)),
155 )
156 .child(
157 SidebarMenu::new().children(pages.iter().enumerate().map(|(page_ix, page)| {
158 let is_page_active =
159 selected_index.page_ix == page_ix && selected_index.group_ix.is_none();
160 SidebarMenuItem::new(page.title.clone())
161 .default_open(page.default_open)
162 .active(is_page_active)
163 .on_click({
164 let state = state.clone();
165 move |_, _, cx| {
166 state.update(cx, |state, cx| {
167 state.selected_index = SelectIndex {
168 page_ix,
169 ..Default::default()
170 };
171 cx.notify();
172 })
173 }
174 })
175 .when(page.groups.len() > 1, |this| {
176 this.children(
177 page.groups
178 .iter()
179 .filter(|g| g.title.is_some())
180 .enumerate()
181 .map(|(group_ix, group)| {
182 let is_active = selected_index.page_ix == page_ix
183 && selected_index.group_ix == Some(group_ix);
184 let title = group.title.clone().unwrap_or_default();
185
186 SidebarMenuItem::new(title).active(is_active).on_click({
187 let state = state.clone();
188 move |_, _, cx| {
189 state.update(cx, |state, cx| {
190 state.selected_index = SelectIndex {
191 page_ix,
192 group_ix: Some(group_ix),
193 };
194 state.deferred_scroll_group_ix = Some(group_ix);
195 cx.notify();
196 })
197 }
198 })
199 }),
200 )
201 })
202 })),
203 )
204 }
205}
206
207impl Sizable for Settings {
208 fn with_size(mut self, size: impl Into<Size>) -> Self {
209 self.size = size.into();
210 self
211 }
212}
213
214pub(super) struct SettingsState {
215 pub(super) selected_index: SelectIndex,
216 pub(super) deferred_scroll_group_ix: Option<usize>,
218 pub(super) search_input: Entity<InputState>,
219}
220
221#[derive(Clone, Copy)]
223pub struct RenderOptions {
224 pub page_ix: usize,
225 pub group_ix: usize,
226 pub item_ix: usize,
227 pub size: Size,
228 pub group_variant: GroupBoxVariant,
229 pub layout: Axis,
230}
231
232#[derive(Clone, Copy, Default)]
233pub(super) struct SelectIndex {
234 page_ix: usize,
235 group_ix: Option<usize>,
236}
237
238impl RenderOnce for Settings {
239 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
240 let state = window.use_keyed_state(self.id.clone(), cx, |window, cx| {
241 let search_input = cx.new(|cx| {
242 InputState::new(window, cx)
243 .placeholder(t!("Settings.search_placeholder"))
244 .default_value("")
245 });
246
247 SettingsState {
248 search_input,
249 selected_index: SelectIndex::default(),
250 deferred_scroll_group_ix: None,
251 }
252 });
253
254 let query = state.read(cx).search_input.read(cx).value();
255 let filtered_pages = self.filtered_pages(&query);
256 let options = RenderOptions {
257 page_ix: 0,
258 group_ix: 0,
259 item_ix: 0,
260 size: self.size,
261 group_variant: self.group_variant,
262 layout: Axis::Horizontal,
263 };
264
265 h_resizable(self.id.clone())
266 .child(
267 resizable_panel()
268 .size(self.sidebar_width)
269 .child(self.render_sidebar(&state, &filtered_pages, window, cx)),
270 )
271 .child(resizable_panel().child(self.render_active_page(
272 &state,
273 &filtered_pages,
274 &options,
275 window,
276 cx,
277 )))
278 }
279}