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
//! Textarea module.
//!
//! This public module implements the Liora multi-line text input component. It keeps the reusable
//! component logic inside `liora-components` rather than Gallery or Docs so
//! downstream GPUI applications can compose the same behavior with their own
//! app state, assets, and release policy.
//!
//! ## Usage model
//!
//! Components in this module render native GPUI element trees. Stateless builder
//! values can be constructed inline, while controls with focus, selection,
//! popup, drag, or editing state should be stored as `gpui::Entity<T>` fields in
//! the parent view so state survives GPUI render passes.
//!
//! ## Design contract
//!
//! The implementation should use Liora theme tokens from `liora-core` and
//! `liora-theme`, keep accessibility-oriented keyboard/pointer behavior close to
//! the component, and avoid app-specific Gallery/Docs resources in this SDK
//! crate.
use crate::Input;
use gpui::{
App, Context, Entity, FocusHandle, Focusable, Render, SharedString, Window, prelude::*, px,
};
use liora_core::Config;
/// Fluent native GPUI component for rendering Liora textarea.
pub struct Textarea {
input: Entity<Input>,
rows: usize,
max_length: Option<usize>,
focus_handle: FocusHandle,
}
impl Textarea {
/// Creates `Textarea` initialized from the supplied value.
pub fn new(value: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
let value = value.into();
let rows = 1;
let input = cx.new(|cx| Input::new(value, cx).min_rows(rows));
Self {
input,
rows,
max_length: None,
focus_handle: cx.focus_handle(),
}
}
/// Sets the visible row count for editor-like controls.
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows;
self
}
/// Uses the supplied placeholder text when the value is empty.
pub fn placeholder(self, p: impl Into<SharedString>, cx: &mut Context<Self>) -> Self {
self.input.update(cx, |input, cx| {
input.set_placeholder(p, cx);
});
self
}
/// Toggles the disabled state and suppresses user interaction when enabled.
pub fn disabled(self, d: bool, cx: &mut Context<Self>) -> Self {
self.input.update(cx, |input, cx| {
input.set_disabled(d, cx);
});
self
}
/// Limits the number of characters accepted by the input.
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
}
impl Focusable for Textarea {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for Textarea {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
let value = self.input.read(cx).value();
let len = value.chars().count();
let rows = self.rows;
// Sync rows to inner input if changed
self.input.update(cx, |input, cx| {
if input.min_rows != rows {
input.set_min_rows(rows, cx);
}
});
gpui::div()
.flex()
.flex_col()
.gap_1()
.child(self.input.clone())
.when_some(self.max_length, |this, max| {
this.child(
gpui::div()
.flex()
.justify_end()
.px(px(4.0))
.text_size(px(theme.font_size.sm))
.text_color(if len > max {
theme.danger.base
} else {
theme.neutral.text_3
})
.child(format!("{}/{}", len, max)),
)
})
}
}