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}