use std::{cell::RefCell, rc::Rc, sync::Arc};
use gpui::{
px, AnyElement, App, Entity, FocusHandle, InteractiveElement, IntoElement, KeyDownEvent,
KeyUpEvent, ParentElement, RenderOnce, SharedString, Styled, Window,
};
use crate::{input::InputState, number_field::NumberState, validation::ValidationErrors};
#[derive(Clone, Debug, PartialEq)]
pub enum FormValue {
Text(SharedString),
Number(f64),
Flag(bool),
Keys(Vec<SharedString>),
}
impl FormValue {
pub fn as_text(&self) -> SharedString {
match self {
FormValue::Text(t) => t.clone(),
FormValue::Number(n) => SharedString::from(n.to_string()),
FormValue::Flag(true) => SharedString::from("on"),
FormValue::Flag(false) => SharedString::from(""),
FormValue::Keys(k) => SharedString::from(
k.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(","),
),
}
}
pub fn is_empty(&self) -> bool {
match self {
FormValue::Text(t) => t.trim().is_empty(),
FormValue::Number(_) => false,
FormValue::Flag(v) => !v,
FormValue::Keys(k) => k.is_empty(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FormData {
entries: Vec<(SharedString, FormValue)>,
}
impl FormData {
pub fn get(&self, name: &str) -> Option<&FormValue> {
self.entries.iter().find(|(n, _)| n == name).map(|(_, v)| v)
}
pub fn text(&self, name: &str) -> Option<SharedString> {
self.get(name).map(FormValue::as_text)
}
pub fn get_all(&self, name: &str) -> Vec<SharedString> {
self.entries
.iter()
.filter(|(entry_name, _)| entry_name == name)
.flat_map(|(_, value)| match value {
FormValue::Keys(keys) => keys.clone(),
value => vec![value.as_text()],
})
.collect()
}
pub fn iter(&self) -> impl Iterator<Item = (&SharedString, &FormValue)> {
self.entries.iter().map(|(n, v)| (n, v))
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn missing_required(&self, required: &[SharedString]) -> Vec<SharedString> {
required
.iter()
.filter(|name| self.get(name).is_none_or(FormValue::is_empty))
.cloned()
.collect()
}
}
type Read = Arc<dyn Fn(&App) -> FormValue + 'static>;
type Restore = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
type ReadName = Arc<dyn Fn(&App) -> Option<SharedString> + 'static>;
type ReadBehavior = Arc<dyn Fn(&App) -> ValidationBehavior + 'static>;
type ReadSuccessful = Arc<dyn Fn(&App) -> bool + 'static>;
type ReadInvalid = Arc<dyn Fn(&App) -> bool + 'static>;
type ReadServerErrors = Arc<dyn Fn(&App) -> Vec<SharedString> + 'static>;
type SetServerErrors = Arc<dyn Fn(&mut App, Vec<SharedString>, u64) + 'static>;
type ClearServerErrors = Arc<dyn Fn(&mut App) + 'static>;
type FocusField = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
type SubmitsOnEnter = Arc<dyn Fn(&Window, &App) -> bool + 'static>;
type ReadReadOnly = Arc<dyn Fn(&App) -> bool + 'static>;
pub(crate) struct LiveFormFieldState {
pub(crate) value: FormValue,
pub(crate) is_invalid: bool,
pub(crate) is_successful: bool,
pub(crate) focus: Option<FocusHandle>,
pub(crate) restore: Option<Restore>,
}
#[derive(Clone)]
pub struct FormField {
name: Option<SharedString>,
name_of: Option<ReadName>,
read: Read,
restore: Option<Restore>,
is_required: bool,
validation_behavior: ValidationBehavior,
behavior_of: Option<ReadBehavior>,
successful_of: Option<ReadSuccessful>,
invalid_of: Option<ReadInvalid>,
server_errors_of: Option<ReadServerErrors>,
set_server_errors: Option<SetServerErrors>,
clear_server_errors: Option<ClearServerErrors>,
focus: Option<FocusField>,
submits_on_enter_of: Option<SubmitsOnEnter>,
read_only_of: Option<ReadReadOnly>,
state_id: Option<gpui::EntityId>,
empty_keys_submits_empty: bool,
}
impl FormField {
pub fn text(state: Entity<InputState>) -> Self {
let read_state = state.clone();
let name_state = state.clone();
let behavior_state = state.clone();
let successful_state = state.clone();
let invalid_state = state.clone();
let enter_state = state.clone();
let read_only_state = state.clone();
let routed_read_state = state.clone();
let routed_set_state = state.clone();
let routed_clear_state = state.clone();
let state_id = state.entity_id();
let focus_state = state;
Self {
name: None,
name_of: Some(Arc::new(move |cx: &App| name_state.read(cx).name())),
behavior_of: Some(Arc::new(move |cx: &App| {
behavior_state.read(cx).validation_behavior()
})),
successful_of: Some(Arc::new(move |cx: &App| {
successful_state.read(cx).is_successful()
})),
invalid_of: Some(Arc::new(move |cx: &App| {
invalid_state.read(cx).validity().is_invalid
})),
server_errors_of: Some(Arc::new(move |cx: &App| {
routed_read_state.read(cx).routed_errors().to_vec()
})),
set_server_errors: Some(Arc::new(
move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
if routed_set_state.read(cx).routed_revision() == revision {
return;
}
routed_set_state.update(cx, |s, cx| {
s.set_routed(messages, revision);
cx.notify();
});
},
)),
clear_server_errors: Some(Arc::new(move |cx: &mut App| {
if !routed_clear_state.read(cx).routed_errors().is_empty() {
routed_clear_state.update(cx, |s, cx| {
s.clear_routed_errors();
cx.notify();
});
}
})),
read: Arc::new(move |cx: &App| {
FormValue::Text(SharedString::from(read_state.read(cx).value().to_owned()))
}),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
focus: Some(Arc::new(move |window, cx| {
let fh = focus_state.read(cx).focus_handle.clone();
window.focus(&fh, cx);
})),
submits_on_enter_of: Some(Arc::new(move |window, cx| {
enter_state.read(cx).focus_handle.is_focused(window)
})),
read_only_of: Some(Arc::new(move |cx: &App| {
read_only_state.read(cx).is_read_only()
})),
state_id: Some(state_id),
empty_keys_submits_empty: false,
}
}
pub fn text_area(state: Entity<InputState>) -> Self {
let field = Self::text(state);
Self {
submits_on_enter_of: None,
..field
}
}
pub(crate) fn live_text(
name: impl Into<SharedString>,
state: Rc<RefCell<LiveFormFieldState>>,
input: Entity<InputState>,
) -> Self {
let live = Self::live(name, state);
let text = Self::text(input);
Self {
name: live.name,
read: live.read,
restore: live.restore,
focus: live.focus,
empty_keys_submits_empty: true,
..text
}
}
pub fn number(state: Entity<NumberState>) -> Self {
let read_state = state.clone();
let name_state = state.clone();
let behavior_state = state.clone();
let successful_state = state.clone();
let invalid_state = state.clone();
let enter_state = state.clone();
let read_only_state = state.clone();
let routed_read_state = state.clone();
let routed_set_state = state.clone();
let routed_clear_state = state.clone();
let state_id = state.entity_id();
let focus_state = state;
Self {
name: None,
name_of: Some(Arc::new(move |cx: &App| {
let st = name_state.read(cx);
st.input.read(cx).name()
})),
behavior_of: Some(Arc::new(move |cx: &App| {
behavior_state.read(cx).input.read(cx).validation_behavior()
})),
successful_of: Some(Arc::new(move |cx: &App| {
successful_state.read(cx).input.read(cx).is_successful()
})),
invalid_of: Some(Arc::new(move |cx: &App| {
invalid_state.read(cx).input.read(cx).validity().is_invalid
})),
server_errors_of: Some(Arc::new(move |cx: &App| {
routed_read_state
.read(cx)
.input
.read(cx)
.routed_errors()
.to_vec()
})),
set_server_errors: Some(Arc::new(
move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
let inner = routed_set_state.read(cx).input.clone();
if inner.read(cx).routed_revision() == revision {
return;
}
inner.update(cx, |s, cx| {
s.set_routed(messages, revision);
cx.notify();
});
},
)),
clear_server_errors: Some(Arc::new(move |cx: &mut App| {
let inner = routed_clear_state.read(cx).input.clone();
if !inner.read(cx).routed_errors().is_empty() {
inner.update(cx, |s, cx| {
s.clear_routed_errors();
cx.notify();
});
}
})),
read: Arc::new(move |cx: &App| FormValue::Number(read_state.read(cx).value())),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
focus: Some(Arc::new(move |window, cx| {
let fh = focus_state.read(cx).input.read(cx).focus_handle.clone();
window.focus(&fh, cx);
})),
submits_on_enter_of: Some(Arc::new(move |window, cx| {
enter_state
.read(cx)
.input
.read(cx)
.focus_handle
.is_focused(window)
})),
read_only_of: Some(Arc::new(move |cx: &App| {
read_only_state.read(cx).input.read(cx).is_read_only()
})),
state_id: Some(state_id),
empty_keys_submits_empty: false,
}
}
pub fn code(name: impl Into<SharedString>, state: Entity<crate::input_otp::OtpState>) -> Self {
let read_state = state.clone();
let successful_state = state.clone();
let invalid_state = state.clone();
let enter_state = state.clone();
let routed_read_state = state.clone();
let routed_set_state = state.clone();
let routed_clear_state = state.clone();
let state_id = state.entity_id();
let focus_state = state;
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |cx: &App| {
FormValue::Text(SharedString::from(read_state.read(cx).code()))
}),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: Some(Arc::new(move |cx: &App| {
successful_state.read(cx).is_successful()
})),
invalid_of: Some(Arc::new(move |cx: &App| {
invalid_state.read(cx).validity().is_invalid
})),
server_errors_of: Some(Arc::new(move |cx: &App| {
routed_read_state.read(cx).routed_errors().to_vec()
})),
set_server_errors: Some(Arc::new(
move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
if routed_set_state.read(cx).routed_revision() == revision {
return;
}
routed_set_state.update(cx, |s, cx| {
s.set_routed(messages, revision);
cx.notify();
});
},
)),
clear_server_errors: Some(Arc::new(move |cx: &mut App| {
if !routed_clear_state.read(cx).routed_errors().is_empty() {
routed_clear_state.update(cx, |s, cx| {
s.clear_routed_errors();
cx.notify();
});
}
})),
focus: Some(Arc::new(move |window, cx| {
let fh = focus_state.read(cx).focus_handle.clone();
window.focus(&fh, cx);
})),
submits_on_enter_of: Some(Arc::new(move |window, cx| {
enter_state.read(cx).focus_handle.is_focused(window)
})),
read_only_of: None,
state_id: Some(state_id),
empty_keys_submits_empty: false,
}
}
pub fn text_value(name: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
let value = value.into();
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |_| FormValue::Text(value.clone())),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: None,
invalid_of: None,
server_errors_of: None,
set_server_errors: None,
clear_server_errors: None,
focus: None,
read_only_of: None,
submits_on_enter_of: None,
state_id: None,
empty_keys_submits_empty: false,
}
}
pub(crate) fn live(
name: impl Into<SharedString>,
state: Rc<RefCell<LiveFormFieldState>>,
) -> Self {
let read_state = state.clone();
let invalid_state = state.clone();
let successful_state = state.clone();
let restore_state = state.clone();
let focus_state = state;
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |_| read_state.borrow().value.clone()),
restore: Some(Arc::new(move |window, cx| {
let restore = restore_state.borrow().restore.clone();
if let Some(restore) = restore {
restore(window, cx);
}
})),
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: Some(Arc::new(move |_| successful_state.borrow().is_successful)),
invalid_of: Some(Arc::new(move |_| invalid_state.borrow().is_invalid)),
server_errors_of: None,
set_server_errors: None,
clear_server_errors: None,
focus: Some(Arc::new(move |window, cx| {
let focus = focus_state.borrow().focus.clone();
if let Some(focus) = focus {
window.focus(&focus, cx);
}
})),
read_only_of: None,
submits_on_enter_of: None,
state_id: None,
empty_keys_submits_empty: false,
}
}
pub fn number_value(name: impl Into<SharedString>, value: f64) -> Self {
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |_| FormValue::Number(value)),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: None,
invalid_of: None,
server_errors_of: None,
set_server_errors: None,
clear_server_errors: None,
focus: None,
read_only_of: None,
submits_on_enter_of: None,
state_id: None,
empty_keys_submits_empty: false,
}
}
pub fn flag(name: impl Into<SharedString>, value: bool) -> Self {
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |_| FormValue::Flag(value)),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: None,
invalid_of: None,
server_errors_of: None,
set_server_errors: None,
clear_server_errors: None,
focus: None,
read_only_of: None,
submits_on_enter_of: None,
state_id: None,
empty_keys_submits_empty: false,
}
}
pub fn keys(
name: impl Into<SharedString>,
values: impl IntoIterator<Item = impl Into<SharedString>>,
) -> Self {
let values: Vec<SharedString> = values.into_iter().map(Into::into).collect();
Self {
name: Some(name.into()),
name_of: None,
read: Arc::new(move |_| FormValue::Keys(values.clone())),
restore: None,
is_required: false,
validation_behavior: ValidationBehavior::Native,
behavior_of: None,
successful_of: None,
invalid_of: None,
server_errors_of: None,
set_server_errors: None,
clear_server_errors: None,
focus: None,
read_only_of: None,
submits_on_enter_of: None,
state_id: None,
empty_keys_submits_empty: false,
}
}
pub fn name(mut self, name: impl Into<SharedString>) -> Self {
self.name = Some(name.into());
self
}
pub fn default_text(
mut self,
state: Entity<InputState>,
value: impl Into<SharedString>,
) -> Self {
let value = value.into();
self.restore = Some(Arc::new(move |_, cx: &mut App| {
state.update(cx, |s, cx| {
s.set_value(value.to_string());
cx.notify();
});
}));
self
}
pub fn default_number(mut self, state: Entity<NumberState>, value: f64) -> Self {
self.restore = Some(Arc::new(move |_, cx: &mut App| {
state.update(cx, |s, cx| {
s.set_value(value, cx);
cx.notify();
});
}));
self
}
pub fn is_required(mut self, v: bool) -> Self {
self.is_required = v;
self
}
pub fn validation_behavior(mut self, behavior: ValidationBehavior) -> Self {
self.validation_behavior = behavior;
self
}
pub fn blocks_submission(&self, cx: &App) -> bool {
let behavior = self
.behavior_of
.as_ref()
.map_or(self.validation_behavior, |f| f(cx));
behavior == ValidationBehavior::Native
}
pub fn field_name(&self, cx: &App) -> Option<SharedString> {
self.name
.clone()
.or_else(|| self.name_of.as_ref().and_then(|f| f(cx)))
}
fn submits_on_enter(&self, window: &Window, cx: &App) -> bool {
self.submits_on_enter_of
.as_ref()
.is_some_and(|f| f(window, cx))
}
fn is_read_only(&self, cx: &App) -> bool {
self.read_only_of.as_ref().is_some_and(|f| f(cx))
}
fn is_invalid(&self, cx: &App) -> bool {
self.invalid_of.as_ref().is_some_and(|f| f(cx))
}
fn server_errors(&self, cx: &App) -> Vec<SharedString> {
self.server_errors_of
.as_ref()
.map_or_else(Vec::new, |f| f(cx))
}
fn has_server_errors(&self, cx: &App) -> bool {
!self.server_errors(cx).is_empty()
}
fn deliver_server_errors(&self, record: &ValidationErrors, cx: &mut App) {
let Some(set) = self.set_server_errors.as_ref() else {
return;
};
let Some(name) = self.field_name(cx) else {
return;
};
let messages = record
.get(&name)
.map(<[SharedString]>::to_vec)
.unwrap_or_default();
if messages.is_empty() && self.server_errors(cx).is_empty() {
return;
}
set(cx, messages, record.revision());
}
fn is_successful(&self, cx: &App) -> bool {
self.successful_of.as_ref().is_none_or(|f| f(cx))
}
}
type OnSubmit = Arc<dyn Fn(&FormData, &mut Window, &mut App) + 'static>;
type OnReset = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ValidationBehavior {
#[default]
Native,
Allow,
}
#[derive(IntoElement)]
pub struct Form {
validation_errors: ValidationErrors,
validation_behavior: ValidationBehavior,
fields: Vec<FormField>,
on_submit: Option<OnSubmit>,
on_reset: Option<OnReset>,
on_invalid: Option<OnSubmit>,
children: Vec<AnyElement>,
}
impl Form {
pub fn new() -> Self {
Self {
validation_errors: ValidationErrors::new(),
validation_behavior: ValidationBehavior::default(),
fields: Vec::new(),
on_submit: None,
on_reset: None,
on_invalid: None,
children: Vec::new(),
}
}
pub fn validation_errors(mut self, errors: ValidationErrors) -> Self {
self.validation_errors = errors;
self
}
pub fn validation_behavior(mut self, behavior: ValidationBehavior) -> Self {
self.validation_behavior = behavior;
self
}
pub fn field(mut self, field: FormField) -> Self {
self.fields.push(field);
self
}
pub fn on_submit(mut self, f: impl Fn(&FormData, &mut Window, &mut App) + 'static) -> Self {
self.on_submit = Some(Arc::new(f));
self
}
pub fn on_reset(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_reset = Some(Arc::new(f));
self
}
pub fn on_invalid(mut self, f: impl Fn(&FormData, &mut Window, &mut App) + 'static) -> Self {
self.on_invalid = Some(Arc::new(f));
self
}
pub fn data(&self, cx: &App) -> FormData {
Self::collect_data(&self.fields, cx)
}
fn collect_data(fields: &[FormField], cx: &App) -> FormData {
let mut entries = Vec::with_capacity(fields.len());
for field in fields {
if !field.is_successful(cx) {
continue;
}
if let Some(name) = field.field_name(cx) {
let value = (field.read)(cx);
let value = match (&value, field.empty_keys_submits_empty) {
(FormValue::Keys(keys), true) if keys.is_empty() => {
FormValue::Text(SharedString::from(""))
}
_ => value,
};
let omitted = match &value {
FormValue::Flag(false) => true,
FormValue::Keys(keys) => keys.is_empty(),
_ => false,
};
if !omitted {
entries.push((name, value));
}
}
}
FormData { entries }
}
fn missing_required_fields(fields: &[FormField], cx: &App) -> Vec<SharedString> {
fields
.iter()
.filter(|f| {
f.is_successful(cx)
&& f.is_required
&& !f.is_read_only(cx)
&& f.blocks_submission(cx)
&& (f.read)(cx).is_empty()
})
.filter_map(|f| f.field_name(cx))
.collect()
}
fn run_submission(
fields: &[FormField],
behavior: ValidationBehavior,
on_submit: Option<&OnSubmit>,
on_invalid: Option<&OnSubmit>,
defer_focus: Option<&Entity<bool>>,
window: &mut Window,
cx: &mut App,
) {
let data = Self::collect_data(fields, cx);
let missing = Self::missing_required_fields(fields, cx);
let own_invalid: Vec<SharedString> = fields
.iter()
.filter(|f| {
f.is_successful(cx)
&& f.blocks_submission(cx)
&& !f.is_read_only(cx)
&& (f.is_invalid(cx) || f.has_server_errors(cx))
})
.filter_map(|f| f.field_name(cx))
.collect();
let blocked = behavior == ValidationBehavior::Native
&& (!missing.is_empty() || !own_invalid.is_empty());
if blocked {
let focus = Self::first_invalid_focus(fields, cx);
match (defer_focus, focus) {
(Some(latch), Some(_)) => latch.update(cx, |pending, _| *pending = true),
(_, focus) => {
if let Some(focus) = focus {
focus(window, cx);
}
}
}
if let Some(f) = on_invalid {
f(&data, window, cx);
}
} else if let Some(f) = on_submit {
f(&data, window, cx);
}
}
fn first_invalid_focus(fields: &[FormField], cx: &App) -> Option<FocusField> {
fields
.iter()
.find(|field| {
field.field_name(cx).is_some()
&& !field.is_read_only(cx)
&& field.is_successful(cx)
&& field.blocks_submission(cx)
&& (field.is_invalid(cx)
|| field.has_server_errors(cx)
|| (field.is_required && (field.read)(cx).is_empty()))
})
.and_then(|field| field.focus.clone())
}
#[allow(clippy::arc_with_non_send_sync)] pub fn submit_handler(&self) -> Arc<dyn Fn(&mut Window, &mut App) + 'static> {
let fields = self.fields.clone();
let on_submit = self.on_submit.clone();
let on_invalid = self.on_invalid.clone();
let behavior = self.validation_behavior;
Arc::new(move |window: &mut Window, cx: &mut App| {
Self::run_submission(
&fields,
behavior,
on_submit.as_ref(),
on_invalid.as_ref(),
None,
window,
cx,
);
})
}
#[allow(clippy::arc_with_non_send_sync)] pub fn reset_handler(&self) -> Arc<dyn Fn(&mut Window, &mut App) + 'static> {
let restores: Vec<Restore> = self
.fields
.iter()
.filter_map(|f| f.restore.clone())
.collect();
let clear_server_errors: Vec<ClearServerErrors> = self
.fields
.iter()
.filter_map(|f| f.clear_server_errors.clone())
.collect();
let on_reset = self.on_reset.clone();
Arc::new(move |window: &mut Window, cx: &mut App| {
for restore in &restores {
restore(window, cx);
}
for clear in &clear_server_errors {
clear(cx);
}
if let Some(f) = &on_reset {
f(window, cx);
}
})
}
}
impl Default for Form {
fn default() -> Self {
Self::new()
}
}
impl ParentElement for Form {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for Form {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let Self {
validation_errors: record,
validation_behavior: behavior,
fields,
on_submit,
on_invalid,
children,
..
} = self;
let mut el = gpui::div().flex().flex_col().gap(px(16.)).w_full();
let latch = fields.iter().find_map(|f| f.state_id).map(|id| {
window.use_keyed_state(
gpui::ElementId::named_usize("form-enter-latch", id.as_u64() as usize),
cx,
|_, _| false,
)
});
let down_fields = fields.clone();
let down_latch = latch.clone();
el = el.on_key_down(move |ev: &KeyDownEvent, window, cx| {
if let Some(latch) = &down_latch {
latch.update(cx, |pending, _| *pending = false);
}
let mods = &ev.keystroke.modifiers;
if ev.keystroke.key.as_str() == "enter"
&& !(mods.control || mods.alt || mods.platform)
&& down_fields.iter().any(|f| f.submits_on_enter(window, cx))
{
Self::run_submission(
&down_fields,
behavior,
on_submit.as_ref(),
on_invalid.as_ref(),
down_latch.as_ref(),
window,
cx,
);
}
});
let up_fields = fields.clone();
let up_latch = latch;
el = el.capture_key_up(move |ev: &KeyUpEvent, window, cx| {
let Some(latch) = &up_latch else {
return;
};
let mods = &ev.keystroke.modifiers;
if ev.keystroke.key.as_str() != "enter"
|| mods.control
|| mods.alt
|| mods.platform
|| !*latch.read(cx)
{
return;
}
latch.update(cx, |pending, _| *pending = false);
window.prevent_default();
if let Some(focus) = Self::first_invalid_focus(&up_fields, cx) {
window.defer(cx, move |window, cx| focus(window, cx));
}
});
el = el.children(children);
if fields.iter().any(|f| f.set_server_errors.is_some()) {
let delivery_fields = fields;
el = el.child(
gpui::canvas(
move |_, _, cx| {
for field in &delivery_fields {
field.deliver_server_errors(&record, cx);
}
},
|_, _, _, _| {},
)
.size_0(),
);
}
el
}
}
#[cfg(test)]
mod tests {
use super::*;
fn data(entries: &[(&str, FormValue)]) -> FormData {
FormData {
entries: entries
.iter()
.map(|(n, v)| (SharedString::from(n.to_string()), v.clone()))
.collect(),
}
}
#[test]
fn a_submission_reads_by_name() {
let d = data(&[
("email", FormValue::Text("a@b.c".into())),
("age", FormValue::Number(41.0)),
]);
assert_eq!(d.text("email").unwrap(), SharedString::from("a@b.c"));
assert_eq!(d.text("age").unwrap(), SharedString::from("41"));
assert!(d.get("missing").is_none());
assert_eq!(d.len(), 2);
}
#[test]
fn flags_and_keys_serialise_the_html_way() {
assert_eq!(FormValue::Flag(true).as_text(), SharedString::from("on"));
assert_eq!(FormValue::Flag(false).as_text(), SharedString::from(""));
let keys = FormValue::Keys(vec!["a".into(), "b".into()]);
assert_eq!(keys.as_text(), SharedString::from("a,b"));
}
#[test]
fn emptiness_follows_the_control() {
assert!(FormValue::Text(" ".into()).is_empty());
assert!(!FormValue::Text("x".into()).is_empty());
assert!(FormValue::Flag(false).is_empty());
assert!(!FormValue::Flag(true).is_empty());
assert!(FormValue::Keys(vec![]).is_empty());
assert!(!FormValue::Number(0.0).is_empty());
}
#[test]
fn missing_required_reports_absent_and_blank_alike() {
let d = data(&[
("name", FormValue::Text("".into())),
("tos", FormValue::Flag(true)),
]);
let required: Vec<SharedString> =
vec!["name".into(), "tos".into(), "never-registered".into()];
assert_eq!(
d.missing_required(&required),
vec![
SharedString::from("name"),
SharedString::from("never-registered")
]
);
}
}