Skip to main content

herogpui_components/
form.rs

1//! Form — port of `@heroui/form` (v3).
2//!
3//! v3's `Form` is a `<form>`: `name` on each field is what makes a submission
4//! carry that field's value, and `onSubmit` receives the collected `FormData`.
5//! There is no DOM here, and gpui gives a child no way to find its ancestor, so
6//! the form is *told* which fields it owns — `Form::field(..)` — instead of
7//! discovering them. Everything downstream of that is the same: names, a
8//! collected submission, reset, and an invalid path that runs instead of submit.
9//!
10//! Submission has two doors that share one implementation
11//! (`Form::run_submission`): the caller-wired submit button
12//! (`Form::submit_handler`, standing in for `<button type="submit">`) and the
13//! native form's implicit submission — Enter pressed in a focused field that
14//! semantically participates, which the form root's key handler answers. Only
15//! the fields that participate carry the Enter reader; a TextArea's Enter is a
16//! newline, and a focused submit button submits through its own click.
17//!
18//! The second door is a GPUI substitute for the browser's implicit submission,
19//! not a port of it. A browser picks the *default submitter* — the first
20//! submit button in tree order — and skips implicit submission entirely when a
21//! form has no submit button and more than one field blocking validation; both
22//! rules read the form's children, which are opaque elements here. A browser
23//! also lets a field's own keydown handler cancel the keystroke; the controls
24//! that own Enter here stop propagation to the same effect. So Enter in a
25//! participating field always runs this form's one submission, whoever else
26//! could have been the submitter.
27//!
28//! A blocked Enter-origin submission defers the focus move to the key's
29//! release. gpui activates whichever element the frame drawn for the release
30//! shows as focused, so focusing a Select trigger or a Switch mid-keystroke
31//! would let the release click it — opening or toggling the very control the
32//! repair was meant to reach. The form latches the blocked keystroke in keyed
33//! state, disarms the release (`prevent_default` gates gpui's activation
34//! listener), and moves the focus once the dispatch is over.
35//!
36//! Read-only controls are the third gate, after disabled and inert: they stay
37//! successful (they submit their value) and focusable, but constraint
38//! validation bars them — neither a missing value nor a stored error on a
39//! read-only field can block a submission, as a native form has it.
40//!
41//! `validationErrors` is HeroUI v3's `ValidationErrors` record —
42//! `Record<string, string | string[]>` — a *name-keyed* mapping of server
43//! errors, not a form-level message stack. The form routes it: every named
44//! field's own state receives its messages and displays them in its own
45//! error slot, exactly as React Aria routes server errors into the field a
46//! `<FieldError />` composes. The upstream row is "displayed immediately and
47//! cleared when user modifies the field", and both halves live with the
48//! field: an edit suppresses only that field's messages, reset hides them
49//! all, and each field keeps its own delivery receipt — the record revision
50//! stored beside its messages (see `SetServerErrors`) — so a clone of a
51//! delivered record re-arms nothing, wherever the field sits or however the
52//! registrations churn, while a freshly built record — same content or not —
53//! re-arms every named field it mentions. Blocking follows the same
54//! routing: a native submit is blocked by a field whose routed messages are
55//! present (unless the field is disabled or read-only, which display without
56//! blocking), `validationBehavior: "aria"` displays without blocking, and a
57//! name nothing displays is a name that never blocks — a control with no
58//! error-display path (the live-registered selects, switches, checkboxes and
59//! pickers) receives no routed messages at all, so a record can never block a
60//! submission invisibly.
61//!
62//! What is deliberately absent is the HTTP half — `action`, `method`, `encType`
63//! and `target`. There is no browser to navigate.
64
65use std::{cell::RefCell, rc::Rc, sync::Arc};
66
67use gpui::{
68    px, AnyElement, App, Entity, FocusHandle, InteractiveElement, IntoElement, KeyDownEvent,
69    KeyUpEvent, ParentElement, RenderOnce, SharedString, Styled, Window,
70};
71
72use crate::{input::InputState, number_field::NumberState, validation::ValidationErrors};
73
74/// One field's value in a submission.
75#[derive(Clone, Debug, PartialEq)]
76pub enum FormValue {
77    /// A text value.
78    Text(SharedString),
79    /// A numeric value.
80    Number(f64),
81    /// A boolean flag.
82    Flag(bool),
83    /// A multi-selection: `CheckboxGroup`, a multiple `Select`, `TagGroup`.
84    Keys(Vec<SharedString>),
85}
86
87impl FormValue {
88    /// The value as a string, the way an HTML form would send it.
89    pub fn as_text(&self) -> SharedString {
90        match self {
91            FormValue::Text(t) => t.clone(),
92            FormValue::Number(n) => SharedString::from(n.to_string()),
93            // An unchecked box sends nothing; "on" is the HTML default value.
94            FormValue::Flag(true) => SharedString::from("on"),
95            FormValue::Flag(false) => SharedString::from(""),
96            FormValue::Keys(k) => SharedString::from(
97                k.iter()
98                    .map(ToString::to_string)
99                    .collect::<Vec<_>>()
100                    .join(","),
101            ),
102        }
103    }
104
105    /// Whether an HTML form would treat this as filled in — what `isRequired`
106    /// checks against.
107    pub fn is_empty(&self) -> bool {
108        match self {
109            FormValue::Text(t) => t.trim().is_empty(),
110            FormValue::Number(_) => false,
111            FormValue::Flag(v) => !v,
112            FormValue::Keys(k) => k.is_empty(),
113        }
114    }
115}
116
117/// A submission: every named field the form was given, in registration order.
118#[derive(Clone, Debug, Default, PartialEq)]
119pub struct FormData {
120    entries: Vec<(SharedString, FormValue)>,
121}
122
123impl FormData {
124    /// The value of the field with the given name, if present.
125    pub fn get(&self, name: &str) -> Option<&FormValue> {
126        self.entries.iter().find(|(n, _)| n == name).map(|(_, v)| v)
127    }
128
129    /// The named field as text, which is how a form field usually reads.
130    pub fn text(&self, name: &str) -> Option<SharedString> {
131        self.get(name).map(FormValue::as_text)
132    }
133
134    /// All values submitted under `name`, matching the browser `FormData`
135    /// `getAll` operation. Multi-selection fields contribute one value per
136    /// selected key.
137    pub fn get_all(&self, name: &str) -> Vec<SharedString> {
138        self.entries
139            .iter()
140            .filter(|(entry_name, _)| entry_name == name)
141            .flat_map(|(_, value)| match value {
142                FormValue::Keys(keys) => keys.clone(),
143                value => vec![value.as_text()],
144            })
145            .collect()
146    }
147
148    /// Iterates over the field names and values.
149    pub fn iter(&self) -> impl Iterator<Item = (&SharedString, &FormValue)> {
150        self.entries.iter().map(|(n, v)| (n, v))
151    }
152
153    /// The number of entries.
154    pub fn len(&self) -> usize {
155        self.entries.len()
156    }
157
158    /// Whether there are no entries.
159    pub fn is_empty(&self) -> bool {
160        self.entries.is_empty()
161    }
162
163    /// The names whose value is missing but required.
164    pub fn missing_required(&self, required: &[SharedString]) -> Vec<SharedString> {
165        required
166            .iter()
167            .filter(|name| self.get(name).is_none_or(FormValue::is_empty))
168            .cloned()
169            .collect()
170    }
171}
172
173type Read = Arc<dyn Fn(&App) -> FormValue + 'static>;
174type Restore = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
175type ReadName = Arc<dyn Fn(&App) -> Option<SharedString> + 'static>;
176type ReadBehavior = Arc<dyn Fn(&App) -> ValidationBehavior + 'static>;
177type ReadSuccessful = Arc<dyn Fn(&App) -> bool + 'static>;
178/// Reads the field's stored validity — whether its own validation is in error,
179/// which blocks a native submission like a missing required value.
180type ReadInvalid = Arc<dyn Fn(&App) -> bool + 'static>;
181/// Reads the server messages currently routed to this field by the form's
182/// `validationErrors` record — present from the moment the record arrives,
183/// empty once the user edited the field or reset cleared them.
184type ReadServerErrors = Arc<dyn Fn(&App) -> Vec<SharedString> + 'static>;
185/// Writes the routed server messages *with the revision of the record they
186/// came from*, in one update. Idempotent per field: the revision is stored
187/// beside the messages as the field's own delivery receipt, so re-running
188/// with a record the field has already received — a clone re-rendered on a
189/// later frame, or after this field moved or was replaced within the
190/// registration — is a no-op, and only a genuinely new revision re-arms.
191/// Present only on fields with an error-display path of their own: a control
192/// that cannot show a routed message must never be blocked by one.
193type SetServerErrors = Arc<dyn Fn(&mut App, Vec<SharedString>, u64) + 'static>;
194/// Clears the routed server messages *without rewinding the delivery
195/// receipt* — the reset half. The record that delivered already named the
196/// field, so its clones must stay hidden; only a genuinely new revision
197/// re-arms.
198type ClearServerErrors = Arc<dyn Fn(&mut App) + 'static>;
199/// Moves the focus to this field, which a blocked submit uses for v3's
200/// "the first invalid field will be focused".
201type FocusField = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
202/// Whether a focused press of Enter submits the form from this field — the
203/// reader half of a native form's implicit submission. Only the controls a
204/// browser submits a form from carry one: the single-line text family (a
205/// `<input type=number>` among them) and, as pinned v3 builds it, the OTP
206/// row's single text input. A multi-line field and every non-text compound
207/// control (switch, select, checkbox group) never do.
208type SubmitsOnEnter = Arc<dyn Fn(&Window, &App) -> bool + 'static>;
209/// Whether the rendered control is read-only. A read-only field stays
210/// successful and focusable, but HTML constraint validation bars it: neither
211/// required emptiness nor stored invalidity may block a submission. Read from
212/// the mirror the component's render writes, so the answer is the rendered
213/// state and not a stale builder flag.
214type ReadReadOnly = Arc<dyn Fn(&App) -> bool + 'static>;
215
216/// Value, validity and focus owned by a rendered control without an entity.
217pub(crate) struct LiveFormFieldState {
218    pub(crate) value: FormValue,
219    pub(crate) is_invalid: bool,
220    pub(crate) is_successful: bool,
221    pub(crate) focus: Option<FocusHandle>,
222    pub(crate) restore: Option<Restore>,
223}
224
225/// A named field a [`Form`] reads on submit.
226///
227/// The `name` may come from the field itself: a component whose value lives in
228/// an entity (`Input`, `TextArea`, `NumberField`) writes its `name` prop into
229/// that entity, so [`FormField::text`] and [`FormField::number`] can pick it up
230/// and the call site does not repeat it.
231#[must_use = "builder methods return a new value; pass the field to its form"]
232#[derive(Clone)]
233pub struct FormField {
234    name: Option<SharedString>,
235    /// Reads the `name` prop out of the field's own state entity.
236    name_of: Option<ReadName>,
237    read: Read,
238    restore: Option<Restore>,
239    is_required: bool,
240    /// `validationBehavior` on the field: `Allow` shows its message without
241    /// blocking submission.
242    validation_behavior: ValidationBehavior,
243    /// Reads `validationBehavior` off the field's own state entity.
244    behavior_of: Option<ReadBehavior>,
245    /// Whether this field is a successful native form control. Disabled
246    /// checkbox inputs are neither submitted nor validated.
247    successful_of: Option<ReadSuccessful>,
248    /// Reads the field's stored validity — whether the field considers itself
249    /// in error, written by `Input::render`.
250    invalid_of: Option<ReadInvalid>,
251    /// Reads the server messages routed to this field by the form's
252    /// `validationErrors` record — what a native submit consults beside the
253    /// stored validity.
254    server_errors_of: Option<ReadServerErrors>,
255    /// Writes (or clears) the routed server messages. `None` on every field
256    /// without an error-display path, so a routed name can neither display
257    /// nor block there.
258    set_server_errors: Option<SetServerErrors>,
259    /// Clears the routed server messages for reset, leaving the field's
260    /// delivery receipt alone. `None` wherever [`Self::set_server_errors`]
261    /// is: a field that cannot display routed messages has nothing to hide.
262    clear_server_errors: Option<ClearServerErrors>,
263    /// Focuses the field — the invalid-path step, when it has a reachable
264    /// handle.
265    focus: Option<FocusField>,
266    /// Whether a focused Enter submits from this field. `None` on every
267    /// non-text or compound field, which never submits implicitly.
268    submits_on_enter_of: Option<SubmitsOnEnter>,
269    /// Whether the rendered control is read-only — still successful and
270    /// focusable, but barred from constraint validation.
271    read_only_of: Option<ReadReadOnly>,
272    /// The state entity carrying this field's value, when one exists. That
273    /// entity outlives the frames, so it is the stable identity the form's
274    /// blocked-keystroke latch is keyed by — the same trick
275    /// `Input`'s `defaultValue` seed uses.
276    state_id: Option<gpui::EntityId>,
277    /// Whether a keyed selection with nothing chosen still submits one empty
278    /// value. Pinned React Aria Components 1.20.0's key-mode serialization
279    /// maps an empty `selectedKeys` to one hidden input with value `""`, so
280    /// the name appears in FormData either way; group-backed fields (a
281    /// checkbox group, a multiple `Select`) omit themselves instead, like the
282    /// HTML controls they stand in for.
283    empty_keys_submits_empty: bool,
284}
285
286impl FormField {
287    /// A single-line text field, read from its [`InputState`].
288    ///
289    /// This is the registration for an [`Input`](crate::Input) or
290    /// [`SearchField`](crate::SearchField) — the controls a native form
291    /// implicitly submits from, so the field carries the Enter reader. A
292    /// multi-line field rendered from the same state must register with
293    /// [`FormField::text_area`].
294    pub fn text(state: Entity<InputState>) -> Self {
295        let read_state = state.clone();
296        let name_state = state.clone();
297        let behavior_state = state.clone();
298        let successful_state = state.clone();
299        let invalid_state = state.clone();
300        let enter_state = state.clone();
301        let read_only_state = state.clone();
302        let routed_read_state = state.clone();
303        let routed_set_state = state.clone();
304        let routed_clear_state = state.clone();
305        let state_id = state.entity_id();
306        let focus_state = state;
307        Self {
308            name: None,
309            name_of: Some(Arc::new(move |cx: &App| name_state.read(cx).name())),
310            behavior_of: Some(Arc::new(move |cx: &App| {
311                behavior_state.read(cx).validation_behavior()
312            })),
313            successful_of: Some(Arc::new(move |cx: &App| {
314                successful_state.read(cx).is_successful()
315            })),
316            invalid_of: Some(Arc::new(move |cx: &App| {
317                invalid_state.read(cx).validity().is_invalid
318            })),
319            server_errors_of: Some(Arc::new(move |cx: &App| {
320                routed_read_state.read(cx).routed_errors().to_vec()
321            })),
322            set_server_errors: Some(Arc::new(
323                move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
324                    // The receipt is the field's own: a revision this state
325                    // has already delivered is a clone of a delivered record,
326                    // and re-delivering it — however many frames re-render it
327                    // or wherever this field now sits in the registration —
328                    // would resurrect a message the user edited away. Receipt
329                    // and messages move in one update, so no frame can see
330                    // one without the other.
331                    if routed_set_state.read(cx).routed_revision() == revision {
332                        return;
333                    }
334                    routed_set_state.update(cx, |s, cx| {
335                        s.set_routed(messages, revision);
336                        cx.notify();
337                    });
338                },
339            )),
340            clear_server_errors: Some(Arc::new(move |cx: &mut App| {
341                // Reset hides the messages but never rewinds the receipt.
342                if !routed_clear_state.read(cx).routed_errors().is_empty() {
343                    routed_clear_state.update(cx, |s, cx| {
344                        s.clear_routed_errors();
345                        cx.notify();
346                    });
347                }
348            })),
349            read: Arc::new(move |cx: &App| {
350                FormValue::Text(SharedString::from(read_state.read(cx).value().to_owned()))
351            }),
352            restore: None,
353            is_required: false,
354            validation_behavior: ValidationBehavior::Native,
355            focus: Some(Arc::new(move |window, cx| {
356                let fh = focus_state.read(cx).focus_handle.clone();
357                window.focus(&fh, cx);
358            })),
359            submits_on_enter_of: Some(Arc::new(move |window, cx| {
360                enter_state.read(cx).focus_handle.is_focused(window)
361            })),
362            read_only_of: Some(Arc::new(move |cx: &App| {
363                read_only_state.read(cx).is_read_only()
364            })),
365            state_id: Some(state_id),
366            empty_keys_submits_empty: false,
367        }
368    }
369
370    /// A multi-line text field, read from its [`InputState`] — the state a
371    /// [`TextArea`](crate::TextArea) renders.
372    ///
373    /// Identical to [`FormField::text`] except that Enter never submits the
374    /// form: a native form does not implicitly submit from a `<textarea>`,
375    /// whose Enter is a newline. The multiline flag lives on the
376    /// [`TextArea`](crate::TextArea) builder, not on the shared state, so
377    /// the registration is where the control kind is named.
378    pub fn text_area(state: Entity<InputState>) -> Self {
379        let field = Self::text(state);
380        Self {
381            submits_on_enter_of: None,
382            ..field
383        }
384    }
385
386    /// A live field whose rendered control is a single-line text input.
387    ///
388    /// The keyed control's own value, restore and focus live in the shared
389    /// [`LiveFormFieldState`] it syncs each frame. Everything else — the
390    /// implicit-Enter reader, the read-only mirror, the resolved validity
391    /// (`validate` included), `validationBehavior`, the routed server errors
392    /// and the latch identity — stays on the [`InputState`] the control
393    /// renders, because that state's mirrors are what the control's own
394    /// render writes; duplicating the readers onto the shared state would
395    /// give the form two answers that can drift apart.
396    pub(crate) fn live_text(
397        name: impl Into<SharedString>,
398        state: Rc<RefCell<LiveFormFieldState>>,
399        input: Entity<InputState>,
400    ) -> Self {
401        let live = Self::live(name, state);
402        let text = Self::text(input);
403        Self {
404            name: live.name,
405            read: live.read,
406            restore: live.restore,
407            focus: live.focus,
408            empty_keys_submits_empty: true,
409            ..text
410        }
411    }
412
413    /// A numeric field, read from its [`NumberState`].
414    ///
415    /// The field's validity is the one `NumberField::render` resolved —
416    /// controlled flags, then server errors, then `validate` — mirrored onto
417    /// the inner [`InputState`] exactly as `name` is, so the submission reads
418    /// the rendered state rather than a builder snapshot. Read-only travels
419    /// the same way: `NumberField` forwards the flag to the inner input,
420    /// whose render mirrors it here.
421    pub fn number(state: Entity<NumberState>) -> Self {
422        let read_state = state.clone();
423        let name_state = state.clone();
424        let behavior_state = state.clone();
425        let successful_state = state.clone();
426        let invalid_state = state.clone();
427        let enter_state = state.clone();
428        let read_only_state = state.clone();
429        let routed_read_state = state.clone();
430        let routed_set_state = state.clone();
431        let routed_clear_state = state.clone();
432        let state_id = state.entity_id();
433        let focus_state = state;
434        Self {
435            name: None,
436            name_of: Some(Arc::new(move |cx: &App| {
437                let st = name_state.read(cx);
438                st.input.read(cx).name()
439            })),
440            behavior_of: Some(Arc::new(move |cx: &App| {
441                behavior_state.read(cx).input.read(cx).validation_behavior()
442            })),
443            successful_of: Some(Arc::new(move |cx: &App| {
444                successful_state.read(cx).input.read(cx).is_successful()
445            })),
446            invalid_of: Some(Arc::new(move |cx: &App| {
447                invalid_state.read(cx).input.read(cx).validity().is_invalid
448            })),
449            // The routed channel lives on the inner `InputState`, the same
450            // state that displays the messages — and the same state that
451            // carries the delivery receipt.
452            server_errors_of: Some(Arc::new(move |cx: &App| {
453                routed_read_state
454                    .read(cx)
455                    .input
456                    .read(cx)
457                    .routed_errors()
458                    .to_vec()
459            })),
460            set_server_errors: Some(Arc::new(
461                move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
462                    let inner = routed_set_state.read(cx).input.clone();
463                    // Same per-field receipt as the text field, one level
464                    // down: a revision the inner state has already delivered
465                    // is a clone, never a re-arm.
466                    if inner.read(cx).routed_revision() == revision {
467                        return;
468                    }
469                    inner.update(cx, |s, cx| {
470                        s.set_routed(messages, revision);
471                        cx.notify();
472                    });
473                },
474            )),
475            clear_server_errors: Some(Arc::new(move |cx: &mut App| {
476                let inner = routed_clear_state.read(cx).input.clone();
477                if !inner.read(cx).routed_errors().is_empty() {
478                    inner.update(cx, |s, cx| {
479                        s.clear_routed_errors();
480                        cx.notify();
481                    });
482                }
483            })),
484            read: Arc::new(move |cx: &App| FormValue::Number(read_state.read(cx).value())),
485            restore: None,
486            is_required: false,
487            validation_behavior: ValidationBehavior::Native,
488            focus: Some(Arc::new(move |window, cx| {
489                let fh = focus_state.read(cx).input.read(cx).focus_handle.clone();
490                window.focus(&fh, cx);
491            })),
492            // `<input type=number>` is a single-line text control: a native
493            // form submits from it.
494            submits_on_enter_of: Some(Arc::new(move |window, cx| {
495                enter_state
496                    .read(cx)
497                    .input
498                    .read(cx)
499                    .focus_handle
500                    .is_focused(window)
501            })),
502            read_only_of: Some(Arc::new(move |cx: &App| {
503                read_only_state.read(cx).input.read(cx).is_read_only()
504            })),
505            state_id: Some(state_id),
506            empty_keys_submits_empty: false,
507        }
508    }
509
510    /// An OTP field, read from its [`crate::input_otp::OtpState`].
511    ///
512    /// Pinned v3 builds `InputOTP` on a single text input, and the row's
513    /// cells share one focus handle, so a focused Enter here is the same
514    /// implicit submission it is in any single-line field. The OTP answers
515    /// no Enter of its own — its handler fills cells and walks the caret —
516    /// so the keystroke bubbles to the form.
517    pub fn code(name: impl Into<SharedString>, state: Entity<crate::input_otp::OtpState>) -> Self {
518        let read_state = state.clone();
519        let successful_state = state.clone();
520        let invalid_state = state.clone();
521        let enter_state = state.clone();
522        let routed_read_state = state.clone();
523        let routed_set_state = state.clone();
524        let routed_clear_state = state.clone();
525        let state_id = state.entity_id();
526        let focus_state = state;
527        Self {
528            name: Some(name.into()),
529            name_of: None,
530            read: Arc::new(move |cx: &App| {
531                FormValue::Text(SharedString::from(read_state.read(cx).code()))
532            }),
533            restore: None,
534            is_required: false,
535            validation_behavior: ValidationBehavior::Native,
536            behavior_of: None,
537            successful_of: Some(Arc::new(move |cx: &App| {
538                successful_state.read(cx).is_successful()
539            })),
540            invalid_of: Some(Arc::new(move |cx: &App| {
541                invalid_state.read(cx).validity().is_invalid
542            })),
543            server_errors_of: Some(Arc::new(move |cx: &App| {
544                routed_read_state.read(cx).routed_errors().to_vec()
545            })),
546            set_server_errors: Some(Arc::new(
547                move |cx: &mut App, messages: Vec<SharedString>, revision: u64| {
548                    // Same per-field receipt as the text field.
549                    if routed_set_state.read(cx).routed_revision() == revision {
550                        return;
551                    }
552                    routed_set_state.update(cx, |s, cx| {
553                        s.set_routed(messages, revision);
554                        cx.notify();
555                    });
556                },
557            )),
558            clear_server_errors: Some(Arc::new(move |cx: &mut App| {
559                if !routed_clear_state.read(cx).routed_errors().is_empty() {
560                    routed_clear_state.update(cx, |s, cx| {
561                        s.clear_routed_errors();
562                        cx.notify();
563                    });
564                }
565            })),
566            focus: Some(Arc::new(move |window, cx| {
567                let fh = focus_state.read(cx).focus_handle.clone();
568                window.focus(&fh, cx);
569            })),
570            // The whole row is one text input to the form: Enter while any
571            // cell holds the focus submits, exactly as it does from a
572            // single-line field.
573            submits_on_enter_of: Some(Arc::new(move |window, cx| {
574                enter_state.read(cx).focus_handle.is_focused(window)
575            })),
576            // The OTP row has no read-only prop, so there is nothing to bar.
577            read_only_of: None,
578            state_id: Some(state_id),
579            empty_keys_submits_empty: false,
580        }
581    }
582
583    /// A plain text value the caller holds — a formatted date, a colour hex,
584    /// an OTP code.
585    pub fn text_value(name: impl Into<SharedString>, value: impl Into<SharedString>) -> Self {
586        let value = value.into();
587        Self {
588            name: Some(name.into()),
589            name_of: None,
590            read: Arc::new(move |_| FormValue::Text(value.clone())),
591            restore: None,
592            is_required: false,
593            validation_behavior: ValidationBehavior::Native,
594            behavior_of: None,
595            successful_of: None,
596            invalid_of: None,
597            // A caller-held value has no state to route messages into — and
598            // nothing to display them with.
599            server_errors_of: None,
600            set_server_errors: None,
601            clear_server_errors: None,
602            focus: None,
603            // A caller-held value has no rendered control to be read-only.
604            read_only_of: None,
605            submits_on_enter_of: None,
606            state_id: None,
607            empty_keys_submits_empty: false,
608        }
609    }
610
611    /// A live field owned by a single-threaded rendered control.
612    pub(crate) fn live(
613        name: impl Into<SharedString>,
614        state: Rc<RefCell<LiveFormFieldState>>,
615    ) -> Self {
616        let read_state = state.clone();
617        let invalid_state = state.clone();
618        let successful_state = state.clone();
619        let restore_state = state.clone();
620        let focus_state = state;
621        Self {
622            name: Some(name.into()),
623            name_of: None,
624            read: Arc::new(move |_| read_state.borrow().value.clone()),
625            restore: Some(Arc::new(move |window, cx| {
626                let restore = restore_state.borrow().restore.clone();
627                if let Some(restore) = restore {
628                    restore(window, cx);
629                }
630            })),
631            is_required: false,
632            validation_behavior: ValidationBehavior::Native,
633            behavior_of: None,
634            successful_of: Some(Arc::new(move |_| successful_state.borrow().is_successful)),
635            invalid_of: Some(Arc::new(move |_| invalid_state.borrow().is_invalid)),
636            // A live field belongs to a rendered non-text control — a switch,
637            // a select, a checkbox — none of which has an error-display path
638            // the form could route into. No channel: a name landing here
639            // neither displays nor blocks.
640            server_errors_of: None,
641            set_server_errors: None,
642            clear_server_errors: None,
643            focus: Some(Arc::new(move |window, cx| {
644                let focus = focus_state.borrow().focus.clone();
645                if let Some(focus) = focus {
646                    window.focus(&focus, cx);
647                }
648            })),
649            // A live field belongs to a rendered non-text control — a switch,
650            // a select, a checkbox — none of which submits implicitly, and
651            // whose read-only state is not mirrored on this shared state.
652            read_only_of: None,
653            submits_on_enter_of: None,
654            state_id: None,
655            empty_keys_submits_empty: false,
656        }
657    }
658
659    /// A plain number the caller holds — a slider or colour channel.
660    pub fn number_value(name: impl Into<SharedString>, value: f64) -> Self {
661        Self {
662            name: Some(name.into()),
663            name_of: None,
664            read: Arc::new(move |_| FormValue::Number(value)),
665            restore: None,
666            is_required: false,
667            validation_behavior: ValidationBehavior::Native,
668            behavior_of: None,
669            successful_of: None,
670            invalid_of: None,
671            server_errors_of: None,
672            set_server_errors: None,
673            clear_server_errors: None,
674            focus: None,
675            read_only_of: None,
676            submits_on_enter_of: None,
677            state_id: None,
678            empty_keys_submits_empty: false,
679        }
680    }
681
682    /// A checkbox or switch, whose value the caller holds.
683    pub fn flag(name: impl Into<SharedString>, value: bool) -> Self {
684        Self {
685            name: Some(name.into()),
686            name_of: None,
687            read: Arc::new(move |_| FormValue::Flag(value)),
688            restore: None,
689            is_required: false,
690            validation_behavior: ValidationBehavior::Native,
691            behavior_of: None,
692            successful_of: None,
693            invalid_of: None,
694            server_errors_of: None,
695            set_server_errors: None,
696            clear_server_errors: None,
697            focus: None,
698            read_only_of: None,
699            submits_on_enter_of: None,
700            state_id: None,
701            empty_keys_submits_empty: false,
702        }
703    }
704
705    /// A selection, whose value the caller holds.
706    pub fn keys(
707        name: impl Into<SharedString>,
708        values: impl IntoIterator<Item = impl Into<SharedString>>,
709    ) -> Self {
710        let values: Vec<SharedString> = values.into_iter().map(Into::into).collect();
711        Self {
712            name: Some(name.into()),
713            name_of: None,
714            read: Arc::new(move |_| FormValue::Keys(values.clone())),
715            restore: None,
716            is_required: false,
717            validation_behavior: ValidationBehavior::Native,
718            behavior_of: None,
719            successful_of: None,
720            invalid_of: None,
721            server_errors_of: None,
722            set_server_errors: None,
723            clear_server_errors: None,
724            focus: None,
725            read_only_of: None,
726            submits_on_enter_of: None,
727            state_id: None,
728            empty_keys_submits_empty: false,
729        }
730    }
731
732    /// Overrides the name, for a field that carries none of its own.
733    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
734        self.name = Some(name.into());
735        self
736    }
737
738    /// The value a reset restores. Without one, a reset only reports itself.
739    pub fn default_text(
740        mut self,
741        state: Entity<InputState>,
742        value: impl Into<SharedString>,
743    ) -> Self {
744        let value = value.into();
745        self.restore = Some(Arc::new(move |_, cx: &mut App| {
746            state.update(cx, |s, cx| {
747                s.set_value(value.to_string());
748                cx.notify();
749            });
750        }));
751        self
752    }
753
754    /// The number a reset restores.
755    pub fn default_number(mut self, state: Entity<NumberState>, value: f64) -> Self {
756        self.restore = Some(Arc::new(move |_, cx: &mut App| {
757            state.update(cx, |s, cx| {
758                s.set_value(value, cx);
759                cx.notify();
760            });
761        }));
762        self
763    }
764
765    /// Marks the field required, which is what `onInvalid` reports on.
766    pub fn is_required(mut self, v: bool) -> Self {
767        self.is_required = v;
768        self
769    }
770
771    /// `validationBehavior` — `Allow` shows the field's message without
772    /// blocking submission.
773    pub fn validation_behavior(mut self, behavior: ValidationBehavior) -> Self {
774        self.validation_behavior = behavior;
775        self
776    }
777
778    /// Whether this field's invalidity blocks submission.
779    pub fn blocks_submission(&self, cx: &App) -> bool {
780        let behavior = self
781            .behavior_of
782            .as_ref()
783            .map_or(self.validation_behavior, |f| f(cx));
784        behavior == ValidationBehavior::Native
785    }
786
787    /// The name this field submits under: an explicit [`FormField::name`],
788    /// otherwise the `name` prop the component wrote into its own state.
789    pub fn field_name(&self, cx: &App) -> Option<SharedString> {
790        self.name
791            .clone()
792            .or_else(|| self.name_of.as_ref().and_then(|f| f(cx)))
793    }
794
795    /// Whether Enter pressed right now submits the form from this field —
796    /// the field holds the focus and is a single-line text control, the
797    /// only thing a native form implicitly submits from.
798    fn submits_on_enter(&self, window: &Window, cx: &App) -> bool {
799        self.submits_on_enter_of
800            .as_ref()
801            .is_some_and(|f| f(window, cx))
802    }
803
804    /// Whether the rendered control is read-only: still successful and
805    /// focusable, but barred from constraint validation — a native form
806    /// neither blocks on a read-only field's emptiness nor on its stored
807    /// errors, while its value still submits.
808    fn is_read_only(&self, cx: &App) -> bool {
809        self.read_only_of.as_ref().is_some_and(|f| f(cx))
810    }
811
812    /// Whether the field's stored validity is in error — the render-side of
813    /// the validation a native submit consults.
814    fn is_invalid(&self, cx: &App) -> bool {
815        self.invalid_of.as_ref().is_some_and(|f| f(cx))
816    }
817
818    /// The server messages currently routed to this field by the form's
819    /// `validationErrors` record — present from the moment the record
820    /// arrives, gone once the user edited the field or a reset hid them.
821    fn server_errors(&self, cx: &App) -> Vec<SharedString> {
822        self.server_errors_of
823            .as_ref()
824            .map_or_else(Vec::new, |f| f(cx))
825    }
826
827    /// Whether this field carries routed server messages.
828    fn has_server_errors(&self, cx: &App) -> bool {
829        !self.server_errors(cx).is_empty()
830    }
831
832    /// Delivers this field's messages from the record — the routing half of
833    /// `Form::validation_errors`. A name the record does not mention clears
834    /// the field's routed messages; a field with no display channel is never
835    /// written, so a name landing there can never block invisibly. The write
836    /// is receipted on the field's own state (see [`SetServerErrors`]), so
837    /// calling this every frame — or after this field moved or was replaced
838    /// within the registration — delivers nothing until a genuinely new
839    /// record arrives.
840    fn deliver_server_errors(&self, record: &ValidationErrors, cx: &mut App) {
841        let Some(set) = self.set_server_errors.as_ref() else {
842            return;
843        };
844        // An unnamed field — its control has not rendered yet, so its name
845        // reader still answers None — receives nothing and stamps no receipt.
846        // A receipt taken before the name exists would spend this record's
847        // revision on an empty delivery, and the error would never reach the
848        // field once its control later renders and publishes its name.
849        let Some(name) = self.field_name(cx) else {
850            return;
851        };
852        let messages = record
853            .get(&name)
854            .map(<[SharedString]>::to_vec)
855            .unwrap_or_default();
856        // A record with nothing for this field, over a slot already empty, is
857        // an unobservable delivery: a freshly built empty record mints a new
858        // revision every frame, and stamping it would rewrite every field's
859        // receipt — and notify — for nothing. An occupied slot fails this
860        // guard and clears below, so an emptied record still hides what it
861        // used to name; a non-empty entry always reaches the receipted write.
862        if messages.is_empty() && self.server_errors(cx).is_empty() {
863            return;
864        }
865        set(cx, messages, record.revision());
866    }
867
868    fn is_successful(&self, cx: &App) -> bool {
869        self.successful_of.as_ref().is_none_or(|f| f(cx))
870    }
871}
872
873type OnSubmit = Arc<dyn Fn(&FormData, &mut Window, &mut App) + 'static>;
874type OnReset = Arc<dyn Fn(&mut Window, &mut App) + 'static>;
875
876/// How invalid children are handled (`validationBehavior`).
877#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
878pub enum ValidationBehavior {
879    /// A failed field blocks submission and `onInvalid` runs instead.
880    #[default]
881    Native,
882    /// Submission proceeds; the messages are shown but not enforced.
883    Allow,
884}
885
886/// HeroUI Form: a vertical field stack that collects a named submission.
887#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
888#[derive(IntoElement)]
889pub struct Form {
890    /// `validationErrors` — the [`ValidationErrors`] record the form routes
891    /// into its named fields' own error slots.
892    validation_errors: ValidationErrors,
893    validation_behavior: ValidationBehavior,
894    fields: Vec<FormField>,
895    on_submit: Option<OnSubmit>,
896    on_reset: Option<OnReset>,
897    on_invalid: Option<OnSubmit>,
898    children: Vec<AnyElement>,
899}
900
901impl Form {
902    /// Creates an empty form.
903    pub fn new() -> Self {
904        Self {
905            validation_errors: ValidationErrors::new(),
906            validation_behavior: ValidationBehavior::default(),
907            fields: Vec::new(),
908            on_submit: None,
909            on_reset: None,
910            on_invalid: None,
911            children: Vec::new(),
912        }
913    }
914
915    /// `validationErrors` — HeroUI v3's `ValidationErrors` record:
916    /// server-side errors mapped by field name
917    /// (`Record<string, string | string[]>`).
918    ///
919    /// The form routes the record; the fields display it. Each entry lands in
920    /// the named field's own state and error slot — there is no form-level
921    /// message stack. Delivery is receipted per field: each field's own state
922    /// stores the record's [`ValidationErrors::revision`] beside its routed
923    /// messages, so re-rendering with a clone keeps the per-field state (an
924    /// edit stays suppressed, and reordering or replacing the registrations
925    /// re-arms nothing), while any freshly built record re-arms every named
926    /// field it mentions, content-equal or not. Names no registered field
927    /// displays neither block nor render.
928    ///
929    /// Identity is the caller's to preserve: keep one record for as long as
930    /// the response is current and pass a [`Clone`] of it each frame; building
931    /// a fresh record per frame — `ValidationErrors::new()` inline, or a
932    /// re-run builder chain — is a new server response every frame and re-arms
933    /// every named field it mentions, even with identical content. This is
934    /// React Stately's reference identity, unchanged.
935    pub fn validation_errors(mut self, errors: ValidationErrors) -> Self {
936        self.validation_errors = errors;
937        self
938    }
939
940    /// `validationBehavior` — whether a failed field blocks submission.
941    pub fn validation_behavior(mut self, behavior: ValidationBehavior) -> Self {
942        self.validation_behavior = behavior;
943        self
944    }
945
946    /// Registers a named field, so its value appears in the submission.
947    pub fn field(mut self, field: FormField) -> Self {
948        self.fields.push(field);
949        self
950    }
951
952    /// `onSubmit` — receives the collected submission.
953    pub fn on_submit(mut self, f: impl Fn(&FormData, &mut Window, &mut App) + 'static) -> Self {
954        self.on_submit = Some(Arc::new(f));
955        self
956    }
957
958    /// `onReset` — fires after the registered fields are restored.
959    pub fn on_reset(mut self, f: impl Fn(&mut Window, &mut App) + 'static) -> Self {
960        self.on_reset = Some(Arc::new(f));
961        self
962    }
963
964    /// `onInvalid` — runs instead of `onSubmit` when validation blocks it.
965    ///
966    /// Blocked means a required field with no value, or a field whose own
967    /// resolved validity is in error (`validate`, `isInvalid`, the field's
968    /// `validationErrors`, an HTML5 attribute violation, or a server message
969    /// routed to it by the form's `validationErrors` record) — and only under
970    /// [`ValidationBehavior::Native`]; `Allow` submits regardless, as v3's
971    /// does. Disabled and read-only fields never block.
972    pub fn on_invalid(mut self, f: impl Fn(&FormData, &mut Window, &mut App) + 'static) -> Self {
973        self.on_invalid = Some(Arc::new(f));
974        self
975    }
976
977    /// Collects the registered fields into a submission.
978    pub fn data(&self, cx: &App) -> FormData {
979        Self::collect_data(&self.fields, cx)
980    }
981
982    /// Collects `fields` into a submission — the body of [`Form::data`],
983    /// shared with the submission path.
984    fn collect_data(fields: &[FormField], cx: &App) -> FormData {
985        let mut entries = Vec::with_capacity(fields.len());
986        for field in fields {
987            if !field.is_successful(cx) {
988                continue;
989            }
990            // An unnamed field is not submitted, exactly as in HTML.
991            if let Some(name) = field.field_name(cx) {
992                let value = (field.read)(cx);
993                // Unchecked checkbox inputs and checkbox groups with no
994                // selected inputs are absent from native FormData — but a
995                // keyed field carrying the pinned key-mode serialization
996                // still submits one empty value, the hidden `value=""`
997                // input RAC's `values.length === 0` branch renders.
998                let value = match (&value, field.empty_keys_submits_empty) {
999                    (FormValue::Keys(keys), true) if keys.is_empty() => {
1000                        FormValue::Text(SharedString::from(""))
1001                    }
1002                    _ => value,
1003                };
1004                let omitted = match &value {
1005                    FormValue::Flag(false) => true,
1006                    FormValue::Keys(keys) => keys.is_empty(),
1007                    _ => false,
1008                };
1009                if !omitted {
1010                    entries.push((name, value));
1011                }
1012            }
1013        }
1014        FormData { entries }
1015    }
1016
1017    /// The names of the required fields whose *own* value is missing and
1018    /// whose error would block: enabled, native, not read-only. Each field is
1019    /// asked about its own live value — two fields registered under one name
1020    /// are validated independently, never collapsed through the submission
1021    /// record's first-wins `get`.
1022    fn missing_required_fields(fields: &[FormField], cx: &App) -> Vec<SharedString> {
1023        fields
1024            .iter()
1025            .filter(|f| {
1026                f.is_successful(cx)
1027                    && f.is_required
1028                    && !f.is_read_only(cx)
1029                    && f.blocks_submission(cx)
1030                    && (f.read)(cx).is_empty()
1031            })
1032            .filter_map(|f| f.field_name(cx))
1033            .collect()
1034    }
1035
1036    /// The one submission implementation: collect the named fields, decide
1037    /// whether validation blocks, and route to `on_submit` or `on_invalid`.
1038    /// Both doors into a submission — the caller-wired submit button
1039    /// ([`Form::submit_handler`]) and the form root's implicit Enter key
1040    /// handler — run this, so a submission is validated, focused and
1041    /// reported identically however it arrived.
1042    ///
1043    /// `defer_focus` is the Enter-origin latch. On the button path it is
1044    /// `None` and a blocked submit focuses the first invalid field inline,
1045    /// exactly as a click handler may. On the Enter path it is the keyed
1046    /// latch: focusing mid-keystroke would leave the newly focused control
1047    /// holding the focus when the key is *released*, and gpui activates a
1048    /// focused element's click listeners on release — a blocked Enter in a
1049    /// text field would open the Select it moved the focus to, or flip the
1050    /// Switch. So the latch is set instead, the release handler disarms the
1051    /// release and moves the focus once the dispatch is over.
1052    fn run_submission(
1053        fields: &[FormField],
1054        behavior: ValidationBehavior,
1055        on_submit: Option<&OnSubmit>,
1056        on_invalid: Option<&OnSubmit>,
1057        defer_focus: Option<&Entity<bool>>,
1058        window: &mut Window,
1059        cx: &mut App,
1060    ) {
1061        let data = Self::collect_data(fields, cx);
1062        let missing = Self::missing_required_fields(fields, cx);
1063        // A field error blocks a native submit however it arose: a required
1064        // field with no value, a field whose stored validity says it is in
1065        // error (`validate`, `isInvalid`, the field's `validationErrors`, or
1066        // an HTML5 attribute violation), or a field the form's
1067        // `validationErrors` record routed a server message to. Read-only
1068        // fields are barred from constraint validation on all counts, and a
1069        // disabled field is not successful, so neither can block.
1070        let own_invalid: Vec<SharedString> = fields
1071            .iter()
1072            .filter(|f| {
1073                f.is_successful(cx)
1074                    && f.blocks_submission(cx)
1075                    && !f.is_read_only(cx)
1076                    && (f.is_invalid(cx) || f.has_server_errors(cx))
1077            })
1078            .filter_map(|f| f.field_name(cx))
1079            .collect();
1080        let blocked = behavior == ValidationBehavior::Native
1081            && (!missing.is_empty() || !own_invalid.is_empty());
1082        if blocked {
1083            // v3, verbatim: "By default, the first invalid field will be
1084            // focused." A blocked submit moves the focus to the first
1085            // registered field whose error keeps the form from submitting —
1086            // the same union the blocked condition above computes. From the
1087            // button path this is an ordinary click handler; from the Enter
1088            // path the move is deferred past the keystroke (see
1089            // `defer_focus`). The tests drive both paths and assert the
1090            // field holds the focus after.
1091            let focus = Self::first_invalid_focus(fields, cx);
1092            match (defer_focus, focus) {
1093                (Some(latch), Some(_)) => latch.update(cx, |pending, _| *pending = true),
1094                (_, focus) => {
1095                    if let Some(focus) = focus {
1096                        focus(window, cx);
1097                    }
1098                }
1099            }
1100            if let Some(f) = on_invalid {
1101                f(&data, window, cx);
1102            }
1103        } else if let Some(f) = on_submit {
1104            f(&data, window, cx);
1105        }
1106    }
1107
1108    /// The focus callback of the first registered field whose error blocks
1109    /// the submission — the same union [`Self::run_submission`] tests for
1110    /// the blocked decision, so the focus lands on the field the report is
1111    /// about. Read-only fields cannot nominate themselves, and a field with
1112    /// no reachable handle contributes none. An unnamed field cannot either:
1113    /// the blocking lists collect names (each `filter_map`s `field_name`),
1114    /// so an unnamed required or invalid field never blocks and must never
1115    /// take the focus from the named blocker behind it. Each field is asked
1116    /// about its own live value, so duplicate names never collapse into the
1117    /// record's first-wins `get`.
1118    fn first_invalid_focus(fields: &[FormField], cx: &App) -> Option<FocusField> {
1119        fields
1120            .iter()
1121            .find(|field| {
1122                field.field_name(cx).is_some()
1123                    && !field.is_read_only(cx)
1124                    && field.is_successful(cx)
1125                    && field.blocks_submission(cx)
1126                    && (field.is_invalid(cx)
1127                        || field.has_server_errors(cx)
1128                        || (field.is_required && (field.read)(cx).is_empty()))
1129            })
1130            .and_then(|field| field.focus.clone())
1131    }
1132
1133    /// The handler a submit button calls: collects the submission, then routes
1134    /// it to `onSubmit` or `onInvalid`.
1135    ///
1136    /// gpui gives a child no way to reach its form, so the caller wires this to
1137    /// the button in place of `<button type="submit">`. The form root's Enter
1138    /// key handler runs the same implementation, so a submission is decided
1139    /// identically however it arrived.
1140    #[allow(clippy::arc_with_non_send_sync)] // see `util::shared`
1141    pub fn submit_handler(&self) -> Arc<dyn Fn(&mut Window, &mut App) + 'static> {
1142        let fields = self.fields.clone();
1143        let on_submit = self.on_submit.clone();
1144        let on_invalid = self.on_invalid.clone();
1145        let behavior = self.validation_behavior;
1146        Arc::new(move |window: &mut Window, cx: &mut App| {
1147            // Button origin: no keystroke is in flight, so the focus move
1148            // needs no deferral.
1149            Self::run_submission(
1150                &fields,
1151                behavior,
1152                on_submit.as_ref(),
1153                on_invalid.as_ref(),
1154                None,
1155                window,
1156                cx,
1157            );
1158        })
1159    }
1160
1161    /// The handler a reset button calls: restores every field that declared a
1162    /// default, hides the routed server errors, then fires `onReset`.
1163    ///
1164    /// Hiding is a clear, not a rewind: each field keeps its delivery receipt
1165    /// (see `SetServerErrors`), so a re-render passing a *clone* keeps the
1166    /// messages hidden, and only a genuinely new record re-arms.
1167    #[allow(clippy::arc_with_non_send_sync)] // see `util::shared`
1168    pub fn reset_handler(&self) -> Arc<dyn Fn(&mut Window, &mut App) + 'static> {
1169        let restores: Vec<Restore> = self
1170            .fields
1171            .iter()
1172            .filter_map(|f| f.restore.clone())
1173            .collect();
1174        let clear_server_errors: Vec<ClearServerErrors> = self
1175            .fields
1176            .iter()
1177            .filter_map(|f| f.clear_server_errors.clone())
1178            .collect();
1179        let on_reset = self.on_reset.clone();
1180        Arc::new(move |window: &mut Window, cx: &mut App| {
1181            for restore in &restores {
1182                restore(window, cx);
1183            }
1184            for clear in &clear_server_errors {
1185                clear(cx);
1186            }
1187            if let Some(f) = &on_reset {
1188                f(window, cx);
1189            }
1190        })
1191    }
1192}
1193
1194impl Default for Form {
1195    fn default() -> Self {
1196        Self::new()
1197    }
1198}
1199
1200impl ParentElement for Form {
1201    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
1202        self.children.extend(elements);
1203    }
1204}
1205
1206impl RenderOnce for Form {
1207    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1208        let Self {
1209            validation_errors: record,
1210            validation_behavior: behavior,
1211            fields,
1212            on_submit,
1213            on_invalid,
1214            children,
1215            ..
1216        } = self;
1217        let mut el = gpui::div().flex().flex_col().gap(px(16.)).w_full();
1218        // The `validationErrors` record routes; it does not render here.
1219        // Each field carries its own delivery receipt — the record revision
1220        // stored beside its routed messages (see [`SetServerErrors`]) — so
1221        // a re-render passing a *clone* redelivers nothing (a message the
1222        // user edited away stays suppressed), and replacing or reordering
1223        // this form's field registrations re-arms nothing either: the
1224        // receipt travels with the field, not with the form. A genuinely
1225        // new record — its content equal or not — re-arms every named field
1226        // it mentions.
1227        // The blocked-Enter latch: "the keystroke whose release must not
1228        // click anything". Keyed window state, because the release can be
1229        // dispatched against a frame drawn after the press — closures from
1230        // different frames — and keyed by the first participating field's
1231        // state entity, the one stable identity a form without a DOM id has
1232        // (only entity-backed fields — text, number, OTP — can submit on
1233        // Enter, so a form without one never arms the latch).
1234        let latch = fields.iter().find_map(|f| f.state_id).map(|id| {
1235            window.use_keyed_state(
1236                gpui::ElementId::named_usize("form-enter-latch", id.as_u64() as usize),
1237                cx,
1238                |_, _| false,
1239            )
1240        });
1241        // A native `<form>` also submits when Enter is pressed in a
1242        // single-line text control — the implicit submission React Aria's
1243        // Form inherits, and the door this port must wire itself because the
1244        // submit button is caller-wired. The handler never infers from an
1245        // arbitrary bubbling Enter: it fires only while a *registered field
1246        // that participates* — [`FormField::text`], [`FormField::number`] or
1247        // [`FormField::code`] — holds the focus. A TextArea's Enter is a
1248        // newline; a focused submit Button submits through its own click,
1249        // which gpui fires on key-up, so firing here too would submit twice;
1250        // and the non-text compound controls never submit implicitly.
1251        let down_fields = fields.clone();
1252        let down_latch = latch.clone();
1253        el = el.on_key_down(move |ev: &KeyDownEvent, window, cx| {
1254            // A latch still set when a press arrives is from a keystroke
1255            // whose release never happened here — always stale, because a
1256            // release follows its own press through this same dispatch
1257            // path. Clear it before anything else arms a fresh one.
1258            if let Some(latch) = &down_latch {
1259                latch.update(cx, |pending, _| *pending = false);
1260            }
1261            // Mirror the single-line field's own Enter branch: the plain key
1262            // (shift allowed — the rendered control submits on shift+enter
1263            // too), never a chord.
1264            let mods = &ev.keystroke.modifiers;
1265            if ev.keystroke.key.as_str() == "enter"
1266                && !(mods.control || mods.alt || mods.platform)
1267                && down_fields.iter().any(|f| f.submits_on_enter(window, cx))
1268            {
1269                Self::run_submission(
1270                    &down_fields,
1271                    behavior,
1272                    on_submit.as_ref(),
1273                    on_invalid.as_ref(),
1274                    down_latch.as_ref(),
1275                    window,
1276                    cx,
1277                );
1278            }
1279        });
1280        // The release half of the same door: a blocked Enter set the latch
1281        // instead of moving the focus. Consume it on the plain release,
1282        // disarm gpui's key-up activation (`prevent_default` gates the
1283        // listener that fires a focused element's click), and move the focus
1284        // to the first invalid field only after the keystroke has fully
1285        // dispatched — the newly focused Select or Switch never holds the
1286        // focus during a key event, so the release cannot click it.
1287        let up_fields = fields.clone();
1288        let up_latch = latch;
1289        el = el.capture_key_up(move |ev: &KeyUpEvent, window, cx| {
1290            let Some(latch) = &up_latch else {
1291                return;
1292            };
1293            let mods = &ev.keystroke.modifiers;
1294            if ev.keystroke.key.as_str() != "enter"
1295                || mods.control
1296                || mods.alt
1297                || mods.platform
1298                || !*latch.read(cx)
1299            {
1300                return;
1301            }
1302            latch.update(cx, |pending, _| *pending = false);
1303            window.prevent_default();
1304            if let Some(focus) = Self::first_invalid_focus(&up_fields, cx) {
1305                window.defer(cx, move |window, cx| focus(window, cx));
1306            }
1307        });
1308        // The delivery itself runs as the last child of the stack: a
1309        // zero-size canvas appended *after* the caller's fields, whose
1310        // prepaint callback therefore fires after every field has rendered
1311        // and written its `name` into its own state — the same tree-order
1312        // trick that lets a field remember its text origin, or a table arm
1313        // its load-more sentinel. Delivering from `Form::render` instead
1314        // would resolve every `name_of` reader to None: the form renders
1315        // before its fields do. The canvas runs every frame; the per-field
1316        // receipt makes each write idempotent, so a settled frame writes
1317        // nothing.
1318        el = el.children(children);
1319        if fields.iter().any(|f| f.set_server_errors.is_some()) {
1320            let delivery_fields = fields;
1321            el = el.child(
1322                gpui::canvas(
1323                    move |_, _, cx| {
1324                        for field in &delivery_fields {
1325                            field.deliver_server_errors(&record, cx);
1326                        }
1327                    },
1328                    |_, _, _, _| {},
1329                )
1330                .size_0(),
1331            );
1332        }
1333        el
1334    }
1335}
1336
1337#[cfg(test)]
1338mod tests {
1339    use super::*;
1340
1341    fn data(entries: &[(&str, FormValue)]) -> FormData {
1342        FormData {
1343            entries: entries
1344                .iter()
1345                .map(|(n, v)| (SharedString::from(n.to_string()), v.clone()))
1346                .collect(),
1347        }
1348    }
1349
1350    #[test]
1351    fn a_submission_reads_by_name() {
1352        let d = data(&[
1353            ("email", FormValue::Text("a@b.c".into())),
1354            ("age", FormValue::Number(41.0)),
1355        ]);
1356        assert_eq!(d.text("email").unwrap(), SharedString::from("a@b.c"));
1357        assert_eq!(d.text("age").unwrap(), SharedString::from("41"));
1358        assert!(d.get("missing").is_none());
1359        assert_eq!(d.len(), 2);
1360    }
1361
1362    #[test]
1363    fn flags_and_keys_serialise_the_html_way() {
1364        assert_eq!(FormValue::Flag(true).as_text(), SharedString::from("on"));
1365        assert_eq!(FormValue::Flag(false).as_text(), SharedString::from(""));
1366        let keys = FormValue::Keys(vec!["a".into(), "b".into()]);
1367        assert_eq!(keys.as_text(), SharedString::from("a,b"));
1368    }
1369
1370    #[test]
1371    fn emptiness_follows_the_control() {
1372        assert!(FormValue::Text("   ".into()).is_empty());
1373        assert!(!FormValue::Text("x".into()).is_empty());
1374        // An unchecked box submits nothing, so it counts as empty.
1375        assert!(FormValue::Flag(false).is_empty());
1376        assert!(!FormValue::Flag(true).is_empty());
1377        assert!(FormValue::Keys(vec![]).is_empty());
1378        // Zero is a value.
1379        assert!(!FormValue::Number(0.0).is_empty());
1380    }
1381
1382    #[test]
1383    fn missing_required_reports_absent_and_blank_alike() {
1384        let d = data(&[
1385            ("name", FormValue::Text("".into())),
1386            ("tos", FormValue::Flag(true)),
1387        ]);
1388        let required: Vec<SharedString> =
1389            vec!["name".into(), "tos".into(), "never-registered".into()];
1390        assert_eq!(
1391            d.missing_required(&required),
1392            vec![
1393                SharedString::from("name"),
1394                SharedString::from("never-registered")
1395            ]
1396        );
1397    }
1398}