1use std::rc::Rc;
2
3use gpui::{
4 App, DefiniteLength, Entity, IntoElement, RenderOnce, SharedString, StyleRefinement, Styled,
5 Window, prelude::FluentBuilder as _,
6};
7
8use super::{Input, TextareaState};
9use crate::native_menu::NativeMenu;
10use crate::{RoleOverride, Sizable, Size, StyledExt as _};
11
12#[derive(IntoElement)]
14pub struct Textarea {
15 token_renderer: Option<gpui_base::input::InlineTokenRenderer>,
16 token_click_listener: Option<gpui_base::input::InlineTokenClickListener>,
17 state: Entity<TextareaState>,
18 style: StyleRefinement,
19 size: Size,
20 height: Option<DefiniteLength>,
21 appearance: bool,
22 bordered: bool,
23 disabled: bool,
24 readonly: bool,
25 tab_index: isize,
26 role: RoleOverride,
27 accessibility_id: Option<SharedString>,
28 aria_label: Option<SharedString>,
29
30 context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
34
35 paste_handler: Option<Rc<dyn Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool>>,
36}
37
38impl Textarea {
39 pub fn token<R: IntoElement>(
43 mut self,
44 render: impl Fn(&super::InlineTokenContext, &mut Window, &mut App) -> R + 'static,
45 ) -> Self {
46 self.token_renderer = Some(Rc::new(move |token, window, cx| {
47 render(token, window, cx).into_any_element()
48 }));
49 self
50 }
51 pub fn on_token_click(
53 mut self,
54 listener: impl Fn(&super::InlineTokenClickEvent, &mut Window, &mut App) + 'static,
55 ) -> Self {
56 self.token_click_listener = Some(Rc::new(listener));
57 self
58 }
59
60 pub fn new(state: &Entity<TextareaState>) -> Self {
61 Self {
62 state: state.clone(),
63 style: StyleRefinement::default(),
64 size: Size::default(),
65 height: None,
66 appearance: true,
67 bordered: true,
68 disabled: false,
69 readonly: false,
70 tab_index: 0,
71 role: RoleOverride::default(),
72 accessibility_id: None,
73 aria_label: None,
74 context_menu_builder: None,
75 paste_handler: None,
76 token_renderer: None,
77 token_click_listener: None,
78 }
79 }
80
81 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
82 self.height = Some(height.into());
83 self
84 }
85
86 pub fn appearance(mut self, appearance: bool) -> Self {
87 self.appearance = appearance;
88 self
89 }
90
91 pub fn bordered(mut self, bordered: bool) -> Self {
92 self.bordered = bordered;
93 self
94 }
95
96 pub fn disabled(mut self, disabled: bool) -> Self {
97 self.disabled = disabled;
98 self
99 }
100
101 pub fn readonly(mut self, readonly: bool) -> Self {
107 self.readonly = readonly;
108 self
109 }
110
111 pub fn tab_index(mut self, index: isize) -> Self {
112 self.tab_index = index;
113 self
114 }
115
116 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
117 self.role = role.into();
118 self
119 }
120
121 pub fn accessibility_id(mut self, id: impl Into<SharedString>) -> Self {
123 self.accessibility_id = Some(id.into());
124 self
125 }
126
127 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
128 self.aria_label = Some(label.into());
129 self
130 }
131
132 pub fn context_menu(
139 mut self,
140 f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
141 ) -> Self {
142 self.context_menu_builder = Some(Rc::new(f));
143 self
144 }
145
146 pub fn on_paste(
153 mut self,
154 handler: impl Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool + 'static,
155 ) -> Self {
156 self.paste_handler = Some(Rc::new(handler));
157 self
158 }
159}
160
161impl Sizable for Textarea {
162 fn with_size(mut self, size: impl Into<Size>) -> Self {
163 self.size = size.into();
164 self
165 }
166}
167
168impl Styled for Textarea {
169 fn style(&mut self) -> &mut StyleRefinement {
170 &mut self.style
171 }
172}
173
174impl Textarea {
175 pub(crate) fn into_input(self) -> Input {
178 Input::from_state(self.state.clone())
179 .when_some(self.token_renderer, |this, render| {
180 this.token(move |token, window, cx| render(token, window, cx))
181 })
182 .when_some(self.token_click_listener, |this, listener| {
183 this.on_token_click(move |event, window, cx| listener(event, window, cx))
184 })
185 .appearance(self.appearance)
186 .bordered(self.bordered)
187 .disabled(self.disabled)
188 .readonly(self.readonly)
189 .tab_index(self.tab_index)
190 .role(self.role)
191 .with_size(self.size)
192 .when_some(self.height, |this, height| this.h(height))
193 .when_some(self.accessibility_id, |this, id| this.accessibility_id(id))
194 .when_some(self.aria_label, |this, label| this.aria_label(label))
195 .when_some(self.context_menu_builder, |this, build| {
196 this.context_menu(move |menu, window, cx| build(menu, window, cx))
197 })
198 .when_some(self.paste_handler, |this, handler| {
199 this.on_paste(move |item, window, cx| handler(item, window, cx))
200 })
201 .refine_style(&self.style)
202 }
203}
204
205impl RenderOnce for Textarea {
206 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
207 self.into_input()
208 }
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[gpui::test]
216 fn test_on_paste_builder(cx: &mut gpui::TestAppContext) {
217 use gpui::{AppContext as _, Render};
218
219 struct Probe;
220 impl Render for Probe {
221 fn render(
222 &mut self,
223 _: &mut Window,
224 _: &mut gpui::Context<Self>,
225 ) -> impl gpui::IntoElement {
226 gpui::div()
227 }
228 }
229
230 cx.update(crate::init);
231 let _ = cx.add_window_view(|window, cx| {
232 let state = cx.new(|cx| TextareaState::new(window, cx));
233 assert!(Textarea::new(&state).paste_handler.is_none());
234 let textarea = Textarea::new(&state).on_paste(|_, _, _| true);
235 assert!(textarea.paste_handler.is_some());
236 Probe
237 });
238 }
239}