1use std::rc::Rc;
2
3use gpui::{
4 div, prelude::FluentBuilder as _, px, AlignItems, AnyElement, AnyView, App, Axis, Div, Element,
5 ElementId, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems, RenderOnce,
6 SharedString, Styled, Window,
7};
8
9use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, Size, StyledExt};
10
11#[derive(Clone, Copy)]
12pub(super) struct FieldProps {
13 pub(super) size: Size,
14 pub(super) layout: Axis,
15 pub(super) columns: usize,
16
17 pub(super) label_width: Option<Pixels>,
18 pub(super) label_text_size: Option<Rems>,
19}
20
21impl Default for FieldProps {
22 fn default() -> Self {
23 Self {
24 layout: Axis::Vertical,
25 size: Size::default(),
26 columns: 1,
27 label_width: Some(px(140.)),
28 label_text_size: None,
29 }
30 }
31}
32
33pub enum FieldBuilder {
34 String(SharedString),
35 Element(Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>),
36 View(AnyView),
37}
38
39impl Default for FieldBuilder {
40 fn default() -> Self {
41 Self::String(SharedString::default())
42 }
43}
44
45impl From<AnyView> for FieldBuilder {
46 fn from(view: AnyView) -> Self {
47 Self::View(view)
48 }
49}
50
51impl RenderOnce for FieldBuilder {
52 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
53 match self {
54 FieldBuilder::String(value) => value.into_any_element(),
55 FieldBuilder::Element(builder) => builder(window, cx),
56 FieldBuilder::View(view) => view.into_any(),
57 }
58 }
59}
60
61impl From<&'static str> for FieldBuilder {
62 fn from(value: &'static str) -> Self {
63 Self::String(value.into())
64 }
65}
66
67impl From<String> for FieldBuilder {
68 fn from(value: String) -> Self {
69 Self::String(value.into())
70 }
71}
72
73impl From<SharedString> for FieldBuilder {
74 fn from(value: SharedString) -> Self {
75 Self::String(value)
76 }
77}
78
79#[derive(IntoElement)]
81pub struct Field {
82 id: ElementId,
83 props: FieldProps,
84 label: Option<FieldBuilder>,
85 label_indent: bool,
86 description: Option<FieldBuilder>,
87 children: Vec<AnyElement>,
89 visible: bool,
90 required: bool,
91 align_items: Option<AlignItems>,
93 col_span: u16,
94 col_start: Option<i16>,
95 col_end: Option<i16>,
96}
97
98impl Field {
99 pub fn new() -> Self {
100 Self {
101 id: 0.into(),
102 label: None,
103 description: None,
104 children: Vec::new(),
105 visible: true,
106 required: false,
107 label_indent: true,
108 align_items: None,
109 props: FieldProps::default(),
110 col_span: 1,
111 col_start: None,
112 col_end: None,
113 }
114 }
115
116 pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
118 self.label = Some(label.into());
119 self
120 }
121
122 pub fn label_indent(mut self, indent: bool) -> Self {
128 self.label_indent = indent;
129 self
130 }
131
132 pub fn label_fn<F, E>(mut self, label: F) -> Self
134 where
135 E: IntoElement,
136 F: Fn(&mut Window, &mut App) -> E + 'static,
137 {
138 self.label = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
139 label(window, cx).into_any_element()
140 })));
141 self
142 }
143
144 pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
146 self.description = Some(description.into());
147 self
148 }
149
150 pub fn description_fn<F, E>(mut self, description: F) -> Self
152 where
153 E: IntoElement,
154 F: Fn(&mut Window, &mut App) -> E + 'static,
155 {
156 self.description = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
157 description(window, cx).into_any_element()
158 })));
159 self
160 }
161
162 pub fn visible(mut self, visible: bool) -> Self {
164 self.visible = visible;
165 self
166 }
167
168 pub fn required(mut self, required: bool) -> Self {
170 self.required = required;
171 self
172 }
173
174 pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
178 self.id = ix.into();
179 self.props = props;
180 self
181 }
182
183 pub fn items_start(mut self) -> Self {
185 self.align_items = Some(AlignItems::Start);
186 self
187 }
188
189 pub fn items_end(mut self) -> Self {
191 self.align_items = Some(AlignItems::End);
192 self
193 }
194
195 pub fn items_center(mut self) -> Self {
197 self.align_items = Some(AlignItems::Center);
198 self
199 }
200
201 pub fn col_span(mut self, col_span: u16) -> Self {
205 self.col_span = col_span;
206 self
207 }
208
209 pub fn col_start(mut self, col_start: i16) -> Self {
211 self.col_start = Some(col_start);
212 self
213 }
214
215 pub fn col_end(mut self, col_end: i16) -> Self {
217 self.col_end = Some(col_end);
218 self
219 }
220}
221
222impl ParentElement for Field {
223 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
224 self.children.extend(elements);
225 }
226}
227
228impl RenderOnce for Field {
229 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
230 let layout = self.props.layout;
231
232 let label_width = if layout.is_vertical() {
233 None
234 } else {
235 self.props.label_width
236 };
237 let has_label = self.label_indent;
238
239 #[inline]
240 fn wrap_div(layout: Axis) -> Div {
241 if layout.is_vertical() {
242 v_flex()
243 } else {
244 h_flex()
245 }
246 }
247
248 #[inline]
249 fn wrap_label(label_width: Option<Pixels>) -> Div {
250 div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
251 }
252
253 let gap = match self.props.size {
254 Size::Large => px(8.),
255 Size::XSmall | Size::Small => px(4.),
256 _ => px(4.),
257 };
258 let inner_gap = if layout.is_horizontal() {
259 gap
260 } else {
261 gap / 2.
262 };
263
264 v_flex()
265 .flex_1()
266 .gap(gap / 2.)
267 .col_span(self.col_span)
268 .when_some(self.col_start, |this, start| this.col_start(start))
269 .when_some(self.col_end, |this, end| this.col_end(end))
270 .child(
271 wrap_div(layout)
273 .id(self.id)
274 .gap(inner_gap)
275 .when_some(self.align_items, |this, align| {
276 this.map(|this| match align {
277 AlignItems::Start => this.items_start(),
278 AlignItems::End => this.items_end(),
279 AlignItems::Center => this.items_center(),
280 AlignItems::Baseline => this.items_baseline(),
281 _ => this,
282 })
283 })
284 .when(has_label, |this| {
285 this.child(
287 wrap_label(label_width)
288 .text_sm()
289 .when_some(self.props.label_text_size, |this, size| {
290 this.text_size(size)
291 })
292 .font_medium()
293 .gap_1()
294 .items_center()
295 .when_some(self.label, |this, builder| {
296 this.child(
297 h_flex()
298 .gap_1()
299 .child(
300 div()
301 .overflow_x_hidden()
302 .child(builder.render(window, cx)),
303 )
304 .when(self.required, |this| {
305 this.child(
306 div().text_color(cx.theme().danger).child("*"),
307 )
308 }),
309 )
310 }),
311 )
312 })
313 .child(
314 div()
315 .w_full()
316 .flex_1()
317 .overflow_x_hidden()
318 .children(self.children),
319 ),
320 )
321 .child(
322 wrap_div(layout)
324 .gap(inner_gap)
325 .when(has_label && layout.is_horizontal(), |this| {
326 this.child(
327 wrap_label(label_width),
329 )
330 })
331 .when_some(self.description, |this, builder| {
332 this.child(
333 div()
334 .text_xs()
335 .text_color(cx.theme().muted_foreground)
336 .child(builder.render(window, cx)),
337 )
338 }),
339 )
340 }
341}