1use std::rc::Rc;
2
3use gpui::{
4 AlignItems, AnyElement, AnyView, App, Axis, Div, ElementId, InteractiveElement as _,
5 IntoElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, StyleRefinement, Styled,
6 Window, div, prelude::FluentBuilder as _, px,
7};
8
9use crate::{ActiveTheme as _, AxisExt, Size, StyledExt, h_flex, v_flex};
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_element(),
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 style: StyleRefinement,
85 label: Option<FieldBuilder>,
86 label_indent: bool,
87 description: Option<FieldBuilder>,
88 children: Vec<AnyElement>,
90 visible: bool,
91 required: bool,
92 align_items: Option<AlignItems>,
94 col_span: u16,
95 col_start: Option<i16>,
96 col_end: Option<i16>,
97}
98
99impl Field {
100 pub fn new() -> Self {
101 Self {
102 id: 0.into(),
103 props: FieldProps::default(),
104 style: StyleRefinement::default(),
105 label: None,
106 description: None,
107 children: Vec::new(),
108 visible: true,
109 required: false,
110 label_indent: true,
111 align_items: None,
112 col_span: 1,
113 col_start: None,
114 col_end: None,
115 }
116 }
117
118 pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
120 self.label = Some(label.into());
121 self
122 }
123
124 pub fn label_indent(mut self, indent: bool) -> Self {
130 self.label_indent = indent;
131 self
132 }
133
134 pub fn label_fn<F, E>(mut self, label: F) -> Self
136 where
137 E: IntoElement,
138 F: Fn(&mut Window, &mut App) -> E + 'static,
139 {
140 self.label = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
141 label(window, cx).into_any_element()
142 })));
143 self
144 }
145
146 pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
148 self.description = Some(description.into());
149 self
150 }
151
152 pub fn description_fn<F, E>(mut self, description: F) -> Self
154 where
155 E: IntoElement,
156 F: Fn(&mut Window, &mut App) -> E + 'static,
157 {
158 self.description = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
159 description(window, cx).into_any_element()
160 })));
161 self
162 }
163
164 pub fn visible(mut self, visible: bool) -> Self {
166 self.visible = visible;
167 self
168 }
169
170 pub(super) fn is_visible(&self) -> bool {
172 self.visible
173 }
174
175 pub fn required(mut self, required: bool) -> Self {
177 self.required = required;
178 self
179 }
180
181 pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
185 self.id = ix.into();
186 self.props = props;
187 self
188 }
189
190 pub fn items_start(mut self) -> Self {
192 self.align_items = Some(AlignItems::Start);
193 self
194 }
195
196 pub fn items_end(mut self) -> Self {
198 self.align_items = Some(AlignItems::End);
199 self
200 }
201
202 pub fn items_center(mut self) -> Self {
204 self.align_items = Some(AlignItems::Center);
205 self
206 }
207
208 pub fn col_span(mut self, col_span: u16) -> Self {
212 self.col_span = col_span;
213 self
214 }
215
216 pub fn col_start(mut self, col_start: i16) -> Self {
218 self.col_start = Some(col_start);
219 self
220 }
221
222 pub fn col_end(mut self, col_end: i16) -> Self {
224 self.col_end = Some(col_end);
225 self
226 }
227}
228
229impl ParentElement for Field {
230 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
231 self.children.extend(elements);
232 }
233}
234
235impl Styled for Field {
236 fn style(&mut self) -> &mut StyleRefinement {
237 &mut self.style
238 }
239}
240
241impl RenderOnce for Field {
242 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
243 let layout = self.props.layout;
244
245 let label_width = if layout.is_vertical() {
246 None
247 } else {
248 self.props.label_width
249 };
250 let has_label = self.label_indent;
251
252 #[inline]
253 fn wrap_div(layout: Axis) -> Div {
254 if layout.is_vertical() {
255 v_flex()
256 } else {
257 h_flex()
258 }
259 }
260
261 #[inline]
262 fn wrap_label(label_width: Option<Pixels>) -> Div {
263 div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
264 }
265
266 let gap = match self.props.size {
267 Size::Large => px(8.),
268 Size::XSmall | Size::Small => px(4.),
269 _ => px(4.),
270 };
271 let inner_gap = if layout.is_horizontal() {
272 gap
273 } else {
274 gap / 2.
275 };
276
277 v_flex()
278 .flex_1()
279 .gap(gap / 2.)
280 .col_span(self.col_span)
281 .when_some(self.col_start, |this, start| this.col_start(start))
282 .when_some(self.col_end, |this, end| this.col_end(end))
283 .refine_style(&self.style)
284 .when(!self.visible, |this| this.hidden())
285 .child(
286 wrap_div(layout)
288 .id(self.id)
289 .gap(inner_gap)
290 .when_some(self.align_items, |this, align| {
291 this.map(|this| match align {
292 AlignItems::Start => this.items_start(),
293 AlignItems::End => this.items_end(),
294 AlignItems::Center => this.items_center(),
295 AlignItems::Baseline => this.items_baseline(),
296 _ => this,
297 })
298 })
299 .when(has_label, |this| {
300 this.child(
302 wrap_label(label_width)
303 .text_sm()
304 .when_some(self.props.label_text_size, |this, size| {
305 this.text_size(size)
306 })
307 .font_medium()
308 .gap_1()
309 .items_center()
310 .when_some(self.label, |this, builder| {
311 this.child(
312 h_flex()
313 .gap_1()
314 .child(
315 div()
316 .overflow_x_hidden()
317 .child(builder.render(window, cx)),
318 )
319 .when(self.required, |this| {
320 this.child(
321 div().text_color(cx.theme().danger).child("*"),
322 )
323 }),
324 )
325 }),
326 )
327 })
328 .child(div().w_full().flex_1().children(self.children)),
329 )
330 .child(
331 wrap_div(layout)
333 .gap(inner_gap)
334 .when(has_label && layout.is_horizontal(), |this| {
335 this.child(
336 wrap_label(label_width),
338 )
339 })
340 .when_some(self.description, |this, builder| {
341 this.child(
342 div()
343 .text_xs()
344 .text_color(cx.theme().muted_foreground)
345 .child(builder.render(window, cx)),
346 )
347 }),
348 )
349 }
350}