Skip to main content

gpui_component/input/
textarea.rs

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/// A styled ordinary multi-line text field.
13#[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    /// An optional context menu builder to allow a custom context menu.
31    ///
32    /// If set, this overrides the built-in context menu.
33    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    /// The element each atomic inline token renders as, in place of the default
40    /// [`InputToken`](super::InputToken); editing and history stay
41    /// with the input.
42    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    /// Open a reference after a completed, unconsumed token click.
52    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    /// Set the textarea to read-only, default is `false`.
102    ///
103    /// Unlike [`Self::disabled`], a read-only textarea keeps the normal appearance
104    /// and still can be focused, selected and copied, it only rejects the changes
105    /// made by the user.
106    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    /// Set the developer-assigned accessibility identifier.
122    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    /// Replace the built-in context menu shown on right-click.
133    ///
134    /// The closure receives an empty menu and returns the one to show, so it
135    /// decides entirely what appears — the default items are not added. It
136    /// shows only while the state's context menu is enabled, which is the
137    /// default.
138    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    /// Intercept paste payloads (images, files) before the default text insertion.
147    ///
148    /// `true` consumes the paste so nothing is inserted, `false` falls through
149    /// to `clipboard.text()`. Copied files arrive as `ExternalPaths` through
150    /// the same hook. On web the clipboard reads `None`; image paste needs
151    /// async clipboard access and is out of scope.
152    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    /// The [`Input`] this textarea renders, for a compound control that frames
176    /// it.
177    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}