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, StyledExt as _};
11
12/// A styled ordinary multi-line text field.
13#[derive(IntoElement)]
14pub struct Textarea {
15    state: Entity<TextareaState>,
16    style: StyleRefinement,
17    height: Option<DefiniteLength>,
18    appearance: bool,
19    bordered: bool,
20    disabled: bool,
21    readonly: bool,
22    tab_index: isize,
23    role: RoleOverride,
24    accessibility_id: Option<SharedString>,
25    aria_label: Option<SharedString>,
26
27    /// An optional context menu builder to allow a custom context menu.
28    ///
29    /// If set, this overrides the built-in context menu.
30    context_menu_builder: Option<Rc<dyn Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu>>,
31
32    paste_handler: Option<Rc<dyn Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool>>,
33}
34
35impl Textarea {
36    pub fn new(state: &Entity<TextareaState>) -> Self {
37        Self {
38            state: state.clone(),
39            style: StyleRefinement::default(),
40            height: None,
41            appearance: true,
42            bordered: true,
43            disabled: false,
44            readonly: false,
45            tab_index: 0,
46            role: RoleOverride::default(),
47            accessibility_id: None,
48            aria_label: None,
49            context_menu_builder: None,
50            paste_handler: None,
51        }
52    }
53
54    pub fn h(mut self, height: impl Into<DefiniteLength>) -> Self {
55        self.height = Some(height.into());
56        self
57    }
58
59    pub fn appearance(mut self, appearance: bool) -> Self {
60        self.appearance = appearance;
61        self
62    }
63
64    pub fn bordered(mut self, bordered: bool) -> Self {
65        self.bordered = bordered;
66        self
67    }
68
69    pub fn disabled(mut self, disabled: bool) -> Self {
70        self.disabled = disabled;
71        self
72    }
73
74    /// Set the textarea to read-only, default is `false`.
75    ///
76    /// Unlike [`Self::disabled`], a read-only textarea keeps the normal appearance
77    /// and still can be focused, selected and copied, it only rejects the changes
78    /// made by the user.
79    pub fn readonly(mut self, readonly: bool) -> Self {
80        self.readonly = readonly;
81        self
82    }
83
84    pub fn tab_index(mut self, index: isize) -> Self {
85        self.tab_index = index;
86        self
87    }
88
89    pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
90        self.role = role.into();
91        self
92    }
93
94    /// Set the developer-assigned accessibility identifier.
95    pub fn accessibility_id(mut self, id: impl Into<SharedString>) -> Self {
96        self.accessibility_id = Some(id.into());
97        self
98    }
99
100    pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
101        self.aria_label = Some(label.into());
102        self
103    }
104
105    /// Replace the built-in context menu shown on right-click.
106    ///
107    /// The closure receives an empty menu and returns the one to show, so it
108    /// decides entirely what appears — the default items are not added.
109    pub fn context_menu(
110        mut self,
111        f: impl Fn(NativeMenu, &mut Window, &mut App) -> NativeMenu + 'static,
112    ) -> Self {
113        self.context_menu_builder = Some(Rc::new(f));
114        self
115    }
116
117    /// Intercept paste payloads (images, files) before the default text insertion.
118    ///
119    /// `true` consumes the paste so nothing is inserted, `false` falls through
120    /// to `clipboard.text()`. Copied files arrive as `ExternalPaths` through
121    /// the same hook. On web the clipboard reads `None`; image paste needs
122    /// async clipboard access and is out of scope.
123    pub fn on_paste(
124        mut self,
125        handler: impl Fn(&gpui::ClipboardItem, &mut Window, &mut App) -> bool + 'static,
126    ) -> Self {
127        self.paste_handler = Some(Rc::new(handler));
128        self
129    }
130}
131
132impl Styled for Textarea {
133    fn style(&mut self) -> &mut StyleRefinement {
134        &mut self.style
135    }
136}
137
138impl Textarea {
139    /// The [`Input`] this textarea renders, for a compound control that frames
140    /// it.
141    pub(crate) fn into_input(self) -> Input {
142        Input::from_state(self.state.clone())
143            .appearance(self.appearance)
144            .bordered(self.bordered)
145            .disabled(self.disabled)
146            .readonly(self.readonly)
147            .tab_index(self.tab_index)
148            .role(self.role)
149            .when_some(self.height, |this, height| this.h(height))
150            .when_some(self.accessibility_id, |this, id| this.accessibility_id(id))
151            .when_some(self.aria_label, |this, label| this.aria_label(label))
152            .when_some(self.context_menu_builder, |this, build| {
153                this.context_menu(move |menu, window, cx| build(menu, window, cx))
154            })
155            .when_some(self.paste_handler, |this, handler| {
156                this.on_paste(move |item, window, cx| handler(item, window, cx))
157            })
158            .refine_style(&self.style)
159    }
160}
161
162impl RenderOnce for Textarea {
163    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
164        self.into_input()
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[gpui::test]
173    fn test_on_paste_builder(cx: &mut gpui::TestAppContext) {
174        use gpui::{AppContext as _, Render};
175
176        struct Probe;
177        impl Render for Probe {
178            fn render(
179                &mut self,
180                _: &mut Window,
181                _: &mut gpui::Context<Self>,
182            ) -> impl gpui::IntoElement {
183                gpui::div()
184            }
185        }
186
187        cx.update(crate::init);
188        let _ = cx.add_window_view(|window, cx| {
189            let state = cx.new(|cx| TextareaState::new(window, cx));
190            assert!(Textarea::new(&state).paste_handler.is_none());
191            let textarea = Textarea::new(&state).on_paste(|_, _, _| true);
192            assert!(textarea.paste_handler.is_some());
193            Probe
194        });
195    }
196}