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::{BoolField, DropdownField, NumberField, SettingFieldRender, StringField},
13 },
14 text::Text,
15 v_flex,
16};
17
18#[derive(Clone)]
20pub enum SettingItem {
21 Item {
23 title: SharedString,
24 description: Option<Text>,
25 layout: Axis,
26 field: Rc<dyn AnySettingField>,
27 },
28 Element {
30 render: Rc<dyn Fn(&RenderOptions, &mut Window, &mut App) -> AnyElement + 'static>,
31 },
32}
33
34impl SettingItem {
35 pub fn new<F>(title: impl Into<SharedString>, field: F) -> Self
37 where
38 F: AnySettingField + 'static,
39 {
40 SettingItem::Item {
41 title: title.into(),
42 description: None,
43 layout: Axis::Horizontal,
44 field: Rc::new(field),
45 }
46 }
47
48 pub fn render<R, E>(render: R) -> Self
50 where
51 E: IntoElement,
52 R: Fn(&RenderOptions, &mut Window, &mut App) -> E + 'static,
53 {
54 SettingItem::Element {
55 render: Rc::new(move |options, window, cx| {
56 render(options, window, cx).into_any_element()
57 }),
58 }
59 }
60
61 pub fn description(mut self, description: impl Into<Text>) -> Self {
65 match &mut self {
66 SettingItem::Item { description: d, .. } => {
67 *d = Some(description.into());
68 }
69 SettingItem::Element { .. } => {}
70 }
71 self
72 }
73
74 pub fn layout(mut self, layout: Axis) -> Self {
78 match &mut self {
79 SettingItem::Item { layout: l, .. } => {
80 *l = layout;
81 }
82 SettingItem::Element { .. } => {}
83 }
84 self
85 }
86
87 pub(crate) fn is_match(&self, query: &str) -> bool {
88 match self {
89 SettingItem::Item {
90 title, description, ..
91 } => {
92 title.to_lowercase().contains(&query.to_lowercase())
93 || description.as_ref().map_or(false, |d| {
94 d.as_str().to_lowercase().contains(&query.to_lowercase())
95 })
96 }
97 SettingItem::Element { .. } => query.is_empty(),
99 }
100 }
101
102 pub(crate) fn is_resettable(&self, cx: &App) -> bool {
103 match self {
104 SettingItem::Item { field, .. } => field.is_resettable(cx),
105 SettingItem::Element { .. } => false,
106 }
107 }
108
109 pub(crate) fn reset(&self, window: &mut Window, cx: &mut App) {
110 match self {
111 SettingItem::Item { field, .. } => field.reset(window, cx),
112 SettingItem::Element { .. } => {}
113 }
114 }
115
116 fn render_field(
117 field: Rc<dyn AnySettingField>,
118 options: RenderOptions,
119 window: &mut Window,
120 cx: &mut App,
121 ) -> impl IntoElement {
122 let field_type = field.field_type();
123 let style = field.style().clone();
124 let type_id = field.deref().type_id();
125 let renderer: Box<dyn SettingFieldRender> = match type_id {
126 t if t == std::any::TypeId::of::<bool>() => {
127 Box::new(BoolField::new(field_type.is_switch()))
128 }
129 t if t == TypeId::of::<f64>() && field_type.is_number_input() => {
130 Box::new(NumberField::new(field_type.number_input_options()))
131 }
132 t if t == TypeId::of::<SharedString>() && field_type.is_input() => {
133 Box::new(StringField::<SharedString>::new())
134 }
135 t if t == TypeId::of::<String>() && field_type.is_input() => {
136 Box::new(StringField::<String>::new())
137 }
138 t if t == TypeId::of::<SharedString>() && field_type.is_dropdown() => Box::new(
139 DropdownField::<SharedString>::new(field_type.dropdown_options()),
140 ),
141 t if t == TypeId::of::<String>() && field_type.is_dropdown() => {
142 Box::new(DropdownField::<String>::new(field_type.dropdown_options()))
143 }
144 _ if field_type.is_element() => Box::new(ElementField::new(field_type.element())),
145 _ => unimplemented!("Unsupported setting type: {}", field.deref().type_name()),
146 };
147
148 renderer.render(field, &options, &style, window, cx)
149 }
150
151 pub(super) fn render_item(
152 self,
153 options: &RenderOptions,
154 window: &mut Window,
155 cx: &mut App,
156 ) -> Stateful<Div> {
157 div()
158 .id(SharedString::from(format!("item-{}", options.item_ix)))
159 .w_full()
160 .child(match self {
161 SettingItem::Item {
162 title,
163 description,
164 layout,
165 field,
166 } => div()
167 .w_full()
168 .overflow_hidden()
169 .map(|this| {
170 if layout.is_horizontal() {
171 this.h_flex().justify_between().items_start()
172 } else {
173 this.v_flex()
174 }
175 })
176 .gap_3()
177 .child(
178 v_flex()
179 .map(|this| {
180 if layout.is_horizontal() {
181 this.flex_1().max_w_3_5()
182 } else {
183 this.w_full()
184 }
185 })
186 .gap_1()
187 .child(Label::new(title.clone()).text_sm())
188 .when_some(description.clone(), |this, description| {
189 this.child(
190 div()
191 .size_full()
192 .text_sm()
193 .text_color(cx.theme().muted_foreground)
194 .child(description),
195 )
196 }),
197 )
198 .child(div().id("field").child(Self::render_field(
199 field,
200 RenderOptions { layout, ..*options },
201 window,
202 cx,
203 )))
204 .into_any_element(),
205 SettingItem::Element { render } => {
206 (render)(&options, window, cx).into_any_element()
207 }
208 })
209 }
210}