Skip to main content

gpui_kit/controls/
form_field.rs

1//! The label, description, and error a control wears.
2//!
3//! A field never decides whether a control is valid. It shows the error the
4//! host handed it, publishes that the control is invalid and whether it is
5//! required, and otherwise stays out of the way.
6
7use gpui::{
8    AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
9    prelude::FluentBuilder, px,
10};
11use gpui_kit_semantics::{NodeSpec, Role, Semantic};
12use gpui_kit_theme::{ActiveTheme, Space, TypeScale};
13
14use crate::foundation::{Ident, StyledExt, text as foundation_text};
15use crate::overlay::Kbd;
16
17/// A labelled control, with the secondary text a typist needs around it.
18///
19/// The description and the error are both shown. An error is information the
20/// description did not already carry — what went wrong this time, on top of
21/// what the field is for — so it is added rather than substituted. The one
22/// exception is an error worded exactly like the description, which would
23/// otherwise be printed twice; then only the error is drawn, because it is
24/// the one that also carries the failure.
25#[derive(IntoElement)]
26pub struct FormField {
27    ident: Ident,
28    label: SharedString,
29    control: Option<SharedString>,
30    description: Option<SharedString>,
31    error: Option<SharedString>,
32    hint: Option<SharedString>,
33    required: bool,
34    children: Vec<AnyElement>,
35}
36
37impl std::fmt::Debug for FormField {
38    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        formatter
40            .debug_struct("FormField")
41            .field("ident", &self.ident)
42            .field("label", &self.label)
43            .field("control", &self.control)
44            .field("required", &self.required)
45            .field("invalid", &self.error.is_some())
46            .finish()
47    }
48}
49
50impl FormField {
51    pub fn new(ident: impl Into<Ident>, label: impl Into<SharedString>) -> Self {
52        Self {
53            ident: ident.into(),
54            label: label.into(),
55            control: None,
56            description: None,
57            error: None,
58            hint: None,
59            required: false,
60            children: Vec::new(),
61        }
62    }
63
64    /// The semantic id of the control this label names.
65    ///
66    /// The label publishes the association, so a test that knows only what a
67    /// field is called can find the control it belongs to.
68    pub fn control(mut self, control: impl Into<SharedString>) -> Self {
69        self.control = Some(control.into());
70        self
71    }
72
73    /// What the field is for, shown whether or not anything went wrong.
74    pub fn description(mut self, description: impl Into<SharedString>) -> Self {
75        self.description = Some(description.into());
76        self
77    }
78
79    /// What the host says is wrong, in the host's own words.
80    pub fn error(mut self, error: impl Into<SharedString>) -> Self {
81        self.error = Some(error.into());
82        self
83    }
84
85    /// The keystroke that operates the control without the pointer.
86    pub fn hint(mut self, keystroke: impl Into<SharedString>) -> Self {
87        self.hint = Some(keystroke.into());
88        self
89    }
90
91    pub fn required(mut self, required: bool) -> Self {
92        self.required = required;
93        self
94    }
95
96    pub fn is_invalid(&self) -> bool {
97        self.error.is_some()
98    }
99
100    /// The description, unless the error would repeat it word for word.
101    fn shown_description(&self) -> Option<SharedString> {
102        match (&self.description, &self.error) {
103            (Some(description), Some(error)) if description == error => None,
104            (description, _) => description.clone(),
105        }
106    }
107}
108
109impl ParentElement for FormField {
110    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
111        self.children.extend(elements);
112    }
113}
114
115impl RenderOnce for FormField {
116    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
117        let theme = cx.theme().clone();
118        let invalid = self.error.is_some();
119        let field_id = self.ident.semantic_id();
120        let label_ident = self.ident.child("label");
121        let description = self.shown_description();
122
123        let mut label_spec = NodeSpec::new(label_ident.semantic_id(), Role::Text)
124            .parent(field_id.clone())
125            .text(self.label.clone());
126        if let Some(control) = self.control.clone() {
127            label_spec = label_spec.labels(control);
128        }
129
130        let label_element = foundation_text(&theme, TypeScale::Label, self.label.clone())
131            .row()
132            .gap_token(&theme, Space::Xs)
133            .when(self.required, |element| {
134                element.child(
135                    foundation_text(&theme, TypeScale::Label, SharedString::from("*"))
136                        .text_color(theme.colors.danger),
137                )
138            })
139            .when_some(self.hint.clone(), |element, keystroke| {
140                element.child(
141                    div()
142                        .ml_auto()
143                        .child(Kbd::new(keystroke).id(self.ident.child("hint"))),
144                )
145            })
146            .semantic_in(cx, label_spec);
147
148        let description = description.map(|text| {
149            foundation_text(&theme, TypeScale::Caption, text.clone())
150                .text_tone(&theme, gpui_kit_theme::TextTone::Muted)
151                .semantic_in(
152                    cx,
153                    NodeSpec::new(self.ident.child("description").semantic_id(), Role::Text)
154                        .parent(field_id.clone())
155                        .text(text),
156                )
157        });
158
159        let error = self.error.clone().map(|text| {
160            foundation_text(&theme, TypeScale::Caption, text.clone())
161                .text_color(theme.colors.danger)
162                .semantic_in(
163                    cx,
164                    NodeSpec::new(self.ident.child("error").semantic_id(), Role::Status)
165                        .parent(field_id.clone())
166                        .invalid(true)
167                        .text(text),
168                )
169        });
170
171        div()
172            .column()
173            .w_full()
174            .gap(px(theme.space(Space::Xs)))
175            .child(label_element)
176            .children(self.children)
177            .children(description)
178            .children(error)
179            .semantic_in(
180                cx,
181                NodeSpec::new(field_id, Role::Field)
182                    .text(self.label.clone())
183                    .required(self.required)
184                    .invalid(invalid),
185            )
186    }
187}