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 fn required(mut self, required: bool) -> Self {
172 self.required = required;
173 self
174 }
175
176 pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
180 self.id = ix.into();
181 self.props = props;
182 self
183 }
184
185 pub fn items_start(mut self) -> Self {
187 self.align_items = Some(AlignItems::Start);
188 self
189 }
190
191 pub fn items_end(mut self) -> Self {
193 self.align_items = Some(AlignItems::End);
194 self
195 }
196
197 pub fn items_center(mut self) -> Self {
199 self.align_items = Some(AlignItems::Center);
200 self
201 }
202
203 pub fn col_span(mut self, col_span: u16) -> Self {
207 self.col_span = col_span;
208 self
209 }
210
211 pub fn col_start(mut self, col_start: i16) -> Self {
213 self.col_start = Some(col_start);
214 self
215 }
216
217 pub fn col_end(mut self, col_end: i16) -> Self {
219 self.col_end = Some(col_end);
220 self
221 }
222}
223
224impl ParentElement for Field {
225 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
226 self.children.extend(elements);
227 }
228}
229
230impl Styled for Field {
231 fn style(&mut self) -> &mut StyleRefinement {
232 &mut self.style
233 }
234}
235
236impl RenderOnce for Field {
237 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
238 let layout = self.props.layout;
239
240 let label_width = if layout.is_vertical() {
241 None
242 } else {
243 self.props.label_width
244 };
245 let has_label = self.label_indent;
246
247 #[inline]
248 fn wrap_div(layout: Axis) -> Div {
249 if layout.is_vertical() {
250 v_flex()
251 } else {
252 h_flex()
253 }
254 }
255
256 #[inline]
257 fn wrap_label(label_width: Option<Pixels>) -> Div {
258 div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
259 }
260
261 let gap = match self.props.size {
262 Size::Large => px(8.),
263 Size::XSmall | Size::Small => px(4.),
264 _ => px(4.),
265 };
266 let inner_gap = if layout.is_horizontal() {
267 gap
268 } else {
269 gap / 2.
270 };
271
272 v_flex()
273 .flex_1()
274 .gap(gap / 2.)
275 .col_span(self.col_span)
276 .when_some(self.col_start, |this, start| this.col_start(start))
277 .when_some(self.col_end, |this, end| this.col_end(end))
278 .refine_style(&self.style)
279 .child(
280 wrap_div(layout)
282 .id(self.id)
283 .gap(inner_gap)
284 .when_some(self.align_items, |this, align| {
285 this.map(|this| match align {
286 AlignItems::Start => this.items_start(),
287 AlignItems::End => this.items_end(),
288 AlignItems::Center => this.items_center(),
289 AlignItems::Baseline => this.items_baseline(),
290 _ => this,
291 })
292 })
293 .when(has_label, |this| {
294 this.child(
296 wrap_label(label_width)
297 .text_sm()
298 .when_some(self.props.label_text_size, |this, size| {
299 this.text_size(size)
300 })
301 .font_medium()
302 .gap_1()
303 .items_center()
304 .when_some(self.label, |this, builder| {
305 this.child(
306 h_flex()
307 .gap_1()
308 .child(
309 div()
310 .overflow_x_hidden()
311 .child(builder.render(window, cx)),
312 )
313 .when(self.required, |this| {
314 this.child(
315 div().text_color(cx.theme().danger).child("*"),
316 )
317 }),
318 )
319 }),
320 )
321 })
322 .child(div().w_full().flex_1().children(self.children)),
323 )
324 .child(
325 wrap_div(layout)
327 .gap(inner_gap)
328 .when(has_label && layout.is_horizontal(), |this| {
329 this.child(
330 wrap_label(label_width),
332 )
333 })
334 .when_some(self.description, |this, builder| {
335 this.child(
336 div()
337 .text_xs()
338 .text_color(cx.theme().muted_foreground)
339 .child(builder.render(window, cx)),
340 )
341 }),
342 )
343 }
344}