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 token_hover_listener: Option<gpui_base::input::InlineTokenHoverListener>,
18 state: Entity<TextareaState>,
19 style: StyleRefinement,
20 size: Size,
21 height: Option<DefiniteLength>,
22 appearance: bool,
23 bordered: bool,
24 disabled: bool,
25 readonly: bool,
26 tab_index: isize,
27 role: RoleOverride,
28 accessibility_id: Option<SharedString>,
29 aria_label: Option<SharedString>,
30
31 context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
35
36 paste_handler: Option<Rc<dyn Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool>>,
37}
38
39impl Textarea {
40 pub fn token<R: IntoElement>(
44 mut self,
45 render: impl Fn(&super::InlineTokenContext, &mut Window, &mut App) -> R + 'static,
46 ) -> Self {
47 self.token_renderer = Some(Rc::new(move |token, window, cx| {
48 render(token, window, cx).into_any_element()
49 }));
50 self
51 }
52 pub fn on_token_click(
54 mut self,
55 listener: impl Fn(&super::InlineTokenClickEvent, &mut Window, &mut App) + 'static,
56 ) -> Self {
57 self.token_click_listener = Some(Rc::new(listener));
58 self
59 }
60 pub fn on_token_hover(
63 mut self,
64 listener: impl Fn(&super::InlineTokenHoverEvent, &mut Window, &mut App) + 'static,
65 ) -> Self {
66 self.token_hover_listener = Some(Rc::new(listener));
67 self
68 }
69
70 pub fn new(state: &Entity<TextareaState>) -> Self {
71 Self {
72 state: state.clone(),
73 style: StyleRefinement::default(),
74 size: Size::default(),
75 height: None,
76 appearance: true,
77 bordered: true,
78 disabled: false,
79 readonly: false,
80 tab_index: 0,
81 role: RoleOverride::default(),
82 accessibility_id: None,
83 aria_label: None,
84 context_menu_builder: None,
85 paste_handler: None,
86 token_renderer: None,
87 token_click_listener: None,
88 token_hover_listener: None,
89 }
90 }
91
92 pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
93 self.height = Some(height.into());
94 self
95 }
96
97 pub fn appearance(mut self, appearance: bool) -> Self {
98 self.appearance = appearance;
99 self
100 }
101
102 pub fn bordered(mut self, bordered: bool) -> Self {
103 self.bordered = bordered;
104 self
105 }
106
107 pub fn disabled(mut self, disabled: bool) -> Self {
108 self.disabled = disabled;
109 self
110 }
111
112 pub fn readonly(mut self, readonly: bool) -> Self {
118 self.readonly = readonly;
119 self
120 }
121
122 pub fn tab_index(mut self, index: isize) -> Self {
123 self.tab_index = index;
124 self
125 }
126
127 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
128 self.role = role.into();
129 self
130 }
131
132 pub fn accessibility_id(mut self, id: impl Into<SharedString>) -> Self {
134 self.accessibility_id = Some(id.into());
135 self
136 }
137
138 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
139 self.aria_label = Some(label.into());
140 self
141 }
142
143 pub fn context_menu(
150 mut self,
151 f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
152 ) -> Self {
153 self.context_menu_builder = Some(Rc::new(f));
154 self
155 }
156
157 pub fn on_paste(
164 mut self,
165 handler: impl Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool + 'static,
166 ) -> Self {
167 self.paste_handler = Some(Rc::new(handler));
168 self
169 }
170}
171
172impl Sizable for Textarea {
173 fn with_size(mut self, size: impl Into<Size>) -> Self {
174 self.size = size.into();
175 self
176 }
177}
178
179impl Styled for Textarea {
180 fn style(&mut self) -> &mut StyleRefinement {
181 &mut self.style
182 }
183}
184
185impl Textarea {
186 pub(crate) fn into_input(self) -> Input {
189 Input::from_state(self.state.clone())
190 .when_some(self.token_renderer, |this, render| {
191 this.token(move |token, window, cx| render(token, window, cx))
192 })
193 .when_some(self.token_click_listener, |this, listener| {
194 this.on_token_click(move |event, window, cx| listener(event, window, cx))
195 })
196 .when_some(self.token_hover_listener, |this, listener| {
197 this.on_token_hover(move |event, window, cx| listener(event, window, cx))
198 })
199 .appearance(self.appearance)
200 .bordered(self.bordered)
201 .disabled(self.disabled)
202 .readonly(self.readonly)
203 .tab_index(self.tab_index)
204 .role(self.role)
205 .with_size(self.size)
206 .when_some(self.height, |this, height| this.h(height))
207 .when_some(self.accessibility_id, |this, id| this.accessibility_id(id))
208 .when_some(self.aria_label, |this, label| this.aria_label(label))
209 .when_some(self.context_menu_builder, |this, build| {
210 this.context_menu(move |menu, window, cx| build(menu, window, cx))
211 })
212 .when_some(self.paste_handler, |this, handler| {
213 this.on_paste(move |item, window, cx| handler(item, window, cx))
214 })
215 .refine_style(&self.style)
216 }
217}
218
219impl RenderOnce for Textarea {
220 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
221 self.into_input()
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[gpui::test]
230 fn test_on_paste_builder(cx: &mut gpui::TestAppContext) {
231 use gpui::{AppContext as _, Render};
232
233 struct Probe;
234 impl Render for Probe {
235 fn render(
236 &mut self,
237 _: &mut Window,
238 _: &mut gpui::Context<Self>,
239 ) -> impl gpui::IntoElement {
240 gpui::div()
241 }
242 }
243
244 cx.update(crate::init);
245 let _ = cx.add_window_view(|window, cx| {
246 let state = cx.new(|cx| TextareaState::new(window, cx));
247 assert!(Textarea::new(&state).paste_handler.is_none());
248 let textarea = Textarea::new(&state).on_paste(|_, _, _| true);
249 assert!(textarea.paste_handler.is_some());
250 Probe
251 });
252 }
253
254 #[gpui::test]
255 fn test_on_token_hover_builder(cx: &mut gpui::TestAppContext) {
256 use gpui::{AppContext as _, Render};
257
258 struct Probe;
259 impl Render for Probe {
260 fn render(
261 &mut self,
262 _: &mut Window,
263 _: &mut gpui::Context<Self>,
264 ) -> impl gpui::IntoElement {
265 gpui::div()
266 }
267 }
268
269 cx.update(crate::init);
270 let _ = cx.add_window_view(|window, cx| {
271 let state = cx.new(|cx| TextareaState::new(window, cx));
272 assert!(Textarea::new(&state).token_hover_listener.is_none());
273 let textarea = Textarea::new(&state).on_token_hover(|_, _, _| {});
274 assert!(textarea.token_hover_listener.is_some());
275 Probe
276 });
277 }
278}