use gpui::{
AnyElement, App, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window, div,
prelude::FluentBuilder, px,
};
use gpui_kit_semantics::{NodeSpec, Role, Semantic};
use gpui_kit_theme::{ActiveTheme, Space, TypeScale};
use crate::foundation::{Ident, StyledExt, text as foundation_text};
use crate::overlay::Kbd;
#[derive(IntoElement)]
pub struct FormField {
ident: Ident,
label: SharedString,
control: Option<SharedString>,
description: Option<SharedString>,
error: Option<SharedString>,
hint: Option<SharedString>,
required: bool,
children: Vec<AnyElement>,
}
impl std::fmt::Debug for FormField {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("FormField")
.field("ident", &self.ident)
.field("label", &self.label)
.field("control", &self.control)
.field("required", &self.required)
.field("invalid", &self.error.is_some())
.finish()
}
}
impl FormField {
pub fn new(ident: impl Into<Ident>, label: impl Into<SharedString>) -> Self {
Self {
ident: ident.into(),
label: label.into(),
control: None,
description: None,
error: None,
hint: None,
required: false,
children: Vec::new(),
}
}
pub fn control(mut self, control: impl Into<SharedString>) -> Self {
self.control = Some(control.into());
self
}
pub fn description(mut self, description: impl Into<SharedString>) -> Self {
self.description = Some(description.into());
self
}
pub fn error(mut self, error: impl Into<SharedString>) -> Self {
self.error = Some(error.into());
self
}
pub fn hint(mut self, keystroke: impl Into<SharedString>) -> Self {
self.hint = Some(keystroke.into());
self
}
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn is_invalid(&self) -> bool {
self.error.is_some()
}
fn shown_description(&self) -> Option<SharedString> {
match (&self.description, &self.error) {
(Some(description), Some(error)) if description == error => None,
(description, _) => description.clone(),
}
}
}
impl ParentElement for FormField {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for FormField {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.theme().clone();
let invalid = self.error.is_some();
let field_id = self.ident.semantic_id();
let label_ident = self.ident.child("label");
let description = self.shown_description();
let mut label_spec = NodeSpec::new(label_ident.semantic_id(), Role::Text)
.parent(field_id.clone())
.text(self.label.clone());
if let Some(control) = self.control.clone() {
label_spec = label_spec.labels(control);
}
let label_element = foundation_text(&theme, TypeScale::Label, self.label.clone())
.row()
.gap_token(&theme, Space::Xs)
.when(self.required, |element| {
element.child(
foundation_text(&theme, TypeScale::Label, SharedString::from("*"))
.text_color(theme.colors.danger),
)
})
.when_some(self.hint.clone(), |element, keystroke| {
element.child(
div()
.ml_auto()
.child(Kbd::new(keystroke).id(self.ident.child("hint"))),
)
})
.semantic_in(cx, label_spec);
let description = description.map(|text| {
foundation_text(&theme, TypeScale::Caption, text.clone())
.text_tone(&theme, gpui_kit_theme::TextTone::Muted)
.semantic_in(
cx,
NodeSpec::new(self.ident.child("description").semantic_id(), Role::Text)
.parent(field_id.clone())
.text(text),
)
});
let error = self.error.clone().map(|text| {
foundation_text(&theme, TypeScale::Caption, text.clone())
.text_color(theme.colors.danger)
.semantic_in(
cx,
NodeSpec::new(self.ident.child("error").semantic_id(), Role::Status)
.parent(field_id.clone())
.invalid(true)
.text(text),
)
});
div()
.column()
.w_full()
.gap(px(theme.space(Space::Xs)))
.child(label_element)
.children(self.children)
.children(description)
.children(error)
.semantic_in(
cx,
NodeSpec::new(field_id, Role::Field)
.text(self.label.clone())
.required(self.required)
.invalid(invalid),
)
}
}