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