Skip to main content

herogpui_components/
textarea.rs

1//! TextArea — port of `@heroui/text-area` (v3).
2//!
3//! Reuses [`InputState`] in multi-line mode: gpui wraps text by default
4//! (`WhiteSpace::Normal`), so the field lays one wrapping paragraph out per
5//! newline, Enter inserts a newline instead of submitting, and the caret is
6//! placed inside the line it falls in. `rows` sets the visible height.
7
8use crate::input::{Input, InputState};
9use gpui::{prelude::*, px, App, Entity, IntoElement, RenderOnce, SharedString, Styled, Window};
10
11/// Multi-line text field.
12#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
13#[derive(IntoElement)]
14pub struct TextArea {
15    inner: Input,
16    /// `cols`, as a pixel width. `None` leaves the field's natural width.
17    min_w: Option<gpui::Pixels>,
18    /// The `sx` slot, refined over the root style at the end of render.
19    sx: Option<Box<gpui::StyleRefinement>>,
20}
21
22/// `rows` as a height: one 20px line each, over `.textarea`'s `py-2`, with
23/// HeroUI's 38px minimum floor for even a one-row textarea.
24fn rows_height(rows: u32) -> gpui::Pixels {
25    px((rows.max(1) as f32 * 20.0 + 16.0).max(38.0))
26}
27
28impl TextArea {
29    /// `value` — v3's controlled-value spelling, forwarded to the inner field;
30    /// see [`crate::input::Input::value`]. A later value is an imperative
31    /// update: `state.update(cx, |s, _| s.set_value(..))`.
32    pub fn value(mut self, value: impl Into<SharedString>) -> Self {
33        self.inner = self.inner.value(value);
34        self
35    }
36
37    /// `maxLength` — refuses keystrokes past this many characters.
38    pub fn max_length(mut self, n: usize) -> Self {
39        self.inner = self.inner.max_length(n);
40        self
41    }
42
43    /// `minLength` — reported by the inner field's `validity`.
44    pub fn min_length(mut self, n: usize) -> Self {
45        self.inner = self.inner.min_length(n);
46        self
47    }
48
49    /// `cols` — visible width, in characters.
50    ///
51    /// gpui has no `ch` unit, so this is the column count times the size's
52    /// character advance, the same approximation [`TextArea::rows`] makes for
53    /// height.
54    pub fn cols(mut self, cols: u32) -> Self {
55        self.min_w = Some(px(cols.max(1) as f32 * 8.0));
56        self
57    }
58
59    /// `rows` — the visible line count, at one line height per row plus the
60    /// field's vertical padding.
61    pub fn rows(mut self, rows: u32) -> Self {
62        self.inner = self.inner.min_h(rows_height(rows));
63        self
64    }
65
66    /// `name` — see [`crate::input::Input::name`].
67    pub fn name(mut self, name: impl Into<SharedString>) -> Self {
68        self.inner = self.inner.name(name);
69        self
70    }
71
72    /// `defaultValue` — see [`crate::input::Input::default_value`].
73    pub fn default_value(mut self, text: impl Into<SharedString>) -> Self {
74        self.inner = self.inner.default_value(text);
75        self
76    }
77
78    /// `validationBehavior` — see [`crate::input::Input::validation_behavior`].
79    pub fn validation_behavior(mut self, behavior: crate::form::ValidationBehavior) -> Self {
80        self.inner = self.inner.validation_behavior(behavior);
81        self
82    }
83
84    /// Sets the field variant (v3 `variant`).
85    pub fn variant(mut self, variant: herogpui_core::FieldVariant) -> Self {
86        self.inner = self.inner.variant(variant);
87        self
88    }
89
90    /// Fills the parent's width.
91    pub fn full_width(mut self) -> Self {
92        self.inner = self.inner.full_width();
93        self
94    }
95
96    /// The box's horizontal padding — see [`crate::input::Input::padding_x`].
97    ///
98    /// The multi-line box keeps its own `py-2` and its `rows`-derived height,
99    /// so only the horizontal padding is a knob here.
100    pub fn padding_x(mut self, p: impl Into<gpui::Pixels>) -> Self {
101        self.inner = self.inner.padding_x(p);
102        self
103    }
104
105    /// Drops the field's chrome — see [`crate::input::Input::is_bare`].
106    pub fn is_bare(mut self, v: bool) -> Self {
107        self.inner = self.inner.is_bare(v);
108        self
109    }
110
111    /// Shows or hides only the inner field's visual focus ring — see
112    /// [`crate::input::Input::focus_ring`].
113    pub fn focus_ring(mut self, v: bool) -> Self {
114        self.inner = self.inner.focus_ring(v);
115        self
116    }
117
118    /// The field text's font family — see
119    /// [`crate::input::Input::font_family`].
120    pub fn font_family(mut self, family: impl Into<SharedString>) -> Self {
121        self.inner = self.inner.font_family(family);
122        self
123    }
124
125    /// The one slot for caller-owned low-level styling: GPUI's styling methods
126    /// (`bg`, `text_color`, `w`, `h`, `p`, `rounded`, `border_color`, …)
127    /// applied to the wrapper around the field after every value the variant
128    /// and the active theme chose, so they win. The field paints its own
129    /// chrome, so this reaches the box that chrome sits in, not the chrome.
130    pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
131        crate::util::refine_sx(&mut self.sx, style);
132        self
133    }
134
135    /// Sets the disabled state (v3 `isDisabled`).
136    pub fn is_disabled(mut self, v: bool) -> Self {
137        self.inner = self.inner.is_disabled(v);
138        self
139    }
140
141    /// Sets the read-only state (v3 `isReadOnly`).
142    pub fn is_read_only(mut self, v: bool) -> Self {
143        self.inner = self.inner.is_read_only(v);
144        self
145    }
146
147    /// Sets the required state (v3 `isRequired`).
148    pub fn is_required(mut self, v: bool) -> Self {
149        self.inner = self.inner.is_required(v);
150        self
151    }
152
153    /// Sets the invalid state (v3 `isInvalid`).
154    pub fn is_invalid(mut self, v: bool) -> Self {
155        self.inner = self.inner.is_invalid(v);
156        self
157    }
158
159    /// `validate` — see [`crate::input::Input::validate`].
160    pub fn validate(mut self, f: impl Fn(&str) -> Option<SharedString> + 'static) -> Self {
161        self.inner = self.inner.validate(f);
162        self
163    }
164
165    /// `validationErrors` — see [`crate::input::Input::validation_errors`].
166    pub fn validation_errors(
167        mut self,
168        errors: impl IntoIterator<Item = impl Into<SharedString>>,
169    ) -> Self {
170        self.inner = self.inner.validation_errors(errors);
171        self
172    }
173
174    /// Sets the error message text.
175    pub fn error_message(mut self, text: impl Into<SharedString>) -> Self {
176        self.inner = self.inner.error_message(text);
177        self
178    }
179
180    /// Sets the handler called with the new text on change.
181    pub fn on_change(mut self, handler: impl Fn(&str, &mut Window, &mut App) + 'static) -> Self {
182        self.inner = self.inner.on_change(handler);
183        self
184    }
185
186    /// Builds the multi-line field over `state`, borrowed or owned — see
187    /// [`crate::input::Input::new`].
188    pub fn new(state: impl std::borrow::Borrow<Entity<InputState>>) -> Self {
189        Self {
190            // v3 documents `rows` as defaulting to 3.
191            inner: Input::new(state).multiline(true).min_h(rows_height(3)),
192            min_w: None,
193            sx: None,
194        }
195    }
196
197    /// Sets the label.
198    pub fn label(mut self, l: impl Into<SharedString>) -> Self {
199        self.inner = self.inner.label(l);
200        self
201    }
202
203    /// Sets the placeholder text.
204    pub fn placeholder(mut self, p: impl Into<SharedString>) -> Self {
205        self.inner = self.inner.placeholder(p);
206        self
207    }
208
209    /// Sets the description.
210    pub fn description(mut self, d: impl Into<SharedString>) -> Self {
211        self.inner = self.inner.description(d);
212        self
213    }
214}
215
216impl TextArea {
217    /// Hands the inner field to [`crate::input_group::InputGroup::text_area`].
218    ///
219    /// `InputGroup.TextArea` is the same multi-line field with the group's
220    /// chrome instead of its own, so the wrapper this normally renders (which
221    /// only carries `cols`) is dropped.
222    pub(crate) fn into_group_input(self) -> Input {
223        self.inner
224    }
225}
226
227impl RenderOnce for TextArea {
228    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
229        // The field itself is multi-line; this wrapper only gives it the height
230        // `rows` asks for and keeps the text at the top of it. The field paints
231        // its own chrome (`util::apply_field_chrome`) -- the wrapper used to
232        // repaint a `default.soft()` background at a hardcoded 10px radius,
233        // neither of which is a v3 value.
234        let el = gpui::div()
235            .flex()
236            .flex_col()
237            .items_start()
238            .when_some(self.min_w, |e, w| e.min_w(w))
239            .child(self.inner);
240        crate::util::apply_sx(el, &self.sx)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::rows_height;
247    use gpui::px;
248
249    #[test]
250    fn rows_keep_the_pinned_textarea_minimum() {
251        assert_eq!(rows_height(0), px(38.));
252        assert_eq!(rows_height(1), px(38.));
253        assert_eq!(rows_height(3), px(76.));
254    }
255}
256
257crate::util::impl_component_styled!(TextArea);