Skip to main content

herogpui_components/
input.rs

1//! Input & InputState — port of `@heroui/input`.
2//!
3//! `InputState` is an entity holding the editable value; [`Input`] is the
4//! styled element bound to it (controlled like HeroUI's controlled inputs).
5
6use gpui::{
7    prelude::*, px, App, Entity, FocusHandle, Focusable, IntoElement, KeyDownEvent, Pixels,
8    RenderOnce, SharedString, Styled, Window,
9};
10use herogpui_core::{element_id, FieldVariant};
11use herogpui_theme::ActiveTheme;
12
13use crate::a11y::A11y as _;
14
15/// Editable state of a single-line text input.
16pub struct InputState {
17    value: String,
18    /// Cursor position in char indices (the focused end of the selection).
19    cursor: usize,
20    /// Selection anchor in char indices; `None` when the caret is collapsed.
21    anchor: Option<usize>,
22    marked: Option<std::ops::Range<usize>>,
23    pub(crate) focus_handle: FocusHandle,
24    /// Clear-button handle. Created without `tab_stop`, matching pinned
25    /// `useSearchField` `excludeFromTabOrder: true`. Never a `tab_stop_handle`.
26    pub(crate) clear_focus_handle: FocusHandle,
27    /// `name` — what this field submits under, written in by the component's
28    /// `name` builder. The state carries it because gpui gives a child no way
29    /// to reach its `Form`; `FormField::text` reads it back out.
30    name: Option<SharedString>,
31    /// `validationBehavior` — travels with the name, for the same reason.
32    validation_behavior: crate::form::ValidationBehavior,
33    /// The resolved validity, written by `Input::render` so a `Form` can see
34    /// whether this field blocks a native submission — the same reason `name`
35    /// rides on the state.
36    validity: crate::validation::Validity,
37    /// Disabled native controls are not successful and therefore do not
38    /// contribute an entry to FormData. Written by `Input::render` so the
39    /// registered [`crate::form::FormField`] can read the live state.
40    is_successful: bool,
41    /// Read-only native controls stay successful and focusable, but HTML
42    /// constraint validation bars them: neither a missing value nor a stored
43    /// error on a read-only field may block a submission. Mirrored by
44    /// `Input::render` like `is_successful`, so the form reads the rendered
45    /// state rather than a builder snapshot — and `NumberField` inherits the
46    /// same mirror through the inner `InputState` it forwards its own
47    /// `isReadOnly` to.
48    is_read_only: bool,
49    /// Server messages routed to this field by its `Form`'s
50    /// `validationErrors` record (`form.rs`). The form writes them on a new
51    /// record, and *this field's own editing* clears them — v3:
52    /// "displayed immediately and cleared when user modifies the field" —
53    /// so a message never outlives the edit that answers it, and a sibling's
54    /// message never disappears with someone else's edit. `Input::render`
55    /// merges them into the resolved validity, ahead of the field's own
56    /// `validationErrors` prop.
57    routed_errors: Vec<SharedString>,
58    /// The delivery receipt for `routed_errors`: the record revision these
59    /// messages last came from, `0` before anything arrived. The form's
60    /// delivery consults it — a record the receipt already names is a clone
61    /// of one already delivered and re-arms nothing — so it moves with the
62    /// messages in one write and survives edit suppression: clearing the
63    /// messages must never rewind the receipt, or the next frame's clone
64    /// would resurrect what the edit answered.
65    routed_revision: u64,
66}
67
68impl InputState {
69    /// Creates an empty state with a fresh focus handle.
70    pub fn new(cx: &mut App) -> Self {
71        Self {
72            value: String::new(),
73            cursor: 0,
74            anchor: None,
75            marked: None,
76            // A field is a tab stop: the handle carries that, not the element.
77            focus_handle: cx.focus_handle().tab_stop(true),
78            // Plain handle: Tab never seats on the clear button.
79            clear_focus_handle: cx.focus_handle(),
80            name: None,
81            validation_behavior: crate::form::ValidationBehavior::Native,
82            validity: crate::validation::Validity::default(),
83            is_successful: true,
84            is_read_only: false,
85            routed_errors: Vec::new(),
86            routed_revision: 0,
87        }
88    }
89
90    /// `defaultValue` — a state seeded with initial text, with the caret at the
91    /// end.
92    ///
93    /// This is the uncontrolled entry point: React needs a `defaultValue` prop
94    /// because it has no state object to seed.
95    pub fn with_value(cx: &mut App, value: impl Into<String>) -> Self {
96        let mut state = Self::new(cx);
97        state.set_value(value);
98        state
99    }
100
101    /// The current text value.
102    pub fn value(&self) -> &str {
103        &self.value
104    }
105
106    /// The `name` this field submits under, if one was set.
107    pub fn name(&self) -> Option<SharedString> {
108        self.name.clone()
109    }
110
111    /// Sets the submission name. Called by the component's `name` builder, not
112    /// usually by hand.
113    pub fn set_name(&mut self, name: Option<SharedString>) {
114        self.name = name;
115    }
116
117    /// Whether this field's invalidity blocks form submission.
118    pub fn validation_behavior(&self) -> crate::form::ValidationBehavior {
119        self.validation_behavior
120    }
121
122    /// Set by the component's `validation_behavior` builder.
123    pub fn set_validation_behavior(&mut self, behavior: crate::form::ValidationBehavior) {
124        self.validation_behavior = behavior;
125    }
126
127    /// Records the resolved validity, written by `Input::render` so `Form`
128    /// can see why a native submission is blocked. The write is guarded at
129    /// the call site (only when the value differs), like `set_name`'s.
130    pub(crate) fn set_validity(&mut self, validity: crate::validation::Validity) {
131        self.validity = validity;
132    }
133
134    /// The resolved validity, as last written by `Input::render`.
135    pub(crate) fn validity(&self) -> &crate::validation::Validity {
136        &self.validity
137    }
138
139    pub(crate) fn is_successful(&self) -> bool {
140        self.is_successful
141    }
142
143    pub(crate) fn set_successful(&mut self, is_successful: bool) {
144        self.is_successful = is_successful;
145    }
146
147    /// The rendered read-only state, as last written by `Input::render`.
148    pub(crate) fn is_read_only(&self) -> bool {
149        self.is_read_only
150    }
151
152    pub(crate) fn set_read_only(&mut self, is_read_only: bool) {
153        self.is_read_only = is_read_only;
154    }
155
156    /// The server messages the form routed to this field, as last written by
157    /// the form's `validationErrors` delivery — what this field's error slot
158    /// renders, and what a native submit consults beside the stored
159    /// validity. Empty once the user edited the field or a reset hid them.
160    pub fn routed_errors(&self) -> &[SharedString] {
161        &self.routed_errors
162    }
163
164    /// The delivery receipt beside [`Self::routed_errors`] — the record
165    /// revision these messages last came from, `0` before any delivery.
166    pub(crate) fn routed_revision(&self) -> u64 {
167        self.routed_revision
168    }
169
170    /// Replaces the routed server messages *and* the receipt that names the
171    /// record they came from, in one update. Guarded by the caller, which
172    /// compares the receipt before writing so a render cannot notify-loop.
173    pub(crate) fn set_routed(&mut self, messages: Vec<SharedString>, revision: u64) {
174        self.routed_errors = messages;
175        self.routed_revision = revision;
176    }
177
178    /// Suppresses the routed server messages: the user modified *this* field,
179    /// so only its messages clear and its siblings keep theirs. The delivery
180    /// receipt is deliberately untouched — the record that delivered already
181    /// named this field, so a clone re-rendered on the next frame must not
182    /// resurrect what the edit answered.
183    pub(crate) fn clear_routed_errors(&mut self) {
184        self.routed_errors.clear();
185    }
186
187    /// Replaces the value, clears any selection and composition, and moves the cursor to the end.
188    pub fn set_value(&mut self, value: impl Into<String>) {
189        self.value = value.into();
190        self.marked = None;
191        self.anchor = None;
192        self.cursor = self.value.chars().count();
193    }
194
195    /// Whether the value is empty.
196    pub fn is_empty(&self) -> bool {
197        self.value.is_empty()
198    }
199
200    /// The clear affordance's focus handle. It is not a tab stop.
201    pub fn clear_focus_handle(&self) -> FocusHandle {
202        self.clear_focus_handle.clone()
203    }
204
205    /// Normalized `(start, end)` char range of the active selection.
206    pub fn selection(&self) -> Option<(usize, usize)> {
207        let a = self.anchor?;
208        let c = self.cursor;
209        if a == c {
210            None
211        } else {
212            Some((a.min(c), a.max(c)))
213        }
214    }
215}
216
217impl Focusable for InputState {
218    fn focus_handle(&self, _cx: &App) -> FocusHandle {
219        self.focus_handle.clone()
220    }
221}
222
223// -- char-index editing helpers -------------------------------------------
224
225/// Checks a whole value against the v3 field constraints.
226#[allow(clippy::too_many_arguments)]
227fn validate_value(
228    value: &str,
229    input_type: InputType,
230    min_length: Option<usize>,
231    min: Option<f64>,
232    max: Option<f64>,
233    step: Option<f64>,
234    pattern: Option<&dyn Fn(&str) -> bool>,
235) -> InputValidity {
236    // An empty field is "not yet filled in", not invalid; `is_required` is the
237    // prop that speaks to emptiness.
238    if value.is_empty() {
239        return InputValidity::Valid;
240    }
241
242    if let Some(f) = pattern {
243        if !f(value) {
244            return InputValidity::PatternMismatch;
245        }
246    }
247
248    if min_length.is_some_and(|n| value.chars().count() < n) {
249        return InputValidity::TooShort;
250    }
251
252    // The numeric bounds only mean anything for a numeric field with a
253    // parsable value.
254    if input_type == InputType::Number {
255        if let Ok(n) = value.parse::<f64>() {
256            if min.is_some_and(|lo| n < lo) {
257                return InputValidity::BelowMin;
258            }
259            if max.is_some_and(|hi| n > hi) {
260                return InputValidity::AboveMax;
261            }
262            if let Some(step) = step.filter(|s| *s > 0.0) {
263                let base = min.unwrap_or(0.0);
264                let steps = (n - base) / step;
265                if (steps - steps.round()).abs() > 1e-6 {
266                    return InputValidity::OffStep;
267                }
268            }
269        }
270    }
271
272    InputValidity::Valid
273}
274
275/// Refreshes the stored validity mirror after an accepted edit. `render`
276/// writes that mirror, so without this it is one frame old — and an
277/// `on_change` may submit the enclosing `Form` synchronously (the auto-submit
278/// v3's own docs invite) before any frame catches up, reading the routed
279/// server error the edit just answered as still blocking. The routed slot is
280/// left out — the edit suppressed it — so the mirror is resolved from the
281/// builder's own sources exactly as the next render would resolve it with an
282/// empty routed slot, HTML5 attribute check included.
283fn refresh_stored_validity(
284    state: &Entity<InputState>,
285    is_invalid: bool,
286    validation_errors: &[SharedString],
287    validate: Option<&crate::validation::Validator<str>>,
288    error_message: Option<SharedString>,
289    native_valid: &dyn Fn(&str) -> bool,
290    cx: &mut App,
291) {
292    let value = state.read(cx).value().to_owned();
293    let mut validity = crate::validation::resolve(
294        is_invalid,
295        validation_errors,
296        validate.and_then(|f| f(&value)),
297        error_message,
298    );
299    if !native_valid(&value) {
300        validity.is_invalid = true;
301    }
302    if state.read(cx).validity() != &validity {
303        state.update(cx, |s, _| s.set_validity(validity));
304    }
305}
306
307/// Whether `ch` may be inserted, honouring `type` and `maxLength`.
308///
309/// Takes the measured lengths rather than the whole state so it stays a pure
310/// function (an `InputState` needs an `App` for its focus handle).
311fn accepts_char(
312    len_chars: usize,
313    selected_chars: usize,
314    ch: char,
315    input_type: InputType,
316    max_length: Option<usize>,
317) -> bool {
318    if !input_type.accepts(ch) {
319        return false;
320    }
321    match max_length {
322        // A selection is replaced by the keystroke, so it frees up room.
323        Some(max) => len_chars.saturating_sub(selected_chars) < max,
324        None => true,
325    }
326}
327
328/// [`accepts_char`] for a live state.
329fn state_accepts(
330    state: &InputState,
331    ch: char,
332    input_type: InputType,
333    max_length: Option<usize>,
334) -> bool {
335    let selected = state.selection().map_or(0, |(lo, hi)| hi - lo);
336    accepts_char(
337        state.value.chars().count(),
338        selected,
339        ch,
340        input_type,
341        max_length,
342    )
343}
344
345fn insert_char(state: &mut InputState, ch: char) {
346    delete_selection(state);
347    // A collapsed anchor can remain after a multi-character platform edit
348    // sets the range once and then inserts several characters. Treat the
349    // first insertion as a normal caret edit so the next character does not
350    // delete the character just inserted.
351    state.anchor = None;
352    let byte_idx = char_to_byte(&state.value, state.cursor);
353    state.value.insert(byte_idx, ch);
354    state.cursor += 1;
355}
356
357/// Removes the active selection (if any); returns true when it did.
358fn delete_selection(state: &mut InputState) -> bool {
359    if let Some((lo, hi)) = state.selection() {
360        let lo_b = char_to_byte(&state.value, lo);
361        let hi_b = char_to_byte(&state.value, hi);
362        state.value.replace_range(lo_b..hi_b, "");
363        state.cursor = lo;
364        state.anchor = None;
365        true
366    } else {
367        false
368    }
369}
370
371/// The char index of the extended grapheme cluster boundary before `cursor`
372/// (a char index), so caret motion and deletion never split an emoji, a flag,
373/// a ZWJ sequence or a base letter from its combining marks.
374fn prev_grapheme(value: &str, cursor: usize) -> usize {
375    if cursor == 0 {
376        return 0;
377    }
378    let byte = char_to_byte(value, cursor);
379    let mut graphemes = unicode_segmentation::GraphemeCursor::new(byte, value.len(), true);
380    match graphemes.prev_boundary(value, 0) {
381        Ok(Some(prev)) => byte_to_char(value, prev),
382        _ => cursor - 1,
383    }
384}
385
386/// The char index of the extended grapheme cluster boundary after `cursor`;
387/// see [`prev_grapheme`].
388fn next_grapheme(value: &str, cursor: usize) -> usize {
389    let byte = char_to_byte(value, cursor);
390    if byte >= value.len() {
391        return cursor;
392    }
393    let mut graphemes = unicode_segmentation::GraphemeCursor::new(byte, value.len(), true);
394    match graphemes.next_boundary(value, 0) {
395        Ok(Some(next)) => byte_to_char(value, next),
396        _ => cursor + 1,
397    }
398}
399
400fn backspace(state: &mut InputState) -> bool {
401    if delete_selection(state) {
402        return true;
403    }
404    if state.cursor == 0 {
405        return false;
406    }
407    let byte_idx = char_to_byte(&state.value, state.cursor);
408    let prev_char = prev_grapheme(&state.value, state.cursor);
409    let prev = char_to_byte(&state.value, prev_char);
410    state.value.replace_range(prev..byte_idx, "");
411    state.cursor = prev_char;
412    true
413}
414
415fn delete(state: &mut InputState) -> bool {
416    if delete_selection(state) {
417        return true;
418    }
419    let len = state.value.chars().count();
420    if state.cursor >= len {
421        return false;
422    }
423    let byte_idx = char_to_byte(&state.value, state.cursor);
424    let next = char_to_byte(&state.value, next_grapheme(&state.value, state.cursor));
425    state.value.replace_range(byte_idx..next, "");
426    true
427}
428
429fn move_left(state: &mut InputState, extend: bool) {
430    if !extend {
431        state.anchor = None;
432    } else if state.anchor.is_none() {
433        state.anchor = Some(state.cursor);
434    }
435    state.cursor = prev_grapheme(&state.value, state.cursor);
436}
437
438fn move_right(state: &mut InputState, extend: bool) {
439    if !extend {
440        state.anchor = None;
441    } else if state.anchor.is_none() {
442        state.anchor = Some(state.cursor);
443    }
444    state.cursor = next_grapheme(&state.value, state.cursor);
445}
446
447fn move_home(state: &mut InputState, extend: bool) {
448    if !extend {
449        state.anchor = None;
450    } else if state.anchor.is_none() {
451        state.anchor = Some(state.cursor);
452    }
453    state.cursor = 0;
454}
455
456fn move_end(state: &mut InputState, extend: bool) {
457    if !extend {
458        state.anchor = None;
459    } else if state.anchor.is_none() {
460        state.anchor = Some(state.cursor);
461    }
462    state.cursor = state.value.chars().count();
463}
464
465fn select_all(state: &mut InputState) {
466    state.anchor = Some(0);
467    state.cursor = state.value.chars().count();
468}
469
470/// The text a char range covers, which is what the clipboard gets.
471///
472/// Pure, so the tests can reach it: building an `InputState` needs an `App` for
473/// its focus handle, and none of the motion logic touches that.
474fn slice_selection(value: &str, selection: Option<(usize, usize)>) -> Option<String> {
475    let (lo, hi) = selection?;
476    let lo_b = char_to_byte(value, lo);
477    let hi_b = char_to_byte(value, hi);
478    Some(value[lo_b..hi_b].to_owned())
479}
480
481/// Starts the selection at the caret if `extend` and there is none yet, and
482/// clears it otherwise. Every motion begins this way.
483fn before_move(state: &mut InputState, extend: bool) {
484    if !extend {
485        state.anchor = None;
486    } else if state.anchor.is_none() {
487        state.anchor = Some(state.cursor);
488    }
489}
490
491/// Whether a char counts as part of a word for `move_word`.
492fn is_word(c: char) -> bool {
493    c.is_alphanumeric() || c == '_'
494}
495
496/// Where Ctrl+Left / Ctrl+Right lands: over any run of separators, then over
497/// the word.
498fn word_target(value: &str, cursor: usize, forward: bool) -> usize {
499    let chars: Vec<char> = value.chars().collect();
500    let mut i = cursor.min(chars.len());
501    if forward {
502        while i < chars.len() && !is_word(chars[i]) {
503            i += 1;
504        }
505        while i < chars.len() && is_word(chars[i]) {
506            i += 1;
507        }
508    } else {
509        while i > 0 && !is_word(chars[i - 1]) {
510            i -= 1;
511        }
512        while i > 0 && is_word(chars[i - 1]) {
513            i -= 1;
514        }
515    }
516    i
517}
518
519fn move_word(state: &mut InputState, forward: bool, extend: bool) {
520    before_move(state, extend);
521    state.cursor = word_target(&state.value, state.cursor, forward);
522}
523
524/// What the field draws: the value, or one bullet per char for a password.
525///
526/// The click maths runs on this rather than on the value, so a masked field maps
527/// its own glyph widths.
528fn displayed_value(state: &InputState, masks: bool) -> String {
529    if masks {
530        "\u{2022}".repeat(state.value.chars().count())
531    } else {
532        state.value.clone()
533    }
534}
535
536/// Which char a pointer at `x` (relative to the text's left edge) is nearest.
537///
538/// A mouse listener is handed the pointer position and nothing else, so the
539/// text's own origin has to be remembered from the previous frame -- see
540/// `TEXT_ORIGIN` in `Input::render`. gpui shapes the line for us, and
541/// `closest_index_for_x` answers in *bytes*, which the state counts in chars.
542fn char_at_x(
543    value: &str,
544    x: Pixels,
545    font: &gpui::Font,
546    font_size: Pixels,
547    window: &mut Window,
548) -> usize {
549    if value.is_empty() {
550        return 0;
551    }
552    let run = gpui::TextRun {
553        len: value.len(),
554        font: font.clone(),
555        // Shaping needs a colour and does not use it.
556        color: gpui::black(),
557        background_color: None,
558        underline: None,
559        strikethrough: None,
560    };
561    let line = window.text_system().shape_line(
562        SharedString::from(value.to_owned()),
563        font_size,
564        &[run],
565        None,
566    );
567    let byte = line.closest_index_for_x(x.max(px(0.)));
568    value[..byte.min(value.len())].chars().count()
569}
570
571/// The char offset a click lands on inside a wrapped, multi-line body.
572///
573/// The paragraph is found by the bounds it was painted with, then gpui shapes
574/// that paragraph at the width it was given and reports the closest character
575/// boundary to the point -- `WrappedLine` derefs to the layout that answers it,
576/// so a wrapped line does have a position after all. Returns `None` when no
577/// paragraph has been laid out yet, which is any frame before the first paint.
578fn char_at_point(
579    value: &str,
580    point: gpui::Point<Pixels>,
581    paragraphs: &[gpui::Bounds<Pixels>],
582    font: &gpui::Font,
583    font_size: Pixels,
584    line_height: Pixels,
585    window: &mut Window,
586) -> Option<usize> {
587    let lines: Vec<&str> = value.split('\n').collect();
588    let usable = paragraphs.len().min(lines.len());
589    if usable == 0 {
590        return None;
591    }
592
593    // The paragraph the click is inside, else the nearest one vertically: a
594    // click in the padding below the last line belongs to the last line.
595    let index = (0..usable)
596        .find(|&i| {
597            let b = paragraphs[i];
598            point.y >= b.origin.y && point.y < b.origin.y + b.size.height.max(line_height)
599        })
600        .unwrap_or_else(|| {
601            (0..usable)
602                .min_by_key(|&i| {
603                    let b = paragraphs[i];
604                    let mid = b.origin.y + b.size.height.max(line_height) / 2.;
605                    (f32::from(point.y - mid)).abs() as i64
606                })
607                .unwrap_or(0)
608        });
609
610    let offset: usize = lines[..index].iter().map(|l| l.chars().count() + 1).sum();
611    let line = lines[index];
612    if line.is_empty() {
613        return Some(offset);
614    }
615
616    let bounds = paragraphs[index];
617    let run = gpui::TextRun {
618        len: line.len(),
619        font: font.clone(),
620        // Shaping needs a colour and does not use it.
621        color: gpui::black(),
622        background_color: None,
623        underline: None,
624        strikethrough: None,
625    };
626    let shaped = window
627        .text_system()
628        .shape_text(
629            SharedString::from(line.to_owned()),
630            font_size,
631            &[run],
632            Some(bounds.size.width.max(px(1.))),
633            None,
634        )
635        .ok()?;
636    let wrapped = shaped.first()?;
637    let local = gpui::point(point.x - bounds.origin.x, point.y - bounds.origin.y);
638    let byte = match wrapped.closest_index_for_position(local, line_height) {
639        Ok(byte) | Err(byte) => byte,
640    };
641    Some(offset + line[..byte.min(line.len())].chars().count())
642}
643
644/// The char indices bounding the line the caret is on, newlines excluded.
645fn line_bounds(value: &str, cursor: usize) -> (usize, usize) {
646    let chars: Vec<char> = value.chars().collect();
647    let mut start = cursor.min(chars.len());
648    while start > 0 && chars[start - 1] != '\n' {
649        start -= 1;
650    }
651    let mut end = cursor.min(chars.len());
652    while end < chars.len() && chars[end] != '\n' {
653        end += 1;
654    }
655    (start, end)
656}
657
658/// Where Up / Down lands in a multiline field, keeping the column where it can.
659///
660/// The lines are the *logical* ones -- the ones `Enter` puts in. Up and Down
661/// keeping to those rather than to visual rows is a deliberate difference from
662/// the pointer, which lands on the visual row it was clicked on
663/// (`char_at_point`); moving the keyboard caret to visual rows would need the
664/// same shaping on every keystroke.
665fn vertical_target(value: &str, cursor: usize, down: bool) -> usize {
666    let (start, end) = line_bounds(value, cursor);
667    let column = cursor - start;
668    if down {
669        let len = value.chars().count();
670        if end >= len {
671            return len;
672        }
673        let (next_start, next_end) = line_bounds(value, end + 1);
674        (next_start + column).min(next_end)
675    } else {
676        if start == 0 {
677            return 0;
678        }
679        let (prev_start, prev_end) = line_bounds(value, start - 1);
680        (prev_start + column).min(prev_end)
681    }
682}
683
684fn move_vertical(state: &mut InputState, down: bool, extend: bool) {
685    before_move(state, extend);
686    state.cursor = vertical_target(&state.value, state.cursor, down);
687}
688
689fn char_to_byte(s: &str, char_idx: usize) -> usize {
690    s.char_indices().nth(char_idx).map_or(s.len(), |(b, _)| b)
691}
692
693/// The char index of a byte offset that sits on a char boundary.
694fn byte_to_char(s: &str, byte: usize) -> usize {
695    s[..byte.min(s.len())].chars().count()
696}
697
698/// A validation outcome for the current value.
699///
700/// v3 leaves validation to React Aria; here the caller reads this and passes
701/// the result to `is_invalid` / `error_message`, so the rules stay declarative
702/// without us owning a validation lifecycle.
703#[derive(Clone, Copy, Debug, PartialEq, Eq)]
704pub enum InputValidity {
705    /// The value satisfies every constraint.
706    Valid,
707    /// Shorter than `minLength`.
708    TooShort,
709    /// Below `min` (numeric types).
710    BelowMin,
711    /// Above `max` (numeric types).
712    AboveMax,
713    /// Not a multiple of `step` from `min`.
714    OffStep,
715    /// Does not satisfy `pattern`.
716    PatternMismatch,
717}
718
719impl InputValidity {
720    /// Whether this outcome is `Valid`.
721    pub fn is_valid(self) -> bool {
722        matches!(self, InputValidity::Valid)
723    }
724}
725
726/// The `type` attribute. Only the variants that change rendering or input
727/// handling in gpui are modelled.
728#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
729pub enum InputType {
730    /// Plain text.
731    #[default]
732    Text,
733    /// Masks the value with bullets.
734    Password,
735    /// Email address; reports the email input role.
736    Email,
737    /// Restricts typing to digits, `-` and `.`.
738    Number,
739    /// Telephone number; reports the telephone input role.
740    Tel,
741    /// URL; reports the URL input role.
742    Url,
743    /// Search text; reports the search input role.
744    Search,
745}
746
747impl InputType {
748    /// The role a native `<input>` of this type reports.
749    ///
750    /// `useTextField` passes `type` straight through to the DOM input
751    /// (`inputOnlyProps = { type, pattern }`), so the role is whatever the
752    /// platform maps that input type to. AccessKit's role enum is modelled on
753    /// exactly those input types, so the mapping is one-to-one.
754    pub(crate) fn a11y_role(self) -> crate::a11y::Role {
755        match self {
756            InputType::Text => crate::a11y::Role::TextInput,
757            InputType::Password => crate::a11y::Role::PasswordInput,
758            InputType::Email => crate::a11y::Role::EmailInput,
759            InputType::Number => crate::a11y::Role::NumberInput,
760            InputType::Tel => crate::a11y::Role::PhoneNumberInput,
761            InputType::Url => crate::a11y::Role::UrlInput,
762            InputType::Search => crate::a11y::Role::SearchInput,
763        }
764    }
765
766    /// Every input type, in declaration order.
767    pub const ALL: [InputType; 7] = [
768        InputType::Text,
769        InputType::Password,
770        InputType::Email,
771        InputType::Number,
772        InputType::Tel,
773        InputType::Url,
774        InputType::Search,
775    ];
776
777    /// The type's display name.
778    pub fn label(self) -> &'static str {
779        match self {
780            InputType::Text => "Text",
781            InputType::Password => "Password",
782            InputType::Email => "Email",
783            InputType::Number => "Number",
784            InputType::Tel => "Tel",
785            InputType::Url => "Url",
786            InputType::Search => "Search",
787        }
788    }
789
790    fn masks(self) -> bool {
791        matches!(self, InputType::Password)
792    }
793
794    /// Whether `ch` may be typed into a field of this type.
795    fn accepts(self, ch: char) -> bool {
796        match self {
797            InputType::Number => ch.is_ascii_digit() || ch == '-' || ch == '.',
798            _ => true,
799        }
800    }
801}
802
803type TextCallback = std::sync::Arc<dyn Fn(&str, &mut Window, &mut App) + 'static>;
804type ClearCallback = std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>;
805
806fn char_to_utf16(value: &str, offset: usize) -> usize {
807    value.chars().take(offset).map(char::len_utf16).sum()
808}
809
810fn utf16_to_char(value: &str, offset: usize) -> usize {
811    let mut units = 0;
812    value
813        .chars()
814        .take_while(|ch| {
815            units += ch.len_utf16();
816            units <= offset
817        })
818        .count()
819}
820
821struct PlatformTextInput {
822    state: Entity<InputState>,
823    on_edit: TextCallback,
824    input_type: InputType,
825    max_length: Option<usize>,
826    multiline: bool,
827    bounds: gpui::Bounds<Pixels>,
828    font: gpui::Font,
829    paragraphs: Entity<Vec<gpui::Bounds<Pixels>>>,
830}
831
832impl PlatformTextInput {
833    fn replace(
834        &mut self,
835        range: Option<std::ops::Range<usize>>,
836        text: &str,
837        marked_selection: Option<Option<std::ops::Range<usize>>>,
838        window: &mut Window,
839        cx: &mut App,
840    ) {
841        let changed = self.state.update(cx, |state, cx| {
842            if state.is_read_only || !state.is_successful {
843                return false;
844            }
845            let range = range
846                .map(|range| {
847                    utf16_to_char(&state.value, range.start)..utf16_to_char(&state.value, range.end)
848                })
849                .or_else(|| state.marked.clone())
850                .unwrap_or_else(|| {
851                    let (start, end) = state.selection().unwrap_or((state.cursor, state.cursor));
852                    start..end
853                });
854            let old_value = state.value.clone();
855            let old_cursor = state.cursor;
856            let old_anchor = state.anchor;
857            state.anchor = Some(range.start);
858            state.cursor = range.end;
859            let mut inserted = false;
860            for ch in text.chars() {
861                if (ch == '\n' && self.multiline || !ch.is_control())
862                    && state_accepts(state, ch, self.input_type, self.max_length)
863                {
864                    insert_char(state, ch);
865                    inserted = true;
866                }
867            }
868            if text.is_empty() {
869                delete_selection(state);
870            } else if !inserted {
871                state.cursor = old_cursor;
872                state.anchor = old_anchor;
873                return false;
874            }
875            let end = state.cursor;
876            state.anchor = None;
877            state.marked = marked_selection
878                .as_ref()
879                .and_then(|_| (end > range.start).then_some(range.start..end));
880            if let Some(Some(selection)) = marked_selection {
881                let inserted: String = state
882                    .value
883                    .chars()
884                    .skip(range.start)
885                    .take(end - range.start)
886                    .collect();
887                state.anchor = Some(range.start + utf16_to_char(&inserted, selection.start));
888                state.cursor = range.start + utf16_to_char(&inserted, selection.end);
889            }
890            cx.notify();
891            state.value != old_value
892        });
893        if changed {
894            let value = self.state.read(cx).value.clone();
895            (self.on_edit)(&value, window, cx);
896        }
897    }
898}
899
900impl gpui::InputHandler for PlatformTextInput {
901    fn selected_text_range(
902        &mut self,
903        _: bool,
904        _: &mut Window,
905        cx: &mut App,
906    ) -> Option<gpui::UTF16Selection> {
907        let state = self.state.read(cx);
908        let (start, end) = state.selection().unwrap_or((state.cursor, state.cursor));
909        Some(gpui::UTF16Selection {
910            range: char_to_utf16(&state.value, start)..char_to_utf16(&state.value, end),
911            reversed: state.anchor.is_some_and(|anchor| anchor > state.cursor),
912        })
913    }
914
915    fn marked_text_range(
916        &mut self,
917        _: &mut Window,
918        cx: &mut App,
919    ) -> Option<std::ops::Range<usize>> {
920        let state = self.state.read(cx);
921        state.marked.as_ref().map(|range| {
922            char_to_utf16(&state.value, range.start)..char_to_utf16(&state.value, range.end)
923        })
924    }
925
926    fn text_for_range(
927        &mut self,
928        range: std::ops::Range<usize>,
929        actual: &mut Option<std::ops::Range<usize>>,
930        _: &mut Window,
931        cx: &mut App,
932    ) -> Option<String> {
933        let value = &self.state.read(cx).value;
934        let start = utf16_to_char(value, range.start);
935        let end = utf16_to_char(value, range.end);
936        *actual = Some(char_to_utf16(value, start)..char_to_utf16(value, end));
937        Some(
938            value
939                .chars()
940                .skip(start)
941                .take(end.saturating_sub(start))
942                .collect(),
943        )
944    }
945
946    fn replace_text_in_range(
947        &mut self,
948        range: Option<std::ops::Range<usize>>,
949        text: &str,
950        window: &mut Window,
951        cx: &mut App,
952    ) {
953        self.replace(range, text, None, window, cx);
954    }
955
956    fn replace_and_mark_text_in_range(
957        &mut self,
958        range: Option<std::ops::Range<usize>>,
959        text: &str,
960        selection: Option<std::ops::Range<usize>>,
961        window: &mut Window,
962        cx: &mut App,
963    ) {
964        self.replace(range, text, Some(selection), window, cx);
965    }
966
967    fn unmark_text(&mut self, _: &mut Window, cx: &mut App) {
968        self.state.update(cx, |state, cx| {
969            state.marked = None;
970            cx.notify();
971        });
972    }
973
974    fn set_selected_text_range(
975        &mut self,
976        range: std::ops::Range<usize>,
977        _: &mut Window,
978        cx: &mut App,
979    ) {
980        self.state.update(cx, |state, cx| {
981            state.anchor = Some(utf16_to_char(&state.value, range.start));
982            state.cursor = utf16_to_char(&state.value, range.end);
983            cx.notify();
984        });
985    }
986
987    fn text_length_utf16(&mut self, _: &mut Window, cx: &mut App) -> Option<usize> {
988        Some(self.state.read(cx).value.encode_utf16().count())
989    }
990
991    fn accepts_text_input(&mut self, _: &mut Window, cx: &mut App) -> bool {
992        let state = self.state.read(cx);
993        !state.is_read_only && state.is_successful
994    }
995
996    fn prefers_ime_for_printable_keys(&mut self, _: &mut Window, _: &mut App) -> bool {
997        true
998    }
999
1000    fn element_bounds(&mut self, _: &mut Window, _: &mut App) -> Option<gpui::Bounds<Pixels>> {
1001        Some(self.bounds)
1002    }
1003
1004    fn bounds_for_range(
1005        &mut self,
1006        range: std::ops::Range<usize>,
1007        window: &mut Window,
1008        cx: &mut App,
1009    ) -> Option<gpui::Bounds<Pixels>> {
1010        let state = self.state.read(cx);
1011        let start = utf16_to_char(&state.value, range.start);
1012        let end = utf16_to_char(&state.value, range.end);
1013        let shown = displayed_value(state, self.input_type.masks());
1014        let (line, offset, bounds) = if self.multiline {
1015            let prefix: String = shown.chars().take(start).collect();
1016            let paragraph = prefix.chars().filter(|&ch| ch == '\n').count();
1017            let offset = prefix.rsplit('\n').next()?.chars().count();
1018            (
1019                shown.split('\n').nth(paragraph)?,
1020                offset,
1021                *self.paragraphs.read(cx).get(paragraph)?,
1022            )
1023        } else {
1024            (shown.as_str(), start, self.bounds)
1025        };
1026        let size = crate::util::FIELD_TEXT;
1027        let height = px(20.);
1028        let run = gpui::TextRun {
1029            len: line.len(),
1030            font: self.font.clone(),
1031            color: gpui::black(),
1032            background_color: None,
1033            underline: None,
1034            strikethrough: None,
1035        };
1036        let shaped = window
1037            .text_system()
1038            .shape_text(
1039                line.to_owned().into(),
1040                size,
1041                &[run],
1042                self.multiline.then_some(bounds.size.width.max(px(1.))),
1043                None,
1044            )
1045            .ok()?;
1046        let shaped = shaped.first()?;
1047        let left = shaped.position_for_index(char_to_byte(line, offset), height)?;
1048        let right = shaped
1049            .position_for_index(
1050                char_to_byte(line, offset + end.saturating_sub(start)),
1051                height,
1052            )
1053            .unwrap_or(left);
1054        let y = if self.multiline {
1055            bounds.top()
1056        } else {
1057            bounds.top() + (bounds.size.height - height) / 2.
1058        };
1059        Some(gpui::Bounds::new(
1060            gpui::point(bounds.left() + left.x, y + left.y),
1061            gpui::size(
1062                if left.y == right.y {
1063                    (right.x - left.x).max(px(1.))
1064                } else {
1065                    px(1.)
1066                },
1067                height,
1068            ),
1069        ))
1070    }
1071
1072    fn character_index_for_point(
1073        &mut self,
1074        point: gpui::Point<Pixels>,
1075        window: &mut Window,
1076        cx: &mut App,
1077    ) -> Option<usize> {
1078        let state = self.state.read(cx);
1079        let shown = displayed_value(state, self.input_type.masks());
1080        let offset = if self.multiline {
1081            char_at_point(
1082                &shown,
1083                point,
1084                self.paragraphs.read(cx),
1085                &self.font,
1086                crate::util::FIELD_TEXT,
1087                px(20.),
1088                window,
1089            )?
1090        } else {
1091            char_at_x(
1092                &shown,
1093                point.x - self.bounds.left(),
1094                &self.font,
1095                crate::util::FIELD_TEXT,
1096                window,
1097            )
1098        };
1099        Some(char_to_utf16(&state.value, offset))
1100    }
1101}
1102
1103/// The character Enter inserts in a multi-line field.
1104const NEWLINE: char = '\n';
1105
1106/// Everything the multi-line body needs to draw itself.
1107struct MultilineBody<'a> {
1108    value: &'a str,
1109    cursor: usize,
1110    selection: Option<(usize, usize)>,
1111    focused: bool,
1112    /// The caret's element id, height and colour.
1113    caret: (gpui::ElementId, Pixels, gpui::Hsla),
1114    selection_bg: gpui::Hsla,
1115    /// Where each paragraph was painted, filled in during layout so a click can
1116    /// be measured against the text it actually landed on.
1117    paragraphs: Entity<Vec<gpui::Bounds<Pixels>>>,
1118}
1119
1120/// One paragraph per newline, each wrapping, with the caret and selection
1121/// placed inside the paragraph they fall in.
1122///
1123/// gpui does wrap text — the default `WhiteSpace::Normal` — so a real
1124/// multi-line surface only needed the newlines split out and the caret located
1125/// within them. The single-line field opts out with `whitespace_nowrap`.
1126fn multiline_body(b: MultilineBody<'_>, cx: &App) -> gpui::AnyElement {
1127    let (caret_id, caret_h, caret_color) = b.caret;
1128    let mut col = gpui::div().flex().flex_col().items_start().w_full();
1129    // Char offset each line starts at, so the cursor and selection — which are
1130    // offsets into the whole value — can be mapped into it.
1131    let mut start = 0usize;
1132    let lines: Vec<&str> = b.value.split('\n').collect();
1133    let last = lines.len().saturating_sub(1);
1134    for (i, line) in lines.iter().enumerate() {
1135        let len = line.chars().count();
1136        let end = start + len;
1137        // `min_w_0` on the text spans is what lets each wrap inside the field
1138        // instead of pushing the row wider than its box.
1139        let mut para = gpui::div()
1140            .flex()
1141            .flex_wrap()
1142            .items_center()
1143            .w_full()
1144            .min_w_0();
1145
1146        // A zero-size probe at the head of the paragraph: `canvas` is the only
1147        // element told its own bounds, and a click in a wrapped paragraph has
1148        // to be measured against where that paragraph was actually laid out.
1149        para = para.child(
1150            gpui::canvas(
1151                {
1152                    let sink = b.paragraphs.clone();
1153                    move |bounds: gpui::Bounds<Pixels>, _window, cx| {
1154                        sink.update(cx, |slots, _| {
1155                            if slots.len() <= i {
1156                                slots.resize(i + 1, gpui::Bounds::default());
1157                            }
1158                            slots[i] = bounds;
1159                        });
1160                        bounds
1161                    }
1162                },
1163                |_, _, _, _| {},
1164            )
1165            .w_full()
1166            .h(px(0.))
1167            .flex_shrink_0(),
1168        );
1169
1170        // The selection, clipped to this line.
1171        let local_sel = b.selection.and_then(|(lo, hi)| {
1172            let lo = lo.max(start);
1173            let hi = hi.min(end);
1174            (lo < hi).then_some((lo - start, hi - start))
1175        });
1176
1177        if let Some((lo, hi)) = local_sel {
1178            let before: String = line.chars().take(lo).collect();
1179            let selected: String = line.chars().skip(lo).take(hi - lo).collect();
1180            let after: String = line.chars().skip(hi).collect();
1181            para = para
1182                .child(gpui::div().min_w_0().child(before))
1183                .child(
1184                    gpui::div()
1185                        .min_w_0()
1186                        .px(px(1.))
1187                        .rounded(px(4.))
1188                        .bg(b.selection_bg)
1189                        .child(selected),
1190                )
1191                .child(gpui::div().min_w_0().child(after));
1192        } else if b.selection.is_none() && b.cursor >= start && b.cursor <= end {
1193            let at = b.cursor - start;
1194            let before: String = line.chars().take(at).collect();
1195            let after: String = line.chars().skip(at).collect();
1196            para = para.child(gpui::div().min_w_0().child(before));
1197            if b.focused {
1198                para = para.child(crate::anim::caret_blink(
1199                    gpui::div()
1200                        .w(px(1.5))
1201                        .h(caret_h)
1202                        .bg(caret_color)
1203                        .flex_shrink_0(),
1204                    caret_id.clone(),
1205                    cx,
1206                ));
1207            }
1208            para = para.child(gpui::div().min_w_0().child(after));
1209        } else {
1210            para = para.child(gpui::div().w_full().min_w_0().child(line.to_string()));
1211        }
1212
1213        col = col.child(para);
1214        // +1 for the newline the split consumed, except after the last line.
1215        start = end + usize::from(i < last);
1216    }
1217    col.into_any_element()
1218}
1219
1220struct InputFieldRenderState {
1221    focus: crate::util::FieldFocus,
1222    is_disabled: bool,
1223    is_invalid: bool,
1224    is_read_only: bool,
1225    is_required: bool,
1226    value: SharedString,
1227}
1228
1229/// HeroUI Input.
1230#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
1231#[derive(IntoElement)]
1232pub struct Input {
1233    /// See [`Input::content`]: v3's field children-as-a-function.
1234    content: Option<std::sync::Arc<dyn Fn(crate::util::FieldFocus) -> gpui::AnyElement + 'static>>,
1235    field_content:
1236        Option<std::sync::Arc<dyn Fn(InputFieldRenderState) -> gpui::AnyElement + 'static>>,
1237    /// `validationBehavior` — written into the state on render.
1238    validation_behavior: Option<crate::form::ValidationBehavior>,
1239    state: Entity<InputState>,
1240    label: Option<SharedString>,
1241    /// An accessible name with no visible label; see [`Input::a11y_label`].
1242    a11y_label: Option<SharedString>,
1243    placeholder: Option<SharedString>,
1244    description: Option<SharedString>,
1245    error_message: Option<SharedString>,
1246    /// `validate` — run by the component, not the caller.
1247    validate: Option<crate::validation::Validator<str>>,
1248    /// `validationErrors` — messages from a server round-trip.
1249    validation_errors: Vec<SharedString>,
1250    variant: FieldVariant,
1251    variant_is_set: bool,
1252    input_type: InputType,
1253    max_length: Option<usize>,
1254    min_length: Option<usize>,
1255    /// `min` / `max` / `step` for the numeric types.
1256    min: Option<f64>,
1257    max: Option<f64>,
1258    step: Option<f64>,
1259    /// `pattern` — a predicate over the whole value.
1260    pattern: Option<std::sync::Arc<dyn Fn(&str) -> bool + 'static>>,
1261    start_content: Option<gpui::AnyElement>,
1262    end_content: Option<gpui::AnyElement>,
1263    /// Stretch beyond the 320px default demo width.
1264    /// Multi-line only: the height `rows` asks for. `None` leaves v3's
1265    /// `min-height: 38px`.
1266    min_h: Option<Pixels>,
1267    /// [`Input::height`] — the single-line box height. `None` keeps
1268    /// `util::FIELD_HEIGHT`; the multi-line path ignores it (see the builder).
1269    height: Option<Pixels>,
1270    /// [`Input::padding_x`] — the standalone box's horizontal padding. `None`
1271    /// keeps v3's `px-3`. Ignored inside a group, whose addon rules own the
1272    /// sides.
1273    padding_x: Option<Pixels>,
1274    /// The group owner's padding override (`InputGroup::padding_x`,
1275    /// `NumberField::padding_x`). Crate-internal: it is not the public
1276    /// `Input::padding_x`, whose grouped behavior stays as documented.
1277    group_padding_x: Option<Pixels>,
1278    /// [`Input::is_bare`] — render the standalone box with no chrome at all,
1279    /// the way `InputGroup.Input` already does.
1280    is_bare: bool,
1281    is_bare_is_set: bool,
1282    /// Whether focused state paints the visual focus ring. `None` lets the
1283    /// active `TextFieldStyle` choose; unset everywhere keeps the stock ring.
1284    focus_ring: Option<bool>,
1285    /// [`Input::text_size`] — the field value and placeholder type size.
1286    text_size: Option<Pixels>,
1287    /// [`Input::font_family`] — the family the field text is drawn and
1288    /// measured with. `None` inherits the window's text style.
1289    font_family: Option<SharedString>,
1290    /// The corner radius, in place of the owning `field_radius` helper.
1291    radius: Option<Pixels>,
1292    /// Set by [`crate::input_group::InputGroup`]: `(has_prefix, has_suffix)`.
1293    /// `InputGroup.Input` has no chrome of its own -- the group paints it -- and
1294    /// drops the padding on whichever side touches an addon (`ps-0`/`pe-0`).
1295    in_group: Option<(bool, bool)>,
1296    /// Set by [`crate::input_group::InputGroup`]: the group box paints the one
1297    /// `status-disabled` dim over the whole row, so the field must not nest a
1298    /// second opacity inside it.
1299    group_dim: bool,
1300    full_width: bool,
1301    is_disabled: bool,
1302    is_read_only: bool,
1303    is_required: bool,
1304    is_invalid: bool,
1305    /// `autoFocus` — take focus on the first render.
1306    auto_focus: bool,
1307    /// `name` — the submission name, written into the state on render.
1308    name: Option<SharedString>,
1309    /// Set by `TextArea`: wrap the text, lay lines out top-down, and let Enter
1310    /// insert a newline instead of submitting.
1311    multiline: bool,
1312    /// `defaultValue` — seeds the state on the first render only.
1313    default_value: Option<SharedString>,
1314    /// `value` — the controlled spelling; seeds the state on the first render
1315    /// only, ahead of `default_value`. See [`Input::value`].
1316    value: Option<SharedString>,
1317    is_clearable: bool,
1318    clear_content: Option<gpui::AnyElement>,
1319    /// The fill the clear button takes on hover, in place of
1320    /// `--default-hover`.
1321    clear_hover_bg: Option<gpui::Hsla>,
1322    /// SearchField-only: Escape clears a non-empty query.
1323    clear_on_escape: bool,
1324    /// SearchField-only: the clear affordance and Escape report this action.
1325    on_clear: Option<ClearCallback>,
1326    on_change: Option<TextCallback>,
1327    on_submit: Option<TextCallback>,
1328    /// ComboBox popup anchor: when set, the field row (not the
1329    /// label-to-error wrapper) records its bounds into `anchor` and carries
1330    /// `selector` for headless probes — the way RAC's `triggerRef` reads
1331    /// `groupRef.current || inputRef.current` (pinned 1.20.0
1332    /// `dist/private/ComboBox.js`: "Position popover relative to group if
1333    /// available, otherwise input").
1334    field_anchor: Option<(
1335        std::rc::Rc<std::cell::Cell<Option<gpui::Bounds<Pixels>>>>,
1336        SharedString,
1337    )>,
1338    /// The `sx` slot, refined over the root style at the end of render.
1339    sx: Option<Box<gpui::StyleRefinement>>,
1340    recipes: Vec<SharedString>,
1341}
1342
1343impl Input {
1344    /// The bound state, so wrappers can write through to it.
1345    pub fn state(&self) -> &Entity<InputState> {
1346        &self.state
1347    }
1348
1349    /// The builder's `is_disabled` flag, read without a rendered frame.
1350    ///
1351    /// `InputGroup` needs it before [`Input::render`] has run: the state's
1352    /// success mirror is last frame's trace, and the first frame has none, so
1353    /// a field disabled while its group is enabled would take an addon click's
1354    /// focus once before the mirror caught up.
1355    pub(crate) fn builder_is_disabled(&self) -> bool {
1356        self.is_disabled
1357    }
1358
1359    /// v3's field `children`-as-a-function, handed `{isFocused, isFocusWithin,
1360    /// isFocusVisible}`.
1361    ///
1362    /// v3's caller writes the parts themselves inside that function -- a
1363    /// `Label`, the group, a `Description` -- and this port exposes the same
1364    /// three as components, so a closure here replaces the field's own stack
1365    /// with whatever the caller builds from the state.
1366    pub fn content(
1367        mut self,
1368        render: impl Fn(crate::util::FieldFocus) -> gpui::AnyElement + 'static,
1369    ) -> Self {
1370        self.content = Some(std::sync::Arc::new(render));
1371        self
1372    }
1373
1374    fn field_content(
1375        mut self,
1376        render: impl Fn(InputFieldRenderState) -> gpui::AnyElement + 'static,
1377    ) -> Self {
1378        self.field_content = Some(std::sync::Arc::new(render));
1379        self
1380    }
1381
1382    /// `value` — v3's controlled-value spelling, as a pure builder.
1383    ///
1384    /// The bound [`InputState`] owns the text once the field renders, so this
1385    /// seeds the state on the first render only, winning over
1386    /// [`Input::default_value`] the way v3's controlled prop outranks the
1387    /// uncontrolled seed; calling `.value(..)` twice keeps the last call, like
1388    /// every other builder here. A later value is an imperative update rather
1389    /// than a builder: `state.update(cx, |s, _| s.set_value(..))`.
1390    pub fn value(mut self, value: impl Into<SharedString>) -> Self {
1391        self.value = Some(value.into());
1392        self
1393    }
1394
1395    /// Builds the field over `state`.
1396    ///
1397    /// The handle may be borrowed — `Input::new(&handle)` — so a caller that
1398    /// keeps its own handle clones nothing at the call site; an owned
1399    /// `Entity<InputState>` still works, and the cheap handle clone happens
1400    /// once inside either way.
1401    pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
1402        Self {
1403            content: None,
1404            field_content: None,
1405            validation_behavior: None,
1406            state: state.borrow().clone(),
1407            label: None,
1408            a11y_label: None,
1409            placeholder: None,
1410            description: None,
1411            error_message: None,
1412            validate: None,
1413            validation_errors: Vec::new(),
1414            variant: FieldVariant::Primary,
1415            variant_is_set: false,
1416            input_type: InputType::Text,
1417            max_length: None,
1418            min_length: None,
1419            min: None,
1420            max: None,
1421            step: None,
1422            pattern: None,
1423            start_content: None,
1424            end_content: None,
1425            min_h: None,
1426            height: None,
1427            padding_x: None,
1428            group_padding_x: None,
1429            is_bare: false,
1430            is_bare_is_set: false,
1431            focus_ring: None,
1432            text_size: None,
1433            font_family: None,
1434            radius: None,
1435            in_group: None,
1436            group_dim: false,
1437            full_width: false,
1438            is_disabled: false,
1439            is_read_only: false,
1440            is_required: false,
1441            is_invalid: false,
1442            auto_focus: false,
1443            name: None,
1444            default_value: None,
1445            value: None,
1446            multiline: false,
1447            is_clearable: false,
1448            clear_content: None,
1449            clear_hover_bg: None,
1450            clear_on_escape: false,
1451            on_clear: None,
1452            on_change: None,
1453            on_submit: None,
1454            field_anchor: None,
1455            sx: None,
1456            recipes: Vec::new(),
1457        }
1458    }
1459
1460    /// `validationBehavior` — `Allow` shows the message without blocking form
1461    /// submission.
1462    ///
1463    /// Stored on the state beside `name`, because the form reads both from
1464    /// there.
1465    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
1466        self.validation_behavior = Some(behavior);
1467        self
1468    }
1469
1470    /// Turns this into the multi-line surface `TextArea` renders.
1471    ///
1472    /// Not a v3 prop: v3 has a separate `<textarea>`, and this is the flag that
1473    /// switches one implementation between the two.
1474    pub(crate) fn multiline(mut self, v: bool) -> Self {
1475        self.multiline = v;
1476        self
1477    }
1478
1479    /// Multi-line only: the height `TextArea::rows` asks for.
1480    pub(crate) fn min_h(mut self, h: Pixels) -> Self {
1481        self.min_h = Some(h);
1482        self
1483    }
1484
1485    /// Renders as v3's `InputGroup.Input`: transparent, unrounded, unshadowed,
1486    /// and flush against whichever addons surround it.
1487    pub(crate) fn in_group(mut self, has_prefix: bool, has_suffix: bool) -> Self {
1488        self.in_group = Some((has_prefix, has_suffix));
1489        self
1490    }
1491
1492    /// The surrounding group box already carries the disabled dim; skip this
1493    /// field's own so the opacity does not nest twice.
1494    pub(crate) fn group_dim(mut self, v: bool) -> Self {
1495        self.group_dim = v;
1496        self
1497    }
1498
1499    /// ComboBox popup anchor: records the 36px field row's bounds (not the
1500    /// label-to-error wrapper) and tags that row with `selector` for headless
1501    /// probes — the way RAC's `triggerRef` reads `groupRef.current ||
1502    /// inputRef.current`.
1503    pub(crate) fn field_anchor(
1504        mut self,
1505        anchor: std::rc::Rc<std::cell::Cell<Option<gpui::Bounds<Pixels>>>>,
1506        selector: impl Into<SharedString>,
1507    ) -> Self {
1508        self.field_anchor = Some((anchor, selector.into()));
1509        self
1510    }
1511
1512    /// `name` — the name this field submits under.
1513    ///
1514    /// Stored on the [`InputState`], because gpui gives a child no way to reach
1515    /// its `Form`; `FormField::text(state)` reads it back out, so the name is
1516    /// written once here and not repeated at the form.
1517    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
1518        self.name = Some(name.into());
1519        self
1520    }
1521
1522    /// `defaultValue` — the uncontrolled initial text.
1523    ///
1524    /// Written into the state on the first render only, so it seeds the field
1525    /// without overwriting what the user types. `InputState::with_value` does
1526    /// the same at construction; this is the prop spelling, for a state the
1527    /// caller made without one.
1528    pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
1529        self.default_value = Some(text.into());
1530        self
1531    }
1532
1533    /// Sets the visible label.
1534    pub fn label(mut self, l: impl Into<SharedString>) -> Self {
1535        self.label = Some(l.into());
1536        self
1537    }
1538
1539    /// The accessible name for a field whose visible label belongs to a
1540    /// composite owner.
1541    ///
1542    /// `NumberField` renders `field::Label` itself, as v3 composes it as a
1543    /// sibling of `NumberField.Group`, so handing the inner `Input` the label
1544    /// through [`Self::label`] would draw it twice. Upstream still passes
1545    /// `label` down to `useFormattedTextField`, which is where the input's
1546    /// accessible name comes from, so this carries the name without the box.
1547    pub(crate) fn a11y_label(mut self, l: impl Into<SharedString>) -> Self {
1548        self.a11y_label = Some(l.into());
1549        self
1550    }
1551
1552    /// Sets the placeholder shown while the value is empty.
1553    pub fn placeholder(mut self, p: impl Into<SharedString>) -> Self {
1554        self.placeholder = Some(p.into());
1555        self
1556    }
1557
1558    /// Sets the description text.
1559    pub fn description(mut self, d: impl Into<SharedString>) -> Self {
1560        self.description = Some(d.into());
1561        self
1562    }
1563
1564    /// `autoFocus` — take focus on the first render.
1565    pub fn auto_focus(mut self, v: bool) -> Self {
1566        self.auto_focus = v;
1567        self
1568    }
1569
1570    /// `validate` — returns the message to show, or `None` when the text is fine.
1571    ///
1572    /// The component runs it and surfaces the result, so a caller does not have
1573    /// to mirror the logic into `is_invalid` / `error_message`.
1574    pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
1575        self.validate = Some(std::sync::Arc::new(f));
1576        self
1577    }
1578
1579    /// `validationErrors` — messages produced elsewhere, shown ahead of
1580    /// whatever `validate` returns.
1581    pub fn validation_errors(
1582        mut self,
1583        errors: impl IntoIterator<Item = impl Into<SharedString>>,
1584    ) -> Self {
1585        self.validation_errors = errors.into_iter().map(Into::into).collect();
1586        self
1587    }
1588
1589    /// Sets the error message shown when the field is invalid.
1590    pub fn error_message(mut self, e: impl Into<SharedString>) -> Self {
1591        self.error_message = Some(e.into());
1592        self
1593    }
1594
1595    /// The `type` attribute — `password` masks, `number` filters keystrokes.
1596    pub fn input_type(mut self, input_type: InputType) -> Self {
1597        self.input_type = input_type;
1598        self
1599    }
1600
1601    /// `maxLength` — refuses keystrokes past this many characters.
1602    pub fn max_length(mut self, n: usize) -> Self {
1603        self.max_length = Some(n);
1604        self
1605    }
1606
1607    /// `minLength` — reported by [`Input::validity`], not enforced while typing.
1608    pub fn min_length(mut self, n: usize) -> Self {
1609        self.min_length = Some(n);
1610        self
1611    }
1612
1613    /// `min` — the lowest accepted value for the numeric types.
1614    pub fn min(mut self, v: f64) -> Self {
1615        self.min = Some(v);
1616        self
1617    }
1618
1619    /// `max` — the highest accepted value for the numeric types.
1620    pub fn max(mut self, v: f64) -> Self {
1621        self.max = Some(v);
1622        self
1623    }
1624
1625    /// `step` — the numeric granularity, measured from `min` (or 0).
1626    pub fn step(mut self, v: f64) -> Self {
1627        self.step = Some(v);
1628        self
1629    }
1630
1631    /// `pattern` — a predicate the whole value must satisfy.
1632    pub fn pattern(mut self, f: impl Fn(&str) -> bool + 'static) -> Self {
1633        self.pattern = Some(std::sync::Arc::new(f));
1634        self
1635    }
1636
1637    /// Checks `value` against this field's constraints.
1638    ///
1639    /// Callers own their validation lifecycle, so this is a pure query — pass
1640    /// the outcome back through `is_invalid` / `error_message`.
1641    pub fn validity(&self, value: &str) -> InputValidity {
1642        validate_value(
1643            value,
1644            self.input_type,
1645            self.min_length,
1646            self.min,
1647            self.max,
1648            self.step,
1649            self.pattern.as_deref(),
1650        )
1651    }
1652
1653    /// Sets the field variant.
1654    pub fn variant(mut self, v: FieldVariant) -> Self {
1655        self.variant = v;
1656        self.variant_is_set = true;
1657        self
1658    }
1659
1660    /// Named theme overlay from [`herogpui_theme::ComponentThemes::text_field`].
1661    /// Stackable; a missing name adds no override.
1662    pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
1663        self.recipes.push(name.into());
1664        self
1665    }
1666
1667    pub(crate) fn with_recipes(mut self, recipes: Vec<SharedString>) -> Self {
1668        self.recipes = recipes;
1669        self
1670    }
1671
1672    /// The height of the single-line field box, replacing v3's 36px
1673    /// (`util::FIELD_HEIGHT`).
1674    ///
1675    /// Only the row height changes: the text keeps its 14px size and 20px
1676    /// line height and stays vertically centred, so a shorter box is a
1677    /// tighter box rather than smaller type. The multi-line surface
1678    /// (`TextArea`) ignores this — its height is content-driven with a
1679    /// `rows`-derived floor, and a fixed height there would either clip the
1680    /// text or fight `rows`; use `TextArea::rows` for that.
1681    pub fn height(mut self, h: impl Into<Pixels>) -> Self {
1682        self.height = Some(h.into());
1683        self
1684    }
1685
1686    /// The horizontal padding of the standalone field box, replacing v3's
1687    /// `px-3` (12px).
1688    ///
1689    /// Inside an [`crate::input_group::InputGroup`] the padding is the group's
1690    /// rule — the side that touches an addon carries none — so this is ignored
1691    /// there.
1692    pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
1693        self.padding_x = Some(p.into());
1694        self
1695    }
1696
1697    /// The group owner's padding override, applied to every side without an
1698    /// addon. Crate-internal: [`Self::padding_x`] is the public, standalone
1699    /// spelling, and it remains ignored inside a group.
1700    pub(crate) fn group_padding_x(mut self, padding_x: impl Into<Pixels>) -> Self {
1701        self.group_padding_x = Some(padding_x.into());
1702        self
1703    }
1704
1705    /// Applies a standalone [`crate::util::FieldBox`] to this field: explicit
1706    /// height and padding, plus the chrome-less bare mode. Crate-internal for
1707    /// the components that forward their box seam to a held `Input`.
1708    /// Hands a wrapper's captured `sx` refinement to this field, so it lands on
1709    /// the same root [`Input::sx`] would (a `ColorField` composing this field
1710    /// has no element of its own to refine).
1711    pub(crate) fn with_sx_refinement(mut self, sx: Option<Box<gpui::StyleRefinement>>) -> Self {
1712        if sx.is_some() {
1713            self.sx = sx;
1714        }
1715        self
1716    }
1717
1718    pub(crate) fn with_field_box(mut self, field: crate::util::FieldBox) -> Self {
1719        if let Some(height) = field.height {
1720            self = self.height(height);
1721        }
1722        if let Some(padding_x) = field.padding_x {
1723            self = self.padding_x(padding_x);
1724        }
1725        if field.is_bare_is_set {
1726            self = self.is_bare(field.is_bare);
1727        }
1728        if let Some(focus_ring) = field.focus_ring {
1729            self = self.focus_ring(focus_ring);
1730        }
1731        self
1732    }
1733
1734    /// Drops the field's own chrome: no background, no border, no field
1735    /// shadow, and no focus or invalid ring.
1736    ///
1737    /// This is exactly the treatment `InputGroup.Input` already gets (v3's
1738    /// `.input-group__input` is `border-0 bg-transparent shadow-none`), for a
1739    /// field the caller paints around — a toolbar, a table cell, an editable
1740    /// label. Everything else, including focus itself and the invalid state
1741    /// the error line reports, is unchanged.
1742    pub fn is_bare(mut self, v: bool) -> Self {
1743        self.is_bare = v;
1744        self.is_bare_is_set = true;
1745        self
1746    }
1747
1748    /// Shows or hides only the field's visual focus ring.
1749    ///
1750    /// `false` keeps the control focusable and editable and keeps invalid
1751    /// feedback, but suppresses the accent ring painted while it is focused.
1752    /// Use [`Input::is_bare`] when the surrounding caller should own all field
1753    /// chrome instead. The default is `true`, or the value supplied by the
1754    /// active [`herogpui_theme::TextFieldStyle`].
1755    pub fn focus_ring(mut self, v: bool) -> Self {
1756        self.focus_ring = Some(v);
1757        self
1758    }
1759
1760    /// The field value and placeholder type size, replacing v3's `text-sm`.
1761    pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
1762        self.text_size = Some(size.into());
1763        self
1764    }
1765
1766    /// The font family the field's value, placeholder and caret measurement
1767    /// use — a monospace face for a code or token field.
1768    ///
1769    /// The field's own text layout captures the font during `render`, before
1770    /// any element's text style is pushed onto the window's stack, so the
1771    /// family is threaded into that captured font here as well as set on the
1772    /// box for the text gpui itself shapes.
1773    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
1774        self.font_family = Some(family.into());
1775        self
1776    }
1777
1778    /// The corner radius, in place of the owning `field_radius` helper. Not a
1779    /// v3 prop; the removed v2 `radius` prop is prohibited and this is a
1780    /// per-component repository extension.
1781    ///
1782    /// The box and shared field chrome use the same resolved radius. Bare
1783    /// and grouped fields retain it without painting their own chrome.
1784    pub fn radius(mut self, radius: impl Into<Pixels>) -> Self {
1785        self.radius = Some(radius.into());
1786        self
1787    }
1788
1789    /// Sets the content rendered before the text.
1790    pub fn start_content(mut self, el: impl IntoElement) -> Self {
1791        self.start_content = Some(el.into_any_element());
1792        self
1793    }
1794
1795    /// Sets the content rendered after the text.
1796    pub fn end_content(mut self, el: impl IntoElement) -> Self {
1797        self.end_content = Some(el.into_any_element());
1798        self
1799    }
1800
1801    /// Fills the parent instead of the 320px default demo width.
1802    pub fn full_width(mut self) -> Self {
1803        self.full_width = true;
1804        self
1805    }
1806
1807    /// Sets whether the field is disabled.
1808    pub fn is_disabled(mut self, v: bool) -> Self {
1809        self.is_disabled = v;
1810        self
1811    }
1812
1813    /// Sets whether the field is read-only.
1814    pub fn is_read_only(mut self, v: bool) -> Self {
1815        self.is_read_only = v;
1816        self
1817    }
1818
1819    /// Sets whether the field is required.
1820    pub fn is_required(mut self, v: bool) -> Self {
1821        self.is_required = v;
1822        self
1823    }
1824
1825    /// Sets whether the field is invalid.
1826    pub fn is_invalid(mut self, v: bool) -> Self {
1827        self.is_invalid = v;
1828        self
1829    }
1830
1831    /// The fill the clear button takes on hover, in place of
1832    /// `--default-hover`.
1833    pub fn clear_hover_bg(mut self, color: impl Into<gpui::Hsla>) -> Self {
1834        self.clear_hover_bg = Some(color.into());
1835        self
1836    }
1837
1838    /// Shows a clear button when there is a value.
1839    pub fn is_clearable(mut self, v: bool) -> Self {
1840        self.is_clearable = v;
1841        self
1842    }
1843
1844    fn clear_content(mut self, content: impl IntoElement) -> Self {
1845        self.clear_content = Some(content.into_any_element());
1846        self
1847    }
1848
1849    /// SearchField's Escape shortcut, kept internal because plain Input has no
1850    /// clear-on-Escape contract.
1851    pub(crate) fn clear_on_escape(mut self, v: bool) -> Self {
1852        self.clear_on_escape = v;
1853        self
1854    }
1855
1856    /// SearchField's dedicated clear action, separate from an edit that merely
1857    /// produces an empty value.
1858    pub(crate) fn on_clear(mut self, f: ClearCallback) -> Self {
1859        self.on_clear = Some(f);
1860        self
1861    }
1862
1863    /// Sets the handler called with the new value whenever it changes.
1864    pub fn on_change(mut self, f: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
1865        self.on_change = Some(std::sync::Arc::new(f));
1866        self
1867    }
1868
1869    /// Sets the handler called with the current value when Enter is pressed.
1870    pub fn on_submit(mut self, f: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
1871        self.on_submit = Some(std::sync::Arc::new(f));
1872        self
1873    }
1874
1875    /// The one slot for caller-owned low-level styling: GPUI's styling methods
1876    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
1877    /// applied to the input's root element after every value the variant and
1878    /// the active theme chose, so they win. The root is the label-to-error
1879    /// column a standalone field returns; inside an `InputGroup` it is the
1880    /// field row itself, which is all the group leaves the field.
1881    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
1882        crate::util::refine_sx(&mut self.sx, style);
1883        self
1884    }
1885}
1886
1887impl Input {
1888    /// The focus handle of the state this field is bound to.
1889    ///
1890    /// `InputGroup` rings on `focus-within`, and the only thing inside it that
1891    /// can hold a focus is this field.
1892    pub(crate) fn state_focus(&self, cx: &App) -> FocusHandle {
1893        self.state.read(cx).focus_handle.clone()
1894    }
1895
1896    /// The captured [`Input::sx`] refinement, handed through by wrappers that
1897    /// hold their `sx` apart from the field — [`SearchField`] builds its
1898    /// `Input` at render time, so its slot travels as the refinement itself.
1899    pub(crate) fn sx_refinement(mut self, sx: Option<Box<gpui::StyleRefinement>>) -> Self {
1900        self.sx = sx;
1901        self
1902    }
1903}
1904
1905impl RenderOnce for Input {
1906    fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
1907        // One structured id per rendered field, and every part of the field
1908        // hangs off it. The state entity is what makes it unique, and
1909        // `named_usize` keeps it as structure, so a part's name can never
1910        // fold into the base the way a formatted `"input-{n}-part"` string
1911        // can.
1912        let base_id =
1913            gpui::ElementId::named_usize("input", self.state.entity_id().as_u64() as usize);
1914        // `validationBehavior` travels with the name, on the state.
1915        if let Some(behavior) = self.validation_behavior {
1916            let state = self.state.clone();
1917            if state.read(cx).validation_behavior() != behavior {
1918                state.update(cx, |s, _| s.set_validation_behavior(behavior));
1919            }
1920        }
1921        // `focus_once` takes `cx` mutably, so it has to run before the theme
1922        // tokens are borrowed.
1923        // `value` / `defaultValue` seed the state once, before anything reads
1924        // it. `value` is v3's controlled spelling, so it outranks the
1925        // uncontrolled seed; the state owns the text afterwards, and
1926        // `InputState::set_value` is the imperative update.
1927        let seed = self.value.clone().or(self.default_value.clone());
1928        if let Some(text) = seed {
1929            let state = self.state.clone();
1930            crate::util::seed_once(
1931                window,
1932                cx,
1933                element_id::scoped(&base_id, "default"),
1934                move |cx| {
1935                    state.update(cx, |s, cx| {
1936                        s.set_value(text.to_string());
1937                        cx.notify();
1938                    });
1939                },
1940            );
1941        }
1942        // `name` lives on the state so the form can find it. Only write when
1943        // it differs, so this does not loop through `notify`.
1944        if self.state.read(cx).name() != self.name {
1945            let name = self.name.clone();
1946            self.state.update(cx, |s, _| s.set_name(name));
1947        }
1948        let is_successful = !self.is_disabled;
1949        if self.state.read(cx).is_successful() != is_successful {
1950            self.state
1951                .update(cx, |s, _| s.set_successful(is_successful));
1952        }
1953        // The read-only mirror: the form's constraint-validation bar reads
1954        // the rendered flag, the way it reads `name` and the resolved
1955        // validity off this state.
1956        if self.state.read(cx).is_read_only() != self.is_read_only {
1957            let read_only = self.is_read_only;
1958            self.state.update(cx, |s, _| s.set_read_only(read_only));
1959        }
1960        // v3 order: the controlled flag, then server errors, then `validate`,
1961        // with `errorMessage` as the fallback. Resolved here, ahead of the
1962        // theme borrow, because the write below needs `&mut cx`. The server
1963        // slot is two sources: the messages the `Form`'s `validationErrors`
1964        // record routed into this state by name, then the field's own
1965        // `validationErrors` prop.
1966        let value_now = self.state.read(cx).value().to_owned();
1967        let mut server_errors = self.state.read(cx).routed_errors().to_vec();
1968        server_errors.extend(self.validation_errors.iter().cloned());
1969        let mut validity = crate::validation::resolve(
1970            self.is_invalid,
1971            &server_errors,
1972            self.validate.as_ref().and_then(|f| f(&value_now)),
1973            self.error_message.clone(),
1974        );
1975        // v3's native validation enforces the HTML5 attribute constraints too:
1976        // a `minLength`/`pattern`/`min`/`max`/`step` violation is a field error
1977        // even when the controlled flags and `validate` say nothing, and a
1978        // native form must block on it. The merge touches only the flag — the
1979        // message slot stays untouched, so no error line appears.
1980        if !self.validity(&value_now).is_valid() {
1981            validity.is_invalid = true;
1982        }
1983        // The form reads this back through `FormField::text`; written only
1984        // when it differs, so the write cannot notify-re-render forever
1985        // (`set_name` guards its state write the same way).
1986        let validity_state = self.state.clone();
1987        if validity_state.read(cx).validity() != &validity {
1988            validity_state.update(cx, |s, _| s.set_validity(validity.clone()));
1989        }
1990        let focus_handle = self.state.read(cx).focus_handle.clone();
1991        if self.auto_focus {
1992            crate::util::focus_once(
1993                window,
1994                cx,
1995                element_id::scoped(&base_id, "autofocus"),
1996                &focus_handle,
1997            );
1998        }
1999        // Where the text starts, remembered from the last frame: a `canvas` is
2000        // the only element that is told its own bounds, and a click has to be
2001        // measured against something. `use_keyed_state` takes `cx` mutably, so
2002        // it precedes the theme.
2003        let text_origin =
2004            window.use_keyed_state(element_id::scoped(&base_id, "text-origin"), cx, |_, _| {
2005                None::<Pixels>
2006            });
2007        // The same trick for a wrapped body, one entry per paragraph.
2008        let paragraph_bounds =
2009            window.use_keyed_state(element_id::scoped(&base_id, "paragraphs"), cx, |_, _| {
2010                Vec::<gpui::Bounds<Pixels>>::new()
2011            });
2012        // The font the field draws with, captured here: at event time the text
2013        // style stack is empty and the shaping would use the wrong face.
2014        // A `font_family` refinement on the box below is pushed onto the
2015        // window's text-style stack only while that element prepaints, which
2016        // is after this `render` has already built the tree — verified against
2017        // pinned gpui-pre 0.3.3 (`Window::text_style` folds
2018        // `text_style_stack`, pushed in `with_text_style`). So the family is
2019        // threaded into the captured font explicitly.
2020        let mut text_font = window.text_style().font();
2021        if let Some(family) = &self.font_family {
2022            text_font.family = family.clone();
2023        }
2024        let disabled_opacity = cx.layout().disabled_opacity;
2025        let focused = focus_handle.is_focused(window);
2026        if !focused && self.state.read(cx).marked.is_some() {
2027            self.state.update(cx, |state, _| state.marked = None);
2028        }
2029        // The shared hover tween below borrows the app mutably; own the theme
2030        // snapshot so the remaining field labels and clear affordance can use
2031        // the same resolved tokens after the animation is installed.
2032        let colors = cx.colors().clone();
2033        let field_theme = cx.theme().components.text_field.resolve(&self.recipes);
2034        if !self.variant_is_set {
2035            if let Some(variant) = field_theme.variant {
2036                self.variant = variant;
2037            }
2038        }
2039        if !self.is_bare_is_set {
2040            if let Some(is_bare) = field_theme.is_bare {
2041                self.is_bare = is_bare;
2042            }
2043        }
2044        if self.focus_ring.is_none() {
2045            self.focus_ring = field_theme.focus_ring;
2046        }
2047        self.height = self.height.or(field_theme.height);
2048        self.padding_x = self.padding_x.or(field_theme.padding_x);
2049        self.radius = self.radius.or(field_theme.radius);
2050        let theme_background = field_theme.background.map(|color| color.resolve(&colors));
2051        let theme_foreground = field_theme.foreground.map(|color| color.resolve(&colors));
2052        let theme_placeholder = field_theme
2053            .placeholder
2054            .map_or(colors.muted, |color| color.resolve(&colors));
2055        let accent = colors.accent;
2056        let field_focus = crate::util::FieldFocus {
2057            is_focused: focused,
2058            is_focus_within: focus_handle.contains_focused(window, cx),
2059            is_focus_visible: focused && crate::util::focus_visible(cx),
2060        };
2061
2062        // v3's field children-as-a-function: the caller builds the parts from the
2063        // focus state, so the field's own stack is skipped entirely.
2064        if let Some(render) = self.field_content.clone() {
2065            return render(InputFieldRenderState {
2066                focus: field_focus,
2067                is_disabled: self.is_disabled,
2068                is_invalid: validity.is_invalid,
2069                is_read_only: self.is_read_only,
2070                is_required: self.is_required,
2071                value: value_now.into(),
2072            });
2073        }
2074        if let Some(render) = self.content.clone() {
2075            return render(field_focus);
2076        }
2077
2078        // Every v3 field is one box: `.input` is `px-3 py-2 text-sm`, which is
2079        // 36px tall, and its siblings say so outright (`.input-group` and
2080        // `.search-field__group` are `min-h-9`, `.number-field__group` is `h-9`).
2081        // This was 40, so every field in the port stood 4px taller than v3's.
2082        // `Input::height` replaces the row height and nothing else: the text
2083        // keeps `FIELD_TEXT` and its 20px line, centred in whatever box the
2084        // caller asked for.
2085        let (h, text) = (crate::util::FIELD_HEIGHT, crate::util::FIELD_TEXT);
2086        let h = self.height.unwrap_or(h);
2087        let text = self.text_size.or(field_theme.text_size).unwrap_or(text);
2088
2089        let is_invalid = validity.is_invalid;
2090        let show_focus_ring = self.focus_ring.unwrap_or(true);
2091        let _border_color = if is_invalid {
2092            colors.danger.color
2093        } else if focused {
2094            colors.focus
2095        } else {
2096            colors.border
2097        };
2098        let multiline = self.multiline;
2099        // `.textarea` is `py-2` over a `min-height: 38px`; `rows` raises
2100        // that floor. Without this the field ignored `rows`, so every
2101        // TextArea came out one line tall inside a taller wrapper.
2102        let multiline_h = self.min_h.unwrap_or(px(38.));
2103        // The field box is the `<input>` (or `<textarea>`) upstream renders:
2104        // its role follows `type`, except that a multi-line field is a
2105        // `<textarea>`, whose role is the multiline one. The name and the
2106        // description come from the field anatomy, joined the way
2107        // `useField` joins the description and error ids.
2108        let a11y_role = if multiline {
2109            crate::a11y::Role::MultilineTextInput
2110        } else {
2111            self.input_type.a11y_role()
2112        };
2113        let a11y_name = crate::a11y::Name::field(
2114            self.label.as_ref().or(self.a11y_label.as_ref()),
2115            self.description.as_ref(),
2116            &validity,
2117        );
2118        // The box and shared field chrome use the same resolved radius.
2119        let radius = self.radius.unwrap_or_else(|| crate::util::field_radius(cx));
2120        let mut field = gpui::div()
2121            .id(base_id.clone())
2122            .a11y_named(a11y_role, &a11y_name)
2123            .a11y_text(&value_now, self.placeholder.as_ref())
2124            .flex()
2125            // Multi-line: the text starts at the top, the box grows downward
2126            // with the content, and the width is fixed so lines can wrap.
2127            .map(|f| {
2128                if multiline {
2129                    f.items_start()
2130                        .min_h(multiline_h)
2131                        .py(px(8.))
2132                        .w_full()
2133                        .overflow_hidden()
2134                } else {
2135                    f.items_center().h(h)
2136                }
2137            })
2138            .gap(px(8.))
2139            // `.input-group__input` keeps `px-3` except on a side that touches
2140            // an addon, which carries the padding instead.
2141            .map(|f| {
2142                // Only the group owner's override reaches a grouped field;
2143                // the public `Input::padding_x` keeps its documented
2144                // grouped-ignored behavior, and unset means `px-3`.
2145                let padding_x = self.group_padding_x.unwrap_or(px(12.));
2146                match self.in_group {
2147                    None => f.px(px(12.)),
2148                    Some((prefix, suffix)) => f
2149                        .flex_1()
2150                        .pl(if prefix { px(0.) } else { padding_x })
2151                        .pr(if suffix { px(0.) } else { padding_x }),
2152                }
2153            })
2154            // `Input::padding_x` replaces the standalone `px-3`; a grouped
2155            // field keeps the addon rules above.
2156            .when_some(
2157                self.padding_x.filter(|_| self.in_group.is_none()),
2158                |f, padding_x| f.px(padding_x),
2159            )
2160            .text_size(text)
2161            .line_height(px(20.))
2162            .when_some(self.font_family.clone(), |f, family| f.font_family(family))
2163            // A grouped input is the transparent middle of the shared
2164            // `.input-group` shell. HeroUI's `.input-group__input` is
2165            // `rounded-none`; letting the inner field keep its field radius
2166            // makes custom fills and focus overlays expose a second set of
2167            // corners inside the group's single rounded outline.
2168            .when(self.in_group.is_none(), |f| f.rounded(radius))
2169            .when(!self.is_disabled, |e| {
2170                e.cursor(gpui::CursorStyle::IBeam)
2171                    .track_focus(&focus_handle)
2172                    .key_context("Input")
2173                    // A click puts the caret where it landed, and a drag with
2174                    // the button down selects -- what a text field does, and
2175                    // what this one did not: the caret stayed wherever the
2176                    // value had left it, so the middle of a word was
2177                    // unreachable with the mouse.
2178                    .on_mouse_down(gpui::MouseButton::Left, {
2179                        let fh = focus_handle.clone();
2180                        let st = self.state.clone();
2181                        let origin = text_origin.clone();
2182                        let font = text_font.clone();
2183                        let masks = self.input_type.masks();
2184                        let multiline = self.multiline;
2185                        let paras = paragraph_bounds.clone();
2186                        // Hit testing follows the text row advance, not the caret glyph height.
2187                        let line_h = px(20.);
2188                        let size = text;
2189                        move |ev: &gpui::MouseDownEvent, window, cx| {
2190                            window.focus(&fh, cx);
2191                            let shown = displayed_value(st.read(cx), masks);
2192                            let at = if multiline {
2193                                let boxes = paras.read(cx).clone();
2194                                let Some(at) = char_at_point(
2195                                    &shown,
2196                                    ev.position,
2197                                    &boxes,
2198                                    &font,
2199                                    size,
2200                                    line_h,
2201                                    window,
2202                                ) else {
2203                                    return;
2204                                };
2205                                at
2206                            } else {
2207                                let Some(left) = *origin.read(cx) else {
2208                                    return;
2209                                };
2210                                char_at_x(&shown, ev.position.x - left, &font, size, window)
2211                            };
2212                            st.update(cx, |s, cx| {
2213                                s.marked = None;
2214                                s.cursor = at;
2215                                s.anchor = None;
2216                                // A double click takes the word under the
2217                                // pointer and a triple click the lot, the way
2218                                // every other text field does.
2219                                match ev.click_count {
2220                                    2 => {
2221                                        s.anchor = Some(word_target(&s.value, at, false));
2222                                        s.cursor = word_target(&s.value, at, true);
2223                                    }
2224                                    n if n >= 3 => select_all(s),
2225                                    _ => {}
2226                                }
2227                                cx.notify();
2228                            });
2229                        }
2230                    })
2231                    .on_mouse_move({
2232                        let st = self.state.clone();
2233                        let origin = text_origin.clone();
2234                        let font = text_font.clone();
2235                        let masks = self.input_type.masks();
2236                        let multiline = self.multiline;
2237                        let paras = paragraph_bounds.clone();
2238                        // The caret is drawn at 1.3x the font size; the rows a wrapped
2239                        // paragraph is measured in are the same height.
2240                        let line_h = text * 1.3;
2241                        let size = text;
2242                        move |ev: &gpui::MouseMoveEvent, window, cx| {
2243                            if ev.pressed_button != Some(gpui::MouseButton::Left) {
2244                                return;
2245                            }
2246                            let shown = displayed_value(st.read(cx), masks);
2247                            let at = if multiline {
2248                                let boxes = paras.read(cx).clone();
2249                                let Some(at) = char_at_point(
2250                                    &shown,
2251                                    ev.position,
2252                                    &boxes,
2253                                    &font,
2254                                    size,
2255                                    line_h,
2256                                    window,
2257                                ) else {
2258                                    return;
2259                                };
2260                                at
2261                            } else {
2262                                let Some(left) = *origin.read(cx) else {
2263                                    return;
2264                                };
2265                                char_at_x(&shown, ev.position.x - left, &font, size, window)
2266                            };
2267                            st.update(cx, |s, cx| {
2268                                if s.anchor.is_none() {
2269                                    s.anchor = Some(s.cursor);
2270                                }
2271                                s.marked = None;
2272                                s.cursor = at;
2273                                cx.notify();
2274                            });
2275                        }
2276                    })
2277            })
2278            // `status-disabled` is `--disabled-opacity`, which the theme owns.
2279            // Inside a group that dims its own box the field skips the second
2280            // coat; on its own (or a field disabled beside an enabled group)
2281            // it still dims itself.
2282            .when(self.is_disabled && !self.group_dim, |e| e.opacity(disabled_opacity));
2283
2284        // The multi-line field's ring, parked until its content is in place;
2285        // see the chrome call below.
2286        let mut carrier_ring = None;
2287
2288        // Inside an `InputGroup` the group is the field: v3's
2289        // `.input-group__input` is `rounded-none border-0 bg-transparent
2290        // shadow-none`.
2291        // `is_bare` takes the same exit: one chrome call site, skipped by
2292        // either reason.
2293        if self.in_group.is_none() && !self.is_bare {
2294            // Both spellings of the field take the same overlay ring. The
2295            // single-line box does not clip, so the ring is its own child;
2296            // the multi-line box clips its wrapped text (`overflow_hidden`
2297            // above) and would cut a child ring, so its ring hangs on the
2298            // non-clipping carrier added once the content is in place.
2299            let ring = crate::util::field_ring_color(is_invalid, focused, show_focus_ring, cx);
2300            field = crate::util::apply_field_chrome_ringless(
2301                field,
2302                self.variant,
2303                is_invalid,
2304                focused,
2305                show_focus_ring,
2306                Some(radius),
2307                cx,
2308            );
2309            if multiline {
2310                carrier_ring = Some(ring);
2311            } else {
2312                field = crate::util::with_field_ring_overlay(field, ring, radius, cx);
2313            }
2314
2315            if !self.is_disabled && !is_invalid && !focused && theme_background.is_none() {
2316                let idle_bg = match self.variant {
2317                    FieldVariant::Primary => colors.field.background,
2318                    FieldVariant::Secondary => colors.default.color,
2319                };
2320                let hover_bg = match self.variant {
2321                    FieldVariant::Primary => colors.field.hover(),
2322                    // `.input--secondary` uses the default-hover endpoint.
2323                    FieldVariant::Secondary => colors.default.hover(),
2324                };
2325                let hover_border = colors.field.border_hover();
2326                // Keep the editable Input as the stable behavior owner while
2327                // the listener-free visual fill follows HeroUI's 150ms
2328                // ease-smooth transition. The border endpoint is immediate,
2329                // and custom theme backgrounds retain their caller-owned fill.
2330                field = crate::anim::hover_fade_with_duration_and_easing(
2331                    field,
2332                    element_id::scoped(&base_id, "hover-fade"),
2333                    (idle_bg, hover_bg),
2334                    None,
2335                    Some(hover_border),
2336                    |fill| fill.rounded(radius),
2337                    Some(150),
2338                    crate::anim::HoverFadeEasing::EaseSmooth,
2339                    window,
2340                    cx,
2341                );
2342            }
2343        }
2344        if let Some(background) = theme_background {
2345            field = field.bg(background);
2346        }
2347        if let Some(foreground) = theme_foreground {
2348            field = field.text_color(foreground);
2349        }
2350
2351        // -- text content -----------------------------------------------------
2352        let st = self.state.read(cx);
2353        // `password` renders bullets while keeping the real value in state, so
2354        // cursor and selection maths stay in char units either way.
2355        let value = if self.input_type.masks() {
2356            "\u{2022}".repeat(st.value.chars().count())
2357        } else {
2358            st.value.clone()
2359        };
2360        let cursor = st.cursor;
2361        let is_empty = value.is_empty();
2362        let selection = st.selection();
2363
2364        // A multi-line field lays its lines out top-down and lets each one
2365        // wrap; a single-line one is a centred, non-wrapping row.
2366        let mut row = if self.multiline {
2367            gpui::div()
2368                .flex()
2369                .flex_col()
2370                .items_start()
2371                .w_full()
2372                .min_w_0()
2373                .flex_1()
2374        } else {
2375            let origin = text_origin;
2376            gpui::div()
2377                .flex()
2378                .items_center()
2379                .min_w_0()
2380                .flex_1()
2381                // A zero-width item at the head of the row: its bounds are the
2382                // text's left edge, which is what a click is measured against.
2383                // Only `canvas` is told its own bounds.
2384                .child(
2385                    gpui::canvas(
2386                        move |bounds, _window, cx| {
2387                            let left = bounds.origin.x;
2388                            if *origin.read(cx) != Some(left) {
2389                                origin.update(cx, |v, _| *v = Some(left));
2390                            }
2391                        },
2392                        |_, _, _, _| {},
2393                    )
2394                    .w(px(0.))
2395                    .h(px(0.))
2396                    .flex_shrink_0(),
2397                )
2398        };
2399
2400        if self.multiline && !(is_empty && !focused && self.placeholder.is_some()) {
2401            // One wrapping paragraph per newline, with the caret and any
2402            // selection placed inside the line they fall in.
2403            let caret_id = element_id::scoped(&base_id, "caret");
2404            row = row.child(multiline_body(
2405                MultilineBody {
2406                    paragraphs: paragraph_bounds.clone(),
2407                    value: &value,
2408                    cursor,
2409                    selection,
2410                    focused,
2411                    caret: (caret_id, text * 1.3, accent.color),
2412                    selection_bg: accent.with_alpha(0.24),
2413                },
2414                cx,
2415            ));
2416        } else if is_empty && !focused && self.placeholder.is_some() {
2417            row = row.child(
2418                gpui::div()
2419                    .text_color(theme_placeholder)
2420                    .truncate()
2421                    .child(self.placeholder.clone().unwrap().to_string()),
2422            );
2423        } else if let Some((lo, hi)) = selection {
2424            // Selected range — three spans.
2425            let before: String = value.chars().take(lo).collect();
2426            let selected: String = value.chars().skip(lo).take(hi - lo).collect();
2427            let after: String = value.chars().skip(hi).collect();
2428            let sel_bg = accent.with_alpha(0.24);
2429            row = row.child(
2430                gpui::div()
2431                    .flex()
2432                    .items_center()
2433                    .whitespace_nowrap()
2434                    .overflow_hidden()
2435                    .child(before)
2436                    .child(
2437                        gpui::div()
2438                            .px(px(1.))
2439                            .py(px(1.))
2440                            .rounded(px(4.))
2441                            .bg(sel_bg)
2442                            .child(selected),
2443                    )
2444                    .child(after),
2445            );
2446        } else {
2447            let before: String = value.chars().take(cursor).collect();
2448            let after: String = value.chars().skip(cursor).collect();
2449            row = row.child(
2450                gpui::div()
2451                    .flex()
2452                    .items_center()
2453                    .whitespace_nowrap()
2454                    .overflow_hidden()
2455                    .child(before)
2456                    .when(focused, |r| {
2457                        // v3's `@keyframes caret-blink`.
2458                        r.child(crate::anim::caret_blink(
2459                            gpui::div()
2460                                .w(px(1.5))
2461                                .h(text * 1.3)
2462                                .bg(accent.color)
2463                                .flex_shrink_0(),
2464                            element_id::scoped(&base_id, "caret"),
2465                            cx,
2466                        ))
2467                    })
2468                    .child(after),
2469            );
2470        }
2471
2472        // The builder inputs an accepted edit re-resolves the stored validity
2473        // from (see `refresh_stored_validity`): this field's own sources,
2474        // never the routed slot the edit suppresses, plus the HTML5 attribute
2475        // check render merges into the flag.
2476        let edit_is_invalid = self.is_invalid;
2477        let edit_validation_errors = self.validation_errors.clone();
2478        let edit_validate = self.validate.clone();
2479        let edit_error_message = self.error_message.clone();
2480        let native_validity = {
2481            let input_type = self.input_type;
2482            let min_length = self.min_length;
2483            let min = self.min;
2484            let max = self.max;
2485            let step = self.step;
2486            let pattern = self.pattern.clone();
2487            move |value: &str| {
2488                validate_value(
2489                    value,
2490                    input_type,
2491                    min_length,
2492                    min,
2493                    max,
2494                    step,
2495                    pattern.as_deref(),
2496                )
2497                .is_valid()
2498            }
2499        };
2500
2501        let edit_state = self.state.clone();
2502        let on_change = self.on_change.clone();
2503        let platform_native_validity = native_validity.clone();
2504        let platform_error_message = edit_error_message.clone();
2505        let platform_validate = edit_validate.clone();
2506        let platform_validation_errors = edit_validation_errors.clone();
2507        let edit_callback: TextCallback = crate::util::shared(move |value, window, cx| {
2508            edit_state.update(cx, |s, _| s.clear_routed_errors());
2509            refresh_stored_validity(
2510                &edit_state,
2511                edit_is_invalid,
2512                &platform_validation_errors,
2513                platform_validate.as_ref(),
2514                platform_error_message.clone(),
2515                &platform_native_validity,
2516                cx,
2517            );
2518            if let Some(callback) = &on_change {
2519                callback(value, window, cx);
2520            }
2521        });
2522        let platform_state = self.state.clone();
2523        let platform_edit = edit_callback.clone();
2524        let platform_focus = focus_handle.clone();
2525        let platform_font = text_font;
2526        let platform_paragraphs = paragraph_bounds;
2527        let platform_type = self.input_type;
2528        let platform_max = self.max_length;
2529        let platform_multiline = self.multiline;
2530        let platform_disabled = self.is_disabled;
2531        row = row.relative().child(
2532            gpui::canvas(
2533                |_, _, _| {},
2534                move |bounds, _, window, cx| {
2535                    if !platform_disabled {
2536                        window.handle_input(
2537                            &platform_focus,
2538                            PlatformTextInput {
2539                                state: platform_state.clone(),
2540                                on_edit: platform_edit.clone(),
2541                                input_type: platform_type,
2542                                max_length: platform_max,
2543                                multiline: platform_multiline,
2544                                bounds,
2545                                font: platform_font.clone(),
2546                                paragraphs: platform_paragraphs.clone(),
2547                            },
2548                            cx,
2549                        );
2550                    }
2551                },
2552            )
2553            .absolute()
2554            .size_full(),
2555        );
2556
2557        field = field.children(self.start_content);
2558        field = field.child(row);
2559        // isClearable — show X when has value and not disabled/readonly
2560        if self.is_clearable && !is_empty && !self.is_disabled && !self.is_read_only {
2561            let clear_focus_handle = self.state.read(cx).clear_focus_handle.clone();
2562            let clear_content = match self.clear_content {
2563                Some(content) => content,
2564                None => gpui::svg()
2565                    .size(px(12.))
2566                    .path(crate::icons::CLOSE)
2567                    .text_color(colors.muted)
2568                    .into_any_element(),
2569            };
2570            let clear_state = self.state.clone();
2571            let clear_on_change = self.on_change.clone();
2572            let on_clear = self.on_clear.clone();
2573            let clear_validation_errors = edit_validation_errors;
2574            let clear_validate = edit_validate.clone();
2575            let clear_error_message = edit_error_message;
2576            let clear_native = native_validity.clone();
2577            // `.search-field__clear-button` *is* a `CloseButton`, and
2578            // `.close-button:hover` fills `bg-default-hover` -- not a
2579            // hand-mixed wash.
2580            let clear_hover_bg = self.clear_hover_bg.unwrap_or(colors.default.hover());
2581            let clear_box = px(20.);
2582            let clear_radius = crate::util::small_radius(cx);
2583            let clear_selector = format!("input-clear-{}", self.state.entity_id().as_u64());
2584            let input_focus_after_clear = focus_handle.clone();
2585            let input_focus_handle = focus_handle.clone();
2586            let mut clear = gpui::div()
2587                .id(element_id::scoped(&base_id, "clear"))
2588                .debug_selector(move || clear_selector)
2589                .flex()
2590                .items_center()
2591                .justify_center()
2592                .size(clear_box)
2593                .p(px(4.))
2594                .rounded(clear_radius)
2595                .cursor(crate::util::interactive_cursor(cx))
2596                .text_color(colors.muted)
2597                .hover(move |s| s.bg(clear_hover_bg))
2598                .active({
2599                    // GPUI 0.2.2 has no div transform; use the same centred
2600                    // geometry as CloseButton's scale(0.93). The active style
2601                    // is an instant swap, including under reduced motion.
2602                    const PRESS_SCALE: f32 = 0.93;
2603                    let inset = px(f32::from(clear_box) * (1.0 - PRESS_SCALE) / 2.0);
2604                    let pressed = px(f32::from(clear_box) * PRESS_SCALE);
2605                    let radius = px(f32::from(clear_radius) * PRESS_SCALE);
2606                    move |s| {
2607                        s.h(pressed)
2608                            .w(pressed)
2609                            .mt(inset)
2610                            .mb(inset)
2611                            .ml(inset)
2612                            .mr(inset)
2613                            .rounded(radius)
2614                    }
2615                })
2616                // Pinned `useSearchField` `excludeFromTabOrder: true` (the
2617                // InputState-owned handle is not a tab stop) and
2618                // `preventFocusOnPress: true`. Pointer presses keep focus on
2619                // the input. Keys on the button must not bubble into the
2620                // field except Tab, which still walks to the next stop.
2621                .track_focus(&clear_focus_handle)
2622                .on_key_down(|event, _, cx| {
2623                    if event.keystroke.key != "tab" {
2624                        cx.stop_propagation();
2625                    }
2626                })
2627                .on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
2628                    cx.stop_propagation();
2629                    window.focus(&input_focus_handle, cx);
2630                    window.prevent_default();
2631                })
2632                .on_click(move |_, window, cx| {
2633                    window.focus(&input_focus_after_clear, cx);
2634                    clear_state.update(cx, |s, cx| {
2635                        s.value.clear();
2636                        s.marked = None;
2637                        s.cursor = 0;
2638                        s.anchor = None;
2639                        // The clear affordance is a user modification:
2640                        // it suppresses the routed server errors too.
2641                        s.clear_routed_errors();
2642                        cx.notify();
2643                    });
2644                    // The emptied value is the mirror's too, before any
2645                    // callback can observe the field: a synchronous
2646                    // submit inside `on_change` must not be blocked by
2647                    // the routed error this click just answered.
2648                    refresh_stored_validity(
2649                        &clear_state,
2650                        edit_is_invalid,
2651                        &clear_validation_errors,
2652                        clear_validate.as_ref(),
2653                        clear_error_message.clone(),
2654                        &clear_native,
2655                        cx,
2656                    );
2657                    if let Some(cb) = &clear_on_change {
2658                        cb("", window, cx);
2659                    }
2660                    if let Some(cb) = &on_clear {
2661                        cb(window, cx);
2662                    }
2663                })
2664                .child(clear_content);
2665            // The clear affordance both carries `clear_radius` and takes the
2666            // focus, so the overlay ring goes straight on it and tracks the
2667            // same corner. Only the button migrates: the field around it
2668            // clips its children in the multi-line case and stays on shadows.
2669            clear = crate::util::ring_overlay_if_focused(
2670                clear,
2671                &clear_focus_handle,
2672                true,
2673                clear_radius,
2674                Vec::new(),
2675                window,
2676                cx,
2677            );
2678            field = field.child(clear);
2679        }
2680        field = field.children(self.end_content);
2681
2682        // -- editing ----------------------------------------------------------
2683        let state_entity = self.state.clone();
2684        let on_submit = self.on_submit.clone();
2685        let on_clear = self.on_clear.clone();
2686        let clear_on_escape = self.clear_on_escape;
2687        let is_read_only = self.is_read_only;
2688        let is_disabled = self.is_disabled;
2689        field = field.on_key_down(move |ev: &KeyDownEvent, window, cx| {
2690            if is_disabled {
2691                return;
2692            }
2693            let key: &str = &ev.keystroke.key;
2694            if crate::util::key_enables_focus_visible(key) {
2695                crate::util::set_focus_visible(true, cx);
2696            }
2697            let mods = ev.keystroke.modifiers;
2698            if state_entity.read(cx).marked.is_some() && !matches!(key, "escape" | "tab") {
2699                cx.stop_propagation();
2700                return;
2701            }
2702            let input_type = self.input_type;
2703            let max_length = self.max_length;
2704            let multiline = self.multiline;
2705            let owns_key = (!mods.control
2706                && !mods.alt
2707                && !mods.platform
2708                && (matches!(key, "backspace" | "delete" | "left" | "right")
2709                    || multiline && matches!(key, "up" | "down" | "home" | "end" | "enter")))
2710                || (mods.control || mods.platform)
2711                    && !mods.alt
2712                    && matches!(key, "a" | "left" | "right");
2713            if owns_key {
2714                cx.stop_propagation();
2715            }
2716            if owns_key || key == "escape" {
2717                state_entity.update(cx, |state, _| state.marked = None);
2718            }
2719            let mut changed = false;
2720            let mut cleared = false;
2721            let mut submit = false;
2722
2723            if key == "a" && (mods.control || mods.platform) {
2724                // Ctrl+A / Cmd+A : select all
2725                if !mods.alt {
2726                    state_entity.update(cx, |s, cx| {
2727                        select_all(s);
2728                        cx.notify();
2729                    });
2730                }
2731            } else if mods.control || mods.alt || mods.platform {
2732                let cmd = mods.control || mods.platform;
2733                if !is_read_only && cmd && key == "v" {
2734                    if let Some(text) = cx.read_from_clipboard().and_then(|c| c.text()) {
2735                        changed = state_entity.update(cx, |s, cx| {
2736                            let mut inserted = false;
2737                            for ch in text.chars() {
2738                                if state_accepts(s, ch, input_type, max_length) {
2739                                    insert_char(s, ch);
2740                                    inserted = true;
2741                                }
2742                            }
2743                            if inserted {
2744                                cx.notify();
2745                            }
2746                            inserted
2747                        });
2748                    }
2749                } else if cmd && (key == "c" || key == "x") {
2750                    // Paste was here without a copy: a field you can paste into
2751                    // and not copy out of is half a clipboard. A masked field
2752                    // is the exception: browsers refuse copy and cut on
2753                    // `type="password"` (the value never reaches the clipboard
2754                    // and cut deletes nothing), and so does this.
2755                    if input_type.masks() {
2756                        return;
2757                    }
2758                    let selected = {
2759                        let st = state_entity.read(cx);
2760                        slice_selection(&st.value, st.selection())
2761                    };
2762                    if let Some(text) = selected {
2763                        cx.write_to_clipboard(gpui::ClipboardItem::new_string(text));
2764                        if key == "x" && !is_read_only {
2765                            state_entity.update(cx, |s, cx| {
2766                                delete_selection(s);
2767                                cx.notify();
2768                            });
2769                            changed = true;
2770                        }
2771                    }
2772                } else if cmd && (key == "left" || key == "right") {
2773                    // Ctrl+arrow is word-wise motion; a password field is the
2774                    // one place it would leak the shape of the value, and it
2775                    // does not there either, since the glyphs are dots.
2776                    state_entity.update(cx, |s, cx| {
2777                        move_word(s, key == "right", mods.shift);
2778                        cx.notify();
2779                    });
2780                }
2781            } else {
2782                let shift = mods.shift;
2783                match key {
2784                    "backspace" => {
2785                        if is_read_only {
2786                            return;
2787                        }
2788                        changed = state_entity.update(cx, |s, cx| {
2789                            let deleted = backspace(s);
2790                            if deleted {
2791                                cx.notify();
2792                            }
2793                            deleted
2794                        });
2795                    }
2796                    "delete" => {
2797                        if is_read_only {
2798                            return;
2799                        }
2800                        changed = state_entity.update(cx, |s, cx| {
2801                            let deleted = delete(s);
2802                            if deleted {
2803                                cx.notify();
2804                            }
2805                            deleted
2806                        });
2807                    }
2808                    "left" => state_entity.update(cx, |s, cx| {
2809                        move_left(s, shift);
2810                        cx.notify();
2811                    }),
2812                    "right" => state_entity.update(cx, |s, cx| {
2813                        move_right(s, shift);
2814                        cx.notify();
2815                    }),
2816                    // Home and End are the *line's* ends in a multi-line field,
2817                    // the way a `<textarea>` has it; only a single-line field
2818                    // has one line to run to.
2819                    "home" => state_entity.update(cx, |s, cx| {
2820                        if multiline {
2821                            before_move(s, shift);
2822                            s.cursor = line_bounds(&s.value, s.cursor).0;
2823                        } else {
2824                            move_home(s, shift);
2825                        }
2826                        cx.notify();
2827                    }),
2828                    "end" => state_entity.update(cx, |s, cx| {
2829                        if multiline {
2830                            before_move(s, shift);
2831                            s.cursor = line_bounds(&s.value, s.cursor).1;
2832                        } else {
2833                            move_end(s, shift);
2834                        }
2835                        cx.notify();
2836                    }),
2837                    // Vertical motion only means something with lines to move
2838                    // between; a single-line field leaves up and down to
2839                    // whatever is around it (a combo box reads them).
2840                    "up" | "down" if multiline => state_entity.update(cx, |s, cx| {
2841                        move_vertical(s, key == "down", shift);
2842                        cx.notify();
2843                    }),
2844                    // A multi-line field takes Enter as a newline, the way a
2845                    // `<textarea>` does; a single-line one submits.
2846                    "enter" if multiline => {
2847                        cx.stop_propagation();
2848                        if is_read_only {
2849                            return;
2850                        }
2851                        changed = state_entity.update(cx, |s, cx| {
2852                            let inserted = state_accepts(s, NEWLINE, input_type, max_length);
2853                            if inserted {
2854                                insert_char(s, NEWLINE);
2855                                cx.notify();
2856                            }
2857                            inserted
2858                        });
2859                    }
2860                    "enter" => submit = true,
2861                    "escape" => {
2862                        let had_value = !state_entity.read(cx).is_empty();
2863                        state_entity.update(cx, |s, cx| {
2864                            s.anchor = None;
2865                            if clear_on_escape && !is_read_only {
2866                                s.value.clear();
2867                                s.cursor = 0;
2868                            }
2869                            cx.notify();
2870                        });
2871                        if clear_on_escape && !is_read_only && had_value {
2872                            changed = true;
2873                            cleared = true;
2874                        }
2875                    }
2876                    _ => {}
2877                }
2878            }
2879
2880            if changed {
2881                if mods.control || mods.platform {
2882                    cx.stop_propagation();
2883                }
2884                state_entity.update(cx, |state, _| state.marked = None);
2885                let value = state_entity.read(cx).value().to_owned();
2886                edit_callback(&value, window, cx);
2887            }
2888            if cleared {
2889                if let Some(cb) = &on_clear {
2890                    cb(window, cx);
2891                }
2892            }
2893            if submit {
2894                if let Some(cb) = &on_submit {
2895                    {
2896                        let v = state_entity.read(cx).value().to_owned();
2897                        cb(&v, window, cx);
2898                    }
2899                    // A field with its own `onSubmit` owns Enter, the way a
2900                    // native input whose keydown handler handles the key
2901                    // stops the form's implicit submission: the keystroke
2902                    // must not also bubble into the enclosing `Form`. A
2903                    // field without `onSubmit` swallows nothing — its Enter
2904                    // bubbling is exactly how a plain field submits its
2905                    // form.
2906                    cx.stop_propagation();
2907                }
2908            }
2909        });
2910
2911        // The `sx` slot refines whatever root this render returns. Inside a
2912        // group the field row *is* the whole component (the wrapper below is
2913        // skipped), so it lands there; standalone it lands on the
2914        // label-to-error column at the tail, and the row keeps its own paint.
2915        let field = if self.in_group.is_some() {
2916            crate::util::apply_sx(field, &self.sx)
2917        } else {
2918            field
2919        };
2920        let field = if self.is_disabled {
2921            field
2922        } else {
2923            crate::util::record_focus_bounds(field, &focus_handle, window, cx)
2924        };
2925
2926        // The anchor is the 36px field row itself — not the
2927        // label-to-error wrapper below — the way RAC's `triggerRef` reads
2928        // `groupRef.current || inputRef.current`. Wrapping here keeps the
2929        // label, description, error and value rows outside the measured
2930        // bounds while preserving their existing rendering order.
2931        let field_element: gpui::AnyElement = if let Some((anchor, selector)) = self.field_anchor {
2932            let selector_text = selector.to_string();
2933            crate::popover::PopoverTriggerMeasure::new(
2934                field.debug_selector(move || selector_text),
2935                anchor,
2936            )
2937            .into_any_element()
2938        } else {
2939            field.into_any_element()
2940        };
2941
2942        // The multi-line box clips its wrapped text, so its focus ring rides
2943        // `util::field_ring_carrier`. The measured anchor bounds stay on the
2944        // field row inside it.
2945        let field_element: gpui::AnyElement = match carrier_ring {
2946            Some(ring) => {
2947                crate::util::field_ring_carrier(field_element, ring, radius, cx).into_any_element()
2948            }
2949            None => field_element,
2950        };
2951
2952        // Inside a group the surrounding component owns the label, the
2953        // description and the error slot -- v3's `InputGroup.Input` is the input
2954        // and nothing else. Returning the wrapper here would drop a whole
2955        // labelled column into the group's row.
2956        if self.in_group.is_some() {
2957            return field_element;
2958        }
2959
2960        // -- wrapper with label / description / error --------------------------
2961        let mut el = gpui::div().flex().flex_col().gap(px(4.));
2962        if self.full_width {
2963            el = el.w_full();
2964        } else {
2965            el = el.max_w(px(320.));
2966        }
2967        if let Some(label) = self.label {
2968            let mut label_row = gpui::div()
2969                .flex()
2970                .items_center()
2971                .gap(px(4.))
2972                .text_size(px(14.))
2973                .line_height(px(20.))
2974                .font_weight(gpui::FontWeight::MEDIUM)
2975                .text_color(colors.foreground)
2976                .child(label.to_string());
2977            if self.is_required {
2978                label_row = label_row.child(
2979                    gpui::div()
2980                        .text_color(colors.danger.color)
2981                        .child("*".to_owned()),
2982                );
2983            }
2984            el = el.child(label_row);
2985        }
2986        el = el.child(field_element);
2987        // Every message, space-joined in upstream order — React Aria's
2988        // `FieldError` default — not just the first. The shared helper keeps
2989        // the last message mounted while the invalid row animates out, so a
2990        // description cannot replace it halfway through the height tween.
2991        let error = (!validity.messages.is_empty()).then(|| validity.joined().into());
2992        if let Some(error) = crate::anim::field_error_panel(&base_id, error, window, cx) {
2993            el = el.child(error);
2994        } else if let Some(desc) = self.description {
2995            el = el.child(
2996                gpui::div()
2997                    .text_size(px(12.))
2998                    .line_height(px(16.))
2999                    .text_color(colors.muted)
3000                    .child(desc.to_string()),
3001            );
3002        }
3003
3004        el = crate::util::apply_sx(el, &self.sx);
3005        el.into_any_element()
3006    }
3007}
3008
3009// ---------------------------------------------------------------------------
3010// TextField / SearchField
3011// ---------------------------------------------------------------------------
3012
3013/// The complete state passed to [`TextField::content`].
3014#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
3015#[non_exhaustive]
3016pub struct TextFieldRenderState {
3017    /// The field cannot receive focus or input.
3018    pub is_disabled: bool,
3019    /// Controlled, server, or custom validation currently fails.
3020    pub is_invalid: bool,
3021    /// The value can be selected but not changed.
3022    pub is_read_only: bool,
3023    /// The field must contain a value before native form submission.
3024    pub is_required: bool,
3025    /// The input itself owns keyboard focus.
3026    pub is_focused: bool,
3027    /// The input or another composed child owns keyboard focus.
3028    pub is_focus_within: bool,
3029    /// Focus was reached through keyboard navigation.
3030    pub is_focus_visible: bool,
3031}
3032
3033/// TextField — port of `@heroui/text-field` (v3).
3034///
3035/// The composition-friendly field: a label, an [`Input`], and a description or
3036/// validation message. `Input` is the bare control; `TextField` is the labelled
3037/// wrapper most applications reach for.
3038#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
3039#[derive(IntoElement)]
3040pub struct TextField {
3041    inner: Input,
3042}
3043
3044impl TextField {
3045    /// Builds the labelled field over `state`, borrowed or owned — see
3046    /// [`Input::new`].
3047    pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
3048        Self {
3049            inner: Input::new(state),
3050        }
3051    }
3052
3053    /// `type` — the HTML input type, which v3 sets on the field and this port
3054    /// forwards to the inner [`Input`].
3055    pub fn input_type(mut self, input_type: InputType) -> Self {
3056        self.inner = self.inner.input_type(input_type);
3057        self
3058    }
3059
3060    /// Sets the visible label.
3061    pub fn label(mut self, text: impl Into<SharedString>) -> Self {
3062        self.inner = self.inner.label(text);
3063        self
3064    }
3065
3066    /// v3's field `children`-as-a-function, handed the complete resolved field
3067    /// state.
3068    pub fn content(
3069        mut self,
3070        render: impl Fn(TextFieldRenderState) -> gpui::AnyElement + 'static,
3071    ) -> Self {
3072        self.inner = self.inner.field_content(move |state| {
3073            render(TextFieldRenderState {
3074                is_disabled: state.is_disabled,
3075                is_invalid: state.is_invalid,
3076                is_read_only: state.is_read_only,
3077                is_required: state.is_required,
3078                is_focused: state.focus.is_focused,
3079                is_focus_within: state.focus.is_focus_within,
3080                is_focus_visible: state.focus.is_focus_visible,
3081            })
3082        });
3083        self
3084    }
3085
3086    /// Sets the placeholder shown while the value is empty.
3087    pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
3088        self.inner = self.inner.placeholder(text);
3089        self
3090    }
3091
3092    /// Sets the description text.
3093    pub fn description(mut self, text: impl Into<SharedString>) -> Self {
3094        self.inner = self.inner.description(text);
3095        self
3096    }
3097
3098    /// `autoFocus` — see [`Input::auto_focus`].
3099    pub fn auto_focus(mut self, v: bool) -> Self {
3100        self.inner = self.inner.auto_focus(v);
3101        self
3102    }
3103
3104    /// `name` — see [`Input::name`].
3105    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
3106        self.inner = self.inner.name(name);
3107        self
3108    }
3109
3110    /// `defaultValue` — see [`Input::default_value`].
3111    pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
3112        self.inner = self.inner.default_value(text);
3113        self
3114    }
3115
3116    /// `value` — see [`Input::value`].
3117    pub fn value(mut self, text: impl Into<SharedString>) -> Self {
3118        self.inner = self.inner.value(text);
3119        self
3120    }
3121
3122    /// `validationBehavior` — see [`crate::input::Input::validation_behavior`].
3123    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
3124        self.inner = self.inner.validation_behavior(behavior);
3125        self
3126    }
3127
3128    /// `validate` — see [`Input::validate`].
3129    pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
3130        self.inner = self.inner.validate(f);
3131        self
3132    }
3133
3134    /// `validationErrors` — see [`Input::validation_errors`].
3135    pub fn validation_errors(
3136        mut self,
3137        errors: impl IntoIterator<Item = impl Into<SharedString>>,
3138    ) -> Self {
3139        self.inner = self.inner.validation_errors(errors);
3140        self
3141    }
3142
3143    /// Sets the error message shown when the field is invalid.
3144    pub fn error_message(mut self, text: impl Into<SharedString>) -> Self {
3145        self.inner = self.inner.error_message(text);
3146        self
3147    }
3148
3149    /// Sets the field variant.
3150    pub fn variant(mut self, variant: FieldVariant) -> Self {
3151        self.inner = self.inner.variant(variant);
3152        self
3153    }
3154
3155    /// Makes the field fill its parent's width.
3156    pub fn full_width(mut self) -> Self {
3157        self.inner = self.inner.full_width();
3158        self
3159    }
3160
3161    /// The single-line box height — see [`Input::height`].
3162    pub fn height(mut self, h: impl Into<Pixels>) -> Self {
3163        self.inner = self.inner.height(h);
3164        self
3165    }
3166
3167    /// The box's horizontal padding — see [`Input::padding_x`].
3168    pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
3169        self.inner = self.inner.padding_x(p);
3170        self
3171    }
3172
3173    /// Named theme recipe — see [`Input::recipe`].
3174    pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
3175        self.inner = self.inner.recipe(name);
3176        self
3177    }
3178
3179    /// The field text size — see [`Input::text_size`].
3180    pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
3181        self.inner = self.inner.text_size(size);
3182        self
3183    }
3184
3185    /// The box's corner radius — see [`Input::radius`].
3186    pub fn radius(mut self, r: impl Into<Pixels>) -> Self {
3187        self.inner = self.inner.radius(r);
3188        self
3189    }
3190
3191    /// Drops the field's chrome — see [`Input::is_bare`].
3192    pub fn is_bare(mut self, v: bool) -> Self {
3193        self.inner = self.inner.is_bare(v);
3194        self
3195    }
3196
3197    /// Shows or hides only the inner field's visual focus ring — see
3198    /// [`Input::focus_ring`].
3199    pub fn focus_ring(mut self, v: bool) -> Self {
3200        self.inner = self.inner.focus_ring(v);
3201        self
3202    }
3203
3204    /// The field text's font family — see [`Input::font_family`].
3205    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
3206        self.inner = self.inner.font_family(family);
3207        self
3208    }
3209
3210    /// Leading content inside the box — see [`Input::start_content`].
3211    pub fn start_content(mut self, el: impl IntoElement) -> Self {
3212        self.inner = self.inner.start_content(el);
3213        self
3214    }
3215
3216    /// Sets whether the field is disabled.
3217    pub fn is_disabled(mut self, v: bool) -> Self {
3218        self.inner = self.inner.is_disabled(v);
3219        self
3220    }
3221
3222    /// Sets whether the field is read-only.
3223    pub fn is_read_only(mut self, v: bool) -> Self {
3224        self.inner = self.inner.is_read_only(v);
3225        self
3226    }
3227
3228    /// Sets whether the field is required.
3229    pub fn is_required(mut self, v: bool) -> Self {
3230        self.inner = self.inner.is_required(v);
3231        self
3232    }
3233
3234    /// Sets whether the field is invalid.
3235    pub fn is_invalid(mut self, v: bool) -> Self {
3236        self.inner = self.inner.is_invalid(v);
3237        self
3238    }
3239
3240    /// Sets the handler called with the new value whenever it changes.
3241    pub fn on_change(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3242        self.inner = self.inner.on_change(handler);
3243        self
3244    }
3245
3246    /// Sets the handler called with the current value when Enter is pressed.
3247    pub fn on_submit(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3248        self.inner = self.inner.on_submit(handler);
3249        self
3250    }
3251
3252    /// The one slot for caller-owned low-level styling: GPUI's styling methods
3253    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
3254    /// applied to the field's root element after every value the variant and
3255    /// the active theme chose, so they win. The wrapper renders no element of
3256    /// its own, so the slot rides the inner [`Input`] and lands on that root.
3257    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
3258        self.inner = self.inner.sx(style);
3259        self
3260    }
3261}
3262
3263impl RenderOnce for TextField {
3264    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
3265        self.inner.render(window, cx)
3266    }
3267}
3268
3269/// The complete state passed to [`SearchField::content`].
3270#[derive(Clone, Debug, PartialEq, Eq)]
3271#[non_exhaustive]
3272pub struct SearchFieldRenderState {
3273    /// The field cannot receive focus or input.
3274    pub is_disabled: bool,
3275    /// Controlled, server, or custom validation currently fails.
3276    pub is_invalid: bool,
3277    /// The value can be selected but not changed.
3278    pub is_read_only: bool,
3279    /// The field must contain a value before native form submission.
3280    pub is_required: bool,
3281    /// The input itself owns keyboard focus.
3282    pub is_focused: bool,
3283    /// The input or another composed child owns keyboard focus.
3284    pub is_focus_within: bool,
3285    /// Focus was reached through keyboard navigation.
3286    pub is_focus_visible: bool,
3287    /// The current search query.
3288    pub value: SharedString,
3289    /// The current search query is empty.
3290    pub is_empty: bool,
3291}
3292
3293/// SearchField — port of `@heroui/search-field` (v3).
3294///
3295/// An [`Input`] specialised for search: a leading magnifier icon and a clear
3296/// button that appears once there is a value. `onSubmit` fires on Enter and
3297/// `onClear` when the value is cleared.
3298#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
3299#[derive(IntoElement)]
3300pub struct SearchField {
3301    state: Entity<InputState>,
3302    /// See [`SearchField::content`]: v3's field children-as-a-function.
3303    content: Option<std::sync::Arc<dyn Fn(SearchFieldRenderState) -> gpui::AnyElement + 'static>>,
3304    /// `name` — the submission name, forwarded to the inner `Input`.
3305    name: Option<SharedString>,
3306    /// `defaultValue` — forwarded to the inner `Input`.
3307    default_value: Option<SharedString>,
3308    /// `value` — forwarded to the inner `Input`.
3309    value: Option<SharedString>,
3310    /// `validationBehavior` — forwarded to the inner `Input`.
3311    validation_behavior: Option<crate::form::ValidationBehavior>,
3312    label: Option<SharedString>,
3313    placeholder: SharedString,
3314    /// Whether `placeholder` came from the caller; otherwise render uses
3315    /// the localized default (`i18n::UiString::Search`).
3316    placeholder_is_set: bool,
3317    description: Option<SharedString>,
3318    variant: FieldVariant,
3319    variant_is_set: bool,
3320    recipes: Vec<SharedString>,
3321    text_size: Option<Pixels>,
3322    /// The field text's family, forwarded to the inner `Input`.
3323    font_family: Option<SharedString>,
3324    radius: Option<Pixels>,
3325    full_width: bool,
3326    /// Optional box geometry/chrome overrides forwarded to the inner `Input`.
3327    field: crate::util::FieldBox,
3328    is_disabled: bool,
3329    is_read_only: bool,
3330    is_required: bool,
3331    is_invalid: bool,
3332    /// `autoFocus` — take focus on the first render.
3333    auto_focus: bool,
3334    /// `validate` — run by the component, not the caller.
3335    validate: Option<crate::validation::Validator<str>>,
3336    /// `validationErrors` — messages from a server round-trip.
3337    validation_errors: Vec<SharedString>,
3338    on_change: Option<TextCallback>,
3339    on_submit: Option<TextCallback>,
3340    on_clear: Option<std::sync::Arc<dyn Fn(&mut Window, &mut App) + 'static>>,
3341    /// `SearchField.SearchIcon` — the leading glyph. `None` draws the magnifier.
3342    search_icon: Option<gpui::AnyElement>,
3343    /// `SearchField.ClearButton` children. `None` draws the close glyph.
3344    clear_icon: Option<gpui::AnyElement>,
3345    /// Trailing content inside the field, before the clear button. v3 composes
3346    /// it (a `Kbd` with the shortcut, in its "With Keyboard Shortcut" example).
3347    end_content: Option<gpui::AnyElement>,
3348    /// The `sx` slot, refined over the root style at the end of render.
3349    sx: Option<Box<gpui::StyleRefinement>>,
3350}
3351
3352impl SearchField {
3353    /// v3's field `children`-as-a-function, handed the complete resolved field
3354    /// and query state.
3355    pub fn content(
3356        mut self,
3357        render: impl Fn(SearchFieldRenderState) -> gpui::AnyElement + 'static,
3358    ) -> Self {
3359        self.content = Some(std::sync::Arc::new(render));
3360        self
3361    }
3362
3363    /// Builds the search field over `state`, borrowed or owned — see
3364    /// [`Input::new`].
3365    pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
3366        Self {
3367            content: None,
3368            state: state.borrow().clone(),
3369            name: None,
3370            default_value: None,
3371            value: None,
3372            validation_behavior: None,
3373            label: None,
3374            placeholder: "Search".into(),
3375            placeholder_is_set: false,
3376            description: None,
3377            variant: FieldVariant::Primary,
3378            variant_is_set: false,
3379            recipes: Vec::new(),
3380            text_size: None,
3381            font_family: None,
3382            radius: None,
3383            full_width: false,
3384            field: crate::util::FieldBox::default(),
3385            is_disabled: false,
3386            is_read_only: false,
3387            is_required: false,
3388            is_invalid: false,
3389            auto_focus: false,
3390            validate: None,
3391            validation_errors: Vec::new(),
3392            on_change: None,
3393            on_submit: None,
3394            on_clear: None,
3395            search_icon: None,
3396            clear_icon: None,
3397            end_content: None,
3398            sx: None,
3399        }
3400    }
3401
3402    /// Sets the visible label.
3403    pub fn label(mut self, text: impl Into<SharedString>) -> Self {
3404        self.label = Some(text.into());
3405        self
3406    }
3407
3408    /// Sets the placeholder shown while the value is empty.
3409    pub fn placeholder(mut self, text: impl Into<SharedString>) -> Self {
3410        self.placeholder = text.into();
3411        self.placeholder_is_set = true;
3412        self
3413    }
3414
3415    /// Sets the description text.
3416    pub fn description(mut self, text: impl Into<SharedString>) -> Self {
3417        self.description = Some(text.into());
3418        self
3419    }
3420
3421    /// Sets the field variant.
3422    pub fn variant(mut self, variant: FieldVariant) -> Self {
3423        self.variant = variant;
3424        self.variant_is_set = true;
3425        self
3426    }
3427
3428    /// Named recipe from `Theme.components.text_field.recipes`. Stackable.
3429    pub fn recipe(mut self, name: impl Into<SharedString>) -> Self {
3430        self.recipes.push(name.into());
3431        self
3432    }
3433
3434    /// The field text size — forwarded to the inner [`Input`].
3435    pub fn text_size(mut self, size: impl Into<Pixels>) -> Self {
3436        self.text_size = Some(size.into());
3437        self
3438    }
3439
3440    /// The field text's font family — forwarded to the inner [`Input`], so
3441    /// the query, placeholder and caret measurement all use it (see
3442    /// [`Input::font_family`]). Not a v3 prop; v3 sets it with a class.
3443    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
3444        self.font_family = Some(family.into());
3445        self
3446    }
3447
3448    /// The box's corner radius — forwarded to the inner [`Input`].
3449    pub fn radius(mut self, r: impl Into<Pixels>) -> Self {
3450        self.radius = Some(r.into());
3451        self
3452    }
3453
3454    /// Makes the field fill its parent's width.
3455    pub fn full_width(mut self) -> Self {
3456        self.full_width = true;
3457        self
3458    }
3459
3460    /// Replaces the 36px box height of the inner field.
3461    pub fn height(mut self, h: impl Into<Pixels>) -> Self {
3462        self.field.height = Some(h.into());
3463        self
3464    }
3465
3466    /// Replaces the box's `px-3` horizontal padding.
3467    pub fn padding_x(mut self, p: impl Into<Pixels>) -> Self {
3468        self.field.padding_x = Some(p.into());
3469        self
3470    }
3471
3472    /// Renders the box with no background, border, field shadow or focus ring,
3473    /// for a caller painting around it.
3474    pub fn is_bare(mut self, v: bool) -> Self {
3475        self.field.is_bare = v;
3476        self.field.is_bare_is_set = true;
3477        self
3478    }
3479
3480    /// Shows or hides only the search field's visual focus ring. The field
3481    /// remains focusable, editable and accessible when set to `false`.
3482    pub fn focus_ring(mut self, v: bool) -> Self {
3483        self.field.focus_ring = Some(v);
3484        self
3485    }
3486
3487    /// Sets whether the field is disabled.
3488    pub fn is_disabled(mut self, v: bool) -> Self {
3489        self.is_disabled = v;
3490        self
3491    }
3492
3493    /// Sets the handler called with the new value whenever it changes.
3494    pub fn on_change(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3495        self.on_change = Some(std::sync::Arc::new(handler));
3496        self
3497    }
3498
3499    /// Sets the handler called with the current value when Enter is pressed.
3500    pub fn on_submit(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
3501        self.on_submit = Some(std::sync::Arc::new(handler));
3502        self
3503    }
3504
3505    /// `name` — see [`Input::name`].
3506    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
3507        self.name = Some(name.into());
3508        self
3509    }
3510
3511    /// `defaultValue` — see [`Input::default_value`].
3512    pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
3513        self.default_value = Some(text.into());
3514        self
3515    }
3516
3517    /// `value` — see [`Input::value`].
3518    pub fn value(mut self, text: impl Into<SharedString>) -> Self {
3519        self.value = Some(text.into());
3520        self
3521    }
3522
3523    /// `SearchField.SearchIcon` — replaces the leading magnifier.
3524    pub fn search_icon(mut self, el: impl IntoElement) -> Self {
3525        self.search_icon = Some(el.into_any_element());
3526        self
3527    }
3528
3529    /// Replaces the glyph inside `SearchField.ClearButton`.
3530    pub fn clear_icon(mut self, el: impl IntoElement) -> Self {
3531        self.clear_icon = Some(el.into_any_element());
3532        self
3533    }
3534
3535    /// Trailing content inside the field — v3 composes a `Kbd` here.
3536    pub fn end_content(mut self, el: impl IntoElement) -> Self {
3537        self.end_content = Some(el.into_any_element());
3538        self
3539    }
3540
3541    /// `validationBehavior` — see [`Input::validation_behavior`].
3542    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
3543        self.validation_behavior = Some(behavior);
3544        self
3545    }
3546
3547    /// `validate` — see [`Input::validate`].
3548    pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
3549        self.validate = Some(std::sync::Arc::new(f));
3550        self
3551    }
3552
3553    /// `validationErrors` — see [`Input::validation_errors`].
3554    pub fn validation_errors(
3555        mut self,
3556        errors: impl IntoIterator<Item = impl Into<SharedString>>,
3557    ) -> Self {
3558        self.validation_errors = errors.into_iter().map(Into::into).collect();
3559        self
3560    }
3561
3562    /// `autoFocus` — take focus on the first render.
3563    pub fn auto_focus(mut self, v: bool) -> Self {
3564        self.auto_focus = v;
3565        self
3566    }
3567
3568    /// `isReadOnly` — the field shows its value but cannot be edited.
3569    pub fn is_read_only(mut self, v: bool) -> Self {
3570        self.is_read_only = v;
3571        self
3572    }
3573
3574    /// `isRequired` — marks the label.
3575    pub fn is_required(mut self, v: bool) -> Self {
3576        self.is_required = v;
3577        self
3578    }
3579
3580    /// `isInvalid` — applies the danger treatment.
3581    pub fn is_invalid(mut self, v: bool) -> Self {
3582        self.is_invalid = v;
3583        self
3584    }
3585
3586    /// Called after the clear button empties the field.
3587    pub fn on_clear(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
3588        self.on_clear = Some(std::sync::Arc::new(handler));
3589        self
3590    }
3591
3592    /// The one slot for caller-owned low-level styling: GPUI's styling methods
3593    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
3594    /// applied to the search field's root element after every value the
3595    /// variant and the active theme chose, so they win. Held here and handed
3596    /// to the [`Input`] this field builds at render time, whose render
3597    /// applies it.
3598    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
3599        crate::util::refine_sx(&mut self.sx, style);
3600        self
3601    }
3602}
3603
3604impl RenderOnce for SearchField {
3605    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
3606        let colors = cx.colors();
3607        let validate = self.validate.clone();
3608        // `useSearchField` defaults `type` to `'search'`, so the rendered
3609        // input is `<input type="search">` and its role is the search one.
3610        let mut input = Input::new(self.state)
3611            .input_type(InputType::Search)
3612            .when_some(self.content, |i, render| {
3613                i.field_content(move |state| {
3614                    let is_empty = state.value.is_empty();
3615                    render(SearchFieldRenderState {
3616                        is_disabled: state.is_disabled,
3617                        is_invalid: state.is_invalid,
3618                        is_read_only: state.is_read_only,
3619                        is_required: state.is_required,
3620                        is_focused: state.focus.is_focused,
3621                        is_focus_within: state.focus.is_focus_within,
3622                        is_focus_visible: state.focus.is_focus_visible,
3623                        value: state.value,
3624                        is_empty,
3625                    })
3626                })
3627            })
3628            .placeholder(if self.placeholder_is_set {
3629                self.placeholder
3630            } else {
3631                crate::i18n::ui_string(crate::i18n::UiString::Search, cx)
3632            })
3633            .when_some(self.name, |i, n| i.name(n))
3634            .when_some(self.value, |i, v| i.value(v))
3635            .when_some(self.default_value, |i, v| i.default_value(v))
3636            .when_some(self.validation_behavior, |i, b| i.validation_behavior(b))
3637            .with_recipes(self.recipes)
3638            .is_disabled(self.is_disabled)
3639            .is_read_only(self.is_read_only)
3640            .is_required(self.is_required)
3641            .is_invalid(self.is_invalid)
3642            .validation_errors(self.validation_errors.clone())
3643            .auto_focus(self.auto_focus)
3644            .when_some(validate, |i, f| i.validate(move |v| f(v)))
3645            .is_clearable(true)
3646            .start_content(match self.search_icon {
3647                Some(icon) => icon,
3648                None => gpui::svg()
3649                    .size(crate::util::FIELD_ICON)
3650                    .path(crate::icons::SEARCH)
3651                    .flex_shrink_0()
3652                    .text_color(colors.muted)
3653                    .into_any_element(),
3654            });
3655        if self.variant_is_set {
3656            input = input.variant(self.variant);
3657        }
3658        if let Some(size) = self.text_size {
3659            input = input.text_size(size);
3660        }
3661        if let Some(family) = self.font_family {
3662            input = input.font_family(family);
3663        }
3664        if let Some(r) = self.radius {
3665            input = input.radius(r);
3666        }
3667        input = input.with_field_box(self.field);
3668        if let Some(icon) = self.clear_icon {
3669            input = input.clear_content(icon);
3670        }
3671        if let Some(end) = self.end_content {
3672            input = input.end_content(end);
3673        }
3674
3675        if self.full_width {
3676            input = input.full_width();
3677        }
3678        if let Some(label) = self.label {
3679            input = input.label(label);
3680        }
3681        if let Some(description) = self.description {
3682            input = input.description(description);
3683        }
3684
3685        // React Aria reports `onClear` only for the clear affordance or its
3686        // Escape shortcut, never because ordinary editing reached "".
3687        input = input
3688            .clear_on_escape(true)
3689            .when_some(self.on_clear, |input, on_clear| input.on_clear(on_clear));
3690        if let Some(on_change) = self.on_change {
3691            input = input.on_change(move |text, window, cx| on_change(text, window, cx));
3692        }
3693
3694        if let Some(on_submit) = self.on_submit {
3695            input = input.on_submit(move |text, window, cx| on_submit(text, window, cx));
3696        }
3697
3698        // The `sx` slot rides the inner field, whose render applies it to the
3699        // root it returns — the same place a plain `Input`'s slot lands.
3700        input = input.sx_refinement(self.sx);
3701
3702        input.render(window, cx)
3703    }
3704}
3705
3706#[cfg(test)]
3707mod tests {
3708    use super::*;
3709
3710    /// Backspace, Delete and the arrow keys step by extended grapheme
3711    /// cluster: an emoji with a skin-tone modifier, a ZWJ family, a flag and
3712    /// a decomposed accent are each one caret stop and one deletion.
3713    #[gpui::test]
3714    fn editing_steps_by_grapheme_cluster(cx: &mut gpui::TestAppContext) {
3715        // "e" + COMBINING ACUTE, thumbs-up + skin tone, family ZWJ sequence,
3716        // the Ukrainian flag (two regional indicators).
3717        let value = "ae\u{301}b\u{1F44D}\u{1F3FD}\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{1F1FA}\u{1F1E6}";
3718        let state = cx.new(|cx| InputState::with_value(cx, value));
3719        cx.update(|cx| {
3720            state.update(cx, |s, _| {
3721                let len = s.value.chars().count();
3722                s.cursor = len;
3723                // One Backspace removes the whole flag.
3724                assert!(backspace(s));
3725                assert_eq!(s.value, value.trim_end_matches(['\u{1F1FA}', '\u{1F1E6}']));
3726                // One Left skips the whole ZWJ family; Delete removes it.
3727                move_left(s, false);
3728                assert!(delete(s));
3729                assert_eq!(s.value, "ae\u{301}b\u{1F44D}\u{1F3FD}");
3730                // Backspace takes the thumbs-up and its modifier together.
3731                move_right(s, false);
3732                assert!(backspace(s));
3733                assert_eq!(s.value, "ae\u{301}b");
3734                // Left from the end lands before "b", then before "é" as one
3735                // stop; Right returns past the combining mark, never inside.
3736                move_left(s, false);
3737                move_left(s, false);
3738                assert_eq!(s.cursor, 1);
3739                move_right(s, false);
3740                assert_eq!(s.cursor, 3);
3741                // Backspace removes the base letter and its accent at once.
3742                assert!(backspace(s));
3743                assert_eq!(s.value, "ab");
3744                assert_eq!(s.cursor, 1);
3745                // Shift+Right extends the selection by one cluster.
3746                move_right(s, true);
3747                assert_eq!(s.selection(), Some((1, 2)));
3748            });
3749        });
3750    }
3751
3752    #[gpui::test]
3753    fn platform_multi_character_insert_keeps_all_characters(cx: &mut gpui::TestAppContext) {
3754        use gpui::InputHandler;
3755        let cx = cx.add_empty_window();
3756        cx.update(|window, cx| {
3757            let state = cx.new(|cx| InputState::new(cx));
3758            let mut handler = PlatformTextInput {
3759                state: state.clone(),
3760                on_edit: crate::util::shared(|_, _, _| {}),
3761                input_type: InputType::Text,
3762                max_length: None,
3763                multiline: false,
3764                bounds: gpui::Bounds::default(),
3765                font: window.text_style().font(),
3766                paragraphs: cx.new(|_| Vec::new()),
3767            };
3768            handler.replace_text_in_range(None, "Go", window, cx);
3769            assert_eq!(state.read(cx).value(), "Go");
3770            assert_eq!(
3771                handler
3772                    .selected_text_range(false, window, cx)
3773                    .unwrap()
3774                    .range,
3775                2..2
3776            );
3777        });
3778    }
3779
3780    #[gpui::test]
3781    fn platform_composition_replaces_marked_utf16_ranges(cx: &mut gpui::TestAppContext) {
3782        use gpui::InputHandler;
3783        let cx = cx.add_empty_window();
3784        cx.update(|window, cx| {
3785            let state = cx.new(|cx| InputState::with_value(cx, "a😀z"));
3786            let mut handler = PlatformTextInput {
3787                state: state.clone(),
3788                on_edit: crate::util::shared(|_, _, _| {}),
3789                input_type: InputType::Text,
3790                max_length: None,
3791                multiline: false,
3792                bounds: gpui::Bounds::default(),
3793                font: window.text_style().font(),
3794                paragraphs: cx.new(|_| Vec::new()),
3795            };
3796            handler.set_selected_text_range(1..3, window, cx);
3797            handler.replace_and_mark_text_in_range(None, "に", Some(1..1), window, cx);
3798            assert_eq!(state.read(cx).value(), "aにz");
3799            assert_eq!(handler.marked_text_range(window, cx), Some(1..2));
3800            handler.replace_and_mark_text_in_range(None, "日本😀", Some(2..4), window, cx);
3801            assert_eq!(state.read(cx).value(), "a日本😀z");
3802            assert_eq!(handler.marked_text_range(window, cx), Some(1..5));
3803            assert_eq!(
3804                handler
3805                    .selected_text_range(false, window, cx)
3806                    .unwrap()
3807                    .range,
3808                3..5
3809            );
3810            let mut actual = None;
3811            assert_eq!(
3812                handler
3813                    .text_for_range(3..5, &mut actual, window, cx)
3814                    .as_deref(),
3815                Some("😀")
3816            );
3817            assert_eq!(actual, Some(3..5));
3818            handler.replace_text_in_range(None, "東京", window, cx);
3819            assert_eq!(state.read(cx).value(), "a東京z");
3820            assert_eq!(handler.marked_text_range(window, cx), None);
3821            assert_eq!(
3822                handler
3823                    .selected_text_range(false, window, cx)
3824                    .unwrap()
3825                    .range,
3826                3..3
3827            );
3828            handler.replace_text_in_range(Some(1..3), "", window, cx);
3829            assert_eq!(state.read(cx).value(), "az");
3830        });
3831    }
3832
3833    #[gpui::test]
3834    fn platform_edits_preserve_filters_and_read_only_state(cx: &mut gpui::TestAppContext) {
3835        use gpui::InputHandler;
3836        let cx = cx.add_empty_window();
3837        cx.update(|window, cx| {
3838            let state = cx.new(|cx| InputState::with_value(cx, "12"));
3839            let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
3840            let recorded = changes.clone();
3841            let mut handler = PlatformTextInput {
3842                state: state.clone(),
3843                on_edit: crate::util::shared(move |value, _, _| {
3844                    recorded.borrow_mut().push(value.to_owned());
3845                }),
3846                input_type: InputType::Number,
3847                max_length: Some(3),
3848                multiline: false,
3849                bounds: gpui::Bounds::default(),
3850                font: window.text_style().font(),
3851                paragraphs: cx.new(|_| Vec::new()),
3852            };
3853            handler.set_selected_text_range(0..2, window, cx);
3854            handler.replace_text_in_range(None, "abc", window, cx);
3855            assert_eq!(state.read(cx).value(), "12");
3856            assert_eq!(
3857                handler
3858                    .selected_text_range(false, window, cx)
3859                    .unwrap()
3860                    .range,
3861                0..2
3862            );
3863            handler.replace_text_in_range(None, "3x456", window, cx);
3864            assert_eq!(state.read(cx).value(), "345");
3865            assert_eq!(changes.borrow().as_slice(), ["345"]);
3866            state.update(cx, |state, _| state.set_read_only(true));
3867            handler.replace_text_in_range(Some(0..3), "7", window, cx);
3868            assert_eq!(state.read(cx).value(), "345");
3869            assert_eq!(changes.borrow().as_slice(), ["345"]);
3870            assert!(!handler.accepts_text_input(window, cx));
3871        });
3872    }
3873
3874    #[gpui::test]
3875    fn platform_geometry_uses_rendered_row_spacing_and_password_offsets(
3876        cx: &mut gpui::TestAppContext,
3877    ) {
3878        use gpui::InputHandler;
3879        let cx = cx.add_empty_window();
3880        cx.update(|window, cx| {
3881            let state = cx.new(|cx| InputState::with_value(cx, "😀secret"));
3882            let origin = gpui::point(px(30.), px(70.));
3883            let bounds = gpui::Bounds::new(origin, gpui::size(px(80.), px(20.)));
3884            let mut handler = PlatformTextInput {
3885                state: state.clone(),
3886                on_edit: crate::util::shared(|_, _, _| {}),
3887                input_type: InputType::Password,
3888                max_length: None,
3889                multiline: false,
3890                bounds,
3891                font: window.text_style().font(),
3892                paragraphs: cx.new(|_| vec![bounds]),
3893            };
3894            let caret = handler.bounds_for_range(2..2, window, cx).unwrap();
3895            assert!(caret.left() > origin.x);
3896            assert_eq!(caret.top(), origin.y);
3897            assert_eq!(
3898                handler.character_index_for_point(caret.origin, window, cx),
3899                Some(2)
3900            );
3901            state.update(cx, |state, _| state.set_value(""));
3902            assert_eq!(
3903                handler.bounds_for_range(0..0, window, cx).unwrap().origin,
3904                origin
3905            );
3906            state.update(cx, |state, _| {
3907                state.set_value("one two three four five six seven eight nine ten");
3908            });
3909            handler.input_type = InputType::Text;
3910            handler.multiline = true;
3911            let text = state.read(cx).value.clone();
3912            let run = gpui::TextRun {
3913                len: text.len(),
3914                font: handler.font.clone(),
3915                color: gpui::black(),
3916                background_color: None,
3917                underline: None,
3918                strikethrough: None,
3919            };
3920            let shaped = window
3921                .text_system()
3922                .shape_text(
3923                    text.clone().into(),
3924                    crate::util::FIELD_TEXT,
3925                    &[run],
3926                    Some(bounds.size.width),
3927                    None,
3928                )
3929                .unwrap();
3930            let third_row = (0..text.len())
3931                .find(|&index| {
3932                    shaped[0]
3933                        .position_for_index(index, px(20.))
3934                        .is_some_and(|point| point.y == px(40.))
3935                })
3936                .expect("fixture must wrap into at least three rows");
3937            let caret = handler
3938                .bounds_for_range(third_row..third_row, window, cx)
3939                .unwrap();
3940            assert_eq!(caret.top(), origin.y + px(40.));
3941            assert_eq!(caret.size.height, px(20.));
3942        });
3943    }
3944
3945    #[gpui::test]
3946    fn composition_enter_does_not_submit_or_insert_a_newline(cx: &mut gpui::TestAppContext) {
3947        struct View {
3948            state: Entity<InputState>,
3949            submitted: std::rc::Rc<std::cell::Cell<usize>>,
3950            multiline: bool,
3951        }
3952        impl Render for View {
3953            fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
3954                let submitted = self.submitted.clone();
3955                Input::new(self.state.clone())
3956                    .multiline(self.multiline)
3957                    .on_submit(move |_, _, _| {
3958                        submitted.set(submitted.get() + 1);
3959                    })
3960            }
3961        }
3962        cx.update(herogpui_theme::ThemeProvider::init);
3963        for multiline in [false, true] {
3964            let state = cx.new(|cx| InputState::with_value(cx, "ab日本z"));
3965            let submitted = std::rc::Rc::new(std::cell::Cell::new(0));
3966            let window = cx.add_window(|window, cx| {
3967                let focus = state.read(cx).focus_handle.clone();
3968                window.focus(&focus, cx);
3969                View {
3970                    state: state.clone(),
3971                    submitted: submitted.clone(),
3972                    multiline,
3973                }
3974            });
3975            let mut visual = gpui::VisualTestContext::from_window(window.into(), cx);
3976            visual.update(|window, cx| {
3977                state.update(cx, |state, cx| {
3978                    state.marked = Some(2..4);
3979                    state.cursor = 4;
3980                    cx.notify();
3981                });
3982                window.refresh();
3983            });
3984            visual.simulate_keystrokes("enter");
3985            assert_eq!(submitted.get(), 0);
3986            assert_eq!(
3987                state.read_with(&visual, |state, _| state.value.clone()),
3988                "ab日本z"
3989            );
3990            visual.simulate_keystrokes("escape");
3991            assert_eq!(
3992                state.read_with(&visual, |state, _| state.marked.clone()),
3993                None
3994            );
3995        }
3996    }
3997
3998    fn check(
3999        value: &str,
4000        ty: InputType,
4001        min_length: Option<usize>,
4002        min: Option<f64>,
4003        max: Option<f64>,
4004        step: Option<f64>,
4005    ) -> InputValidity {
4006        validate_value(value, ty, min_length, min, max, step, None)
4007    }
4008
4009    #[test]
4010    fn empty_is_valid_regardless_of_bounds() {
4011        // Emptiness is `is_required`'s business, not the bounds'.
4012        assert_eq!(
4013            check(
4014                "",
4015                InputType::Number,
4016                Some(5),
4017                Some(10.0),
4018                Some(20.0),
4019                Some(2.0)
4020            ),
4021            InputValidity::Valid
4022        );
4023    }
4024
4025    #[test]
4026    fn min_length_counts_chars_not_bytes() {
4027        assert_eq!(
4028            check("ab", InputType::Text, Some(3), None, None, None),
4029            InputValidity::TooShort
4030        );
4031        assert_eq!(
4032            check("abc", InputType::Text, Some(3), None, None, None),
4033            InputValidity::Valid
4034        );
4035        // Two-byte chars still count as one each.
4036        assert_eq!(
4037            check("éé", InputType::Text, Some(3), None, None, None),
4038            InputValidity::TooShort
4039        );
4040    }
4041
4042    #[test]
4043    fn numeric_bounds_are_inclusive() {
4044        let n = InputType::Number;
4045        assert_eq!(
4046            check("9", n, None, Some(10.0), None, None),
4047            InputValidity::BelowMin
4048        );
4049        assert_eq!(
4050            check("10", n, None, Some(10.0), None, None),
4051            InputValidity::Valid
4052        );
4053        assert_eq!(
4054            check("20", n, None, None, Some(20.0), None),
4055            InputValidity::Valid
4056        );
4057        assert_eq!(
4058            check("21", n, None, None, Some(20.0), None),
4059            InputValidity::AboveMax
4060        );
4061    }
4062
4063    #[test]
4064    fn bounds_are_ignored_for_non_numeric_types() {
4065        // A text field holding "5" is not subject to `min`.
4066        assert_eq!(
4067            check("5", InputType::Text, None, Some(10.0), None, None),
4068            InputValidity::Valid
4069        );
4070    }
4071
4072    #[test]
4073    fn unparsable_numeric_input_is_not_a_bounds_error() {
4074        assert_eq!(
4075            check("-", InputType::Number, None, Some(0.0), Some(9.0), None),
4076            InputValidity::Valid
4077        );
4078    }
4079
4080    #[test]
4081    fn step_is_measured_from_min() {
4082        let n = InputType::Number;
4083        // Steps of 3 from 1: 1, 4, 7 ...
4084        assert_eq!(
4085            check("4", n, None, Some(1.0), None, Some(3.0)),
4086            InputValidity::Valid
4087        );
4088        assert_eq!(
4089            check("5", n, None, Some(1.0), None, Some(3.0)),
4090            InputValidity::OffStep
4091        );
4092        // With no min, the base is 0.
4093        assert_eq!(
4094            check("6", n, None, None, None, Some(3.0)),
4095            InputValidity::Valid
4096        );
4097        assert_eq!(
4098            check("5", n, None, None, None, Some(3.0)),
4099            InputValidity::OffStep
4100        );
4101    }
4102
4103    #[test]
4104    fn fractional_steps_tolerate_float_error() {
4105        assert_eq!(
4106            check("0.3", InputType::Number, None, None, None, Some(0.1)),
4107            InputValidity::Valid
4108        );
4109    }
4110
4111    #[test]
4112    fn zero_step_is_ignored_rather_than_dividing_by_zero() {
4113        assert_eq!(
4114            check("5", InputType::Number, None, None, None, Some(0.0)),
4115            InputValidity::Valid
4116        );
4117    }
4118
4119    #[test]
4120    fn pattern_is_checked_before_the_other_rules() {
4121        let deny = |_: &str| false;
4122        assert_eq!(
4123            validate_value(
4124                "ab",
4125                InputType::Text,
4126                Some(10),
4127                None,
4128                None,
4129                None,
4130                Some(&deny)
4131            ),
4132            InputValidity::PatternMismatch
4133        );
4134        let allow = |v: &str| v.starts_with('a');
4135        assert_eq!(
4136            validate_value("abc", InputType::Text, None, None, None, None, Some(&allow)),
4137            InputValidity::Valid
4138        );
4139    }
4140
4141    #[test]
4142    fn validity_helper_reports_valid() {
4143        assert!(InputValidity::Valid.is_valid());
4144        assert!(!InputValidity::TooShort.is_valid());
4145    }
4146
4147    #[test]
4148    fn number_type_rejects_letters() {
4149        assert!(accepts_char(2, 0, '3', InputType::Number, None));
4150        assert!(accepts_char(2, 0, '-', InputType::Number, None));
4151        assert!(accepts_char(2, 0, '.', InputType::Number, None));
4152        assert!(!accepts_char(2, 0, 'a', InputType::Number, None));
4153    }
4154
4155    #[test]
4156    fn text_type_accepts_anything() {
4157        assert!(accepts_char(2, 0, 'c', InputType::Text, None));
4158        assert!(accepts_char(2, 0, '!', InputType::Text, None));
4159    }
4160
4161    #[test]
4162    fn no_max_length_never_blocks() {
4163        assert!(accepts_char(9999, 0, 'x', InputType::Text, None));
4164    }
4165
4166    #[test]
4167    fn max_length_blocks_at_the_limit() {
4168        assert!(accepts_char(2, 0, 'c', InputType::Text, Some(3)));
4169        assert!(!accepts_char(3, 0, 'd', InputType::Text, Some(3)));
4170        // Already over the limit (e.g. set_value bypassed the gate).
4171        assert!(!accepts_char(4, 0, 'd', InputType::Text, Some(3)));
4172    }
4173
4174    #[test]
4175    fn max_length_counts_a_replaced_selection_as_free() {
4176        // 3 chars, all selected: the keystroke replaces them, so it fits.
4177        assert!(accepts_char(3, 3, 'd', InputType::Text, Some(3)));
4178        // Only one selected: 2 survive, still room for one more.
4179        assert!(accepts_char(3, 1, 'd', InputType::Text, Some(3)));
4180    }
4181
4182    #[test]
4183    fn type_filter_wins_over_available_room() {
4184        assert!(!accepts_char(0, 0, 'a', InputType::Number, Some(10)));
4185    }
4186
4187    #[test]
4188    fn password_masking_preserves_char_count() {
4189        let value = "sécret";
4190        let masked = "•".repeat(value.chars().count());
4191        assert_eq!(masked.chars().count(), value.chars().count());
4192    }
4193
4194    #[test]
4195    fn word_motion_crosses_the_separators_then_the_word() {
4196        let v = "hello world, again";
4197        assert_eq!(word_target(v, 0, true), 5); // end of "hello"
4198        assert_eq!(word_target(v, 5, true), 11); // end of "world"
4199        assert_eq!(word_target(v, 11, false), 6); // start of "world"
4200        assert_eq!(word_target(v, 0, false), 0); // nowhere left to go
4201        assert_eq!(word_target(v, 18, true), 18);
4202    }
4203
4204    #[test]
4205    fn line_bounds_exclude_the_newlines() {
4206        let value = "one
4207two
4208three";
4209        assert_eq!(line_bounds(value, 0), (0, 3));
4210        assert_eq!(line_bounds(value, 5), (4, 7));
4211        assert_eq!(line_bounds(value, 13), (8, 13));
4212    }
4213
4214    #[test]
4215    fn vertical_motion_keeps_the_column_where_the_line_is_long_enough() {
4216        let v = "hello
4217hi
4218world";
4219        assert_eq!(vertical_target(v, 3, true), 8); // "hi" is shorter: its end
4220        assert_eq!(vertical_target(v, 8, true), 11); // column 2 of "world"
4221        assert_eq!(vertical_target(v, 11, false), 8);
4222    }
4223
4224    #[test]
4225    fn vertical_motion_runs_to_the_ends_at_the_edges() {
4226        let v = "one
4227two";
4228        assert_eq!(vertical_target(v, 1, false), 0);
4229        assert_eq!(vertical_target(v, 5, true), 7);
4230    }
4231
4232    #[test]
4233    fn a_selection_slices_on_char_boundaries() {
4234        assert_eq!(
4235            slice_selection("hello world", Some((6, 11))).as_deref(),
4236            Some("world")
4237        );
4238        // Multi-byte: the indices are chars, not bytes.
4239        assert_eq!(
4240            slice_selection("sécret", Some((0, 2))).as_deref(),
4241            Some("sé")
4242        );
4243        assert_eq!(slice_selection("hello", None), None);
4244    }
4245}
4246
4247// The pinned `.search-field__clear-button` composes `.close-button`, whose
4248// hover fills `bg-default-hover`; a hand-mixed wash looks plausible on
4249// screen, so the check is mechanical.
4250#[cfg(test)]
4251mod hover_tokens {
4252    #[test]
4253    fn the_field_uses_the_pinned_hover_transition() {
4254        let source = include_str!("input.rs")
4255            .split("#[cfg(test)]")
4256            .next()
4257            .expect("the implementation section is always present");
4258        assert!(
4259            source.contains("colors.field.hover()")
4260                && source.contains("colors.default.hover()")
4261                && source.contains("colors.field.border_hover()")
4262        );
4263        assert!(
4264            source.contains("hover_fade_with_duration_and_easing")
4265                && source.contains("HoverFadeEasing::EaseSmooth")
4266                && source.contains("Some(150)")
4267        );
4268    }
4269
4270    #[test]
4271    fn the_clear_button_hovers_the_role_hover_token() {
4272        // Scan the implementation only; this test's own text names the
4273        // forbidden accessor.
4274        let source = include_str!("input.rs")
4275            .split("#[cfg(test)]")
4276            .next()
4277            .expect("the implementation section is always present");
4278        assert!(
4279            source.contains(".hover(move |s| s.bg(clear_hover_bg))")
4280                && source.contains(
4281                    "let clear_hover_bg = self.clear_hover_bg.unwrap_or(colors.default.hover());"
4282                ),
4283            "the clear button must hover `bg-default-hover` \
4284             (pinned `.close-button:hover`)"
4285        );
4286        assert!(
4287            !source.contains("with_alpha(0.15)"),
4288            "the clear button must not hover a hand-mixed wash"
4289        );
4290    }
4291}
4292
4293crate::util::impl_component_styled!(Input, SearchField);