1use gpui::{
2 AnyElement, App, Axis, Div, InteractiveElement as _, IntoElement, ParentElement, SharedString,
3 Stateful, Styled, Window, div, prelude::FluentBuilder as _,
4};
5use std::{any::TypeId, ops::Deref, rc::Rc};
6
7use crate::{
8 ActiveTheme as _, AxisExt, StyledExt as _,
9 label::Label,
10 setting::{
11 AnySettingField, ElementField, RenderOptions,
12 fields::{
13 BoolField, DropdownField, NumberField, ResetHandler, SettingFieldRender, StringField,
14 },
15 },
16 text::Text,
17 v_flex,
18};
19
20#[derive(Clone)]
22pub enum SettingItem {
23 Item {
25 title: SharedString,
26 description: Option<Text>,
27 keywords: Vec<SharedString>,
28 layout: Axis,
29 disabled: bool,
30 field: Rc<dyn AnySettingField>,
31 },
32 Element {
34 disabled: bool,
35 keywords: Vec<SharedString>,
36 reset_handler: Option<ResetHandler>,
40 render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
41 },
42}
43
44impl SettingItem {
45 pub fn new<F>(title: impl Into<SharedString>, field: F) -> Self
47 where
48 F: AnySettingField + 'static,
49 {
50 SettingItem::Item {
51 title: title.into(),
52 description: None,
53 layout: Axis::Horizontal,
54 disabled: false,
55 keywords: Vec::new(),
56 field: Rc::new(field),
57 }
58 }
59
60 pub fn render<R, E>(render: R) -> Self
62 where
63 E: IntoElement,
64 R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
65 {
66 SettingItem::Element {
67 disabled: false,
68 keywords: Vec::new(),
69 reset_handler: None,
70 render: Rc::new(move |options, window, cx| {
71 render(options, window, cx).into_any_element()
72 }),
73 }
74 }
75
76 pub fn on_reset<D, R>(mut self, is_dirty: D, reset: R) -> Self
85 where
86 D: Fn(&App) -> bool + 'static,
87 R: Fn(&mut Window, &mut App) + 'static,
88 {
89 match &mut self {
90 SettingItem::Element { reset_handler, .. } => {
91 *reset_handler = Some((Rc::new(is_dirty), Rc::new(reset)));
92 }
93 SettingItem::Item { .. } => {
96 debug_assert!(
97 false,
98 "SettingItem::on_reset only applies to SettingItem::Element; \
99 use SettingField::default_value or SettingField::on_reset for a normal item"
100 );
101 }
102 }
103 self
104 }
105
106 pub fn keywords<I, S>(mut self, keywords: I) -> Self
112 where
113 I: IntoIterator<Item = S>,
114 S: Into<SharedString>,
115 {
116 let keywords: Vec<SharedString> = keywords.into_iter().map(Into::into).collect();
117 match &mut self {
118 SettingItem::Item { keywords: k, .. } => *k = keywords,
119 SettingItem::Element { keywords: k, .. } => *k = keywords,
120 }
121 self
122 }
123
124 pub fn disabled(mut self, disabled: bool) -> Self {
132 match &mut self {
133 SettingItem::Item { disabled: d, .. } => *d = disabled,
134 SettingItem::Element { disabled: d, .. } => *d = disabled,
135 }
136 self
137 }
138
139 pub fn description(mut self, description: impl Into<Text>) -> Self {
143 match &mut self {
144 SettingItem::Item { description: d, .. } => {
145 *d = Some(description.into());
146 }
147 SettingItem::Element { .. } => {}
148 }
149 self
150 }
151
152 pub fn layout(mut self, layout: Axis) -> Self {
156 match &mut self {
157 SettingItem::Item { layout: l, .. } => {
158 *l = layout;
159 }
160 SettingItem::Element { .. } => {}
161 }
162 self
163 }
164
165 pub(crate) fn is_match(&self, query: &str, cx: &App) -> bool {
166 match self {
167 SettingItem::Item {
168 title,
169 description,
170 keywords,
171 ..
172 } => {
173 let q = &query.to_lowercase();
174 title.to_lowercase().contains(q)
175 || description
176 .as_ref()
177 .map_or(false, |d| d.get_text(cx).to_lowercase().contains(q))
178 || keywords.iter().any(|s| s.to_lowercase().contains(q))
179 }
180 SettingItem::Element { keywords, .. } => {
182 let q = &query.to_lowercase();
183 query.is_empty() || keywords.iter().any(|s| s.to_lowercase().contains(q))
184 }
185 }
186 }
187
188 pub(crate) fn is_resettable(&self, cx: &App) -> bool {
189 match self {
190 SettingItem::Item { field, .. } => field.is_resettable(cx),
191 SettingItem::Element { reset_handler, .. } => reset_handler
192 .as_ref()
193 .is_some_and(|(is_dirty, _)| is_dirty(cx)),
194 }
195 }
196
197 pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
198 match self {
199 SettingItem::Item { field, .. } => field.reset(window, cx),
200 SettingItem::Element { reset_handler, .. } => {
201 if let Some((_, reset)) = reset_handler.as_ref() {
202 reset(window, cx);
203 }
204 }
205 }
206 }
207
208 fn render_field(
209 title: SharedString,
210 field: Rc<dyn AnySettingField>,
211 options: RenderOptions,
212 window: &mut Window,
213 cx: &mut App,
214 ) -> impl IntoElement {
215 let field_type = field.field_type();
216 let style = field.style().clone();
217 let type_id = field.deref().type_id();
218 let renderer: Box<dyn SettingFieldRender> = match type_id {
219 t if t == std::any::TypeId::of::<bool>() => {
220 Box::new(BoolField::new(field_type.is_switch(), title))
221 }
222 t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
223 Box::new(NumberField::new(field_type.number_input_options(), title))
224 }
225 t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
226 Box::new(StringField::<SharedString>::new(title))
227 }
228 t if t == TypeId::of::<String>() && field_type.is_input() => {
229 Box::new(StringField::<String>::new(title))
230 }
231 t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => {
232 Box::new(DropdownField::<SharedString>::new(
233 field_type.dropdown_options(),
234 field_type.dropdown_scrollable(),
235 title,
236 ))
237 }
238 t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
239 Box::new(DropdownField::<String>::new(
240 field_type.dropdown_options(),
241 field_type.dropdown_scrollable(),
242 title,
243 ))
244 }
245 _ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
246 _ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
247 };
248
249 renderer.render(field, &options, &style, window, cx)
250 }
251
252 pub(super) fn render_item(
253 self,
254 options: &RenderOptions,
255 window: &mut Window,
256 cx: &mut App,
257 ) -> Stateful<Div> {
258 div()
259 .id(SharedString::from(format!("item-{}", options.item_ix())))
260 .w_full()
261 .child(match self {
262 SettingItem::Item {
263 title,
264 description,
265 layout,
266 disabled,
267 field,
268 ..
269 } => {
270 let layout = if options.layout().is_vertical() {
271 Axis::Vertical
272 } else {
273 layout
274 };
275
276 div()
277 .w_full()
278 .when(disabled, |this| this.opacity(0.5))
279 .map(|this| {
280 if layout.is_horizontal() {
281 this.h_flex().justify_between().items_center()
282 } else {
283 this.v_flex()
284 }
285 })
286 .gap_3()
287 .child(
288 v_flex()
289 .map(|this| {
290 if layout.is_horizontal() {
291 this.flex_1().max_w_3_5()
292 } else {
293 this.w_full()
294 }
295 })
296 .child(Label::new(title.clone()).text_sm())
297 .when_some(description, |this, description| {
298 this.child(
299 div()
300 .size_full()
301 .text_sm()
302 .text_color(cx.theme().muted_foreground)
303 .child(description),
304 )
305 }),
306 )
307 .child(div().id("field").child(Self::render_field(
308 title,
309 field,
310 options.with_layout(layout).with_disabled(disabled),
311 window,
312 cx,
313 )))
314 .into_any_element()
315 }
316 SettingItem::Element {
317 disabled, render, ..
318 } => div()
319 .w_full()
320 .when(disabled, |this| this.opacity(0.5))
321 .child((render)(&options.with_disabled(disabled), window, cx))
322 .into_any_element(),
323 })
324 }
325}