intelli_shell/widgets/
textarea.rs

1use std::{
2    borrow::Cow,
3    ops::{Deref, DerefMut},
4};
5
6use ratatui::{
7    buffer::Buffer,
8    layout::{Constraint, Layout, Rect},
9    style::{Modifier, Style},
10    text::Text,
11    widgets::{Block, Borders, Widget},
12};
13use tui_textarea::{CursorMove, TextArea};
14
15use crate::utils::remove_newlines;
16
17const DEFAULT_STYLE: Style = Style::new();
18
19/// A custom text area widget
20#[derive(Clone)]
21pub struct CustomTextArea<'a> {
22    inline: bool,
23    inline_title: Option<Text<'a>>,
24    textarea: TextArea<'a>,
25    cursor_style: Style,
26    focus: bool,
27    multiline: bool,
28}
29
30impl<'a> CustomTextArea<'a> {
31    /// Creates a new custom text area
32    pub fn new(style: impl Into<Style>, inline: bool, multiline: bool, text: impl Into<String>) -> Self {
33        let style = style.into();
34        let cursor_style = style.add_modifier(Modifier::REVERSED);
35        let cursor_line_style = style;
36
37        let text = text.into();
38        let mut textarea = if multiline {
39            TextArea::from(
40                text.split('\n')
41                    .map(|s| s.strip_suffix('\r').unwrap_or(s).to_string())
42                    .collect::<Vec<_>>(),
43            )
44        } else {
45            TextArea::from([remove_newlines(text)])
46        };
47        textarea.set_style(style);
48        textarea.set_cursor_style(DEFAULT_STYLE);
49        textarea.set_cursor_line_style(cursor_line_style);
50        textarea.move_cursor(CursorMove::Jump(u16::MAX, u16::MAX));
51        if !inline {
52            textarea.set_block(Block::default().borders(Borders::ALL).style(style));
53        }
54
55        Self {
56            inline,
57            inline_title: None,
58            textarea,
59            cursor_style,
60            focus: false,
61            multiline,
62        }
63    }
64
65    /// Updates the title of the text area
66    pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
67        self.set_title(title);
68        self
69    }
70
71    /// Updates the text area to be focused
72    pub fn focused(mut self) -> Self {
73        self.set_focus(true);
74        self
75    }
76
77    /// Updates the textarea mask char
78    pub fn secret(mut self, secret: bool) -> Self {
79        self.set_secret(secret);
80        self
81    }
82
83    /// Returns whether the text area supports multiple lines or not
84    pub fn is_multiline(&self) -> bool {
85        self.multiline
86    }
87
88    /// Returns whether the text area is currently focused or not
89    pub fn is_focused(&self) -> bool {
90        self.focus
91    }
92
93    /// Sets or clear the the text area mask char
94    pub fn set_secret(&mut self, secret: bool) {
95        if secret {
96            self.textarea.set_mask_char('●');
97        } else {
98            self.textarea.clear_mask_char();
99        }
100    }
101
102    /// Sets the focus state of the text area
103    pub fn set_focus(&mut self, focus: bool) {
104        if focus != self.focus {
105            self.focus = focus;
106            if self.focus {
107                self.textarea.set_cursor_style(self.cursor_style);
108            } else {
109                self.textarea.set_cursor_style(DEFAULT_STYLE);
110            }
111        }
112    }
113
114    /// Updates the title of the text area
115    pub fn set_title(&mut self, new_title: impl Into<Cow<'a, str>>) {
116        let style = self.textarea.style();
117        if self.inline {
118            self.inline_title = Some(Text::from(new_title.into()).style(style));
119        } else {
120            self.textarea.set_block(
121                Block::default()
122                    .borders(Borders::ALL)
123                    .style(style)
124                    .title(new_title.into()),
125            );
126        }
127    }
128
129    /// Updates the style of this text area
130    pub fn set_style(&mut self, style: impl Into<Style>) {
131        let style = style.into();
132        self.cursor_style = style.add_modifier(Modifier::REVERSED);
133
134        self.textarea.set_style(style);
135        self.textarea
136            .set_cursor_style(if self.focus { self.cursor_style } else { DEFAULT_STYLE });
137        self.textarea.set_cursor_line_style(style);
138
139        if let Some(ref mut inline_title) = self.inline_title {
140            *inline_title = inline_title.clone().style(style);
141        } else if let Some(block) = self.textarea.block().cloned() {
142            self.textarea.set_block(block.style(style));
143        }
144    }
145
146    /// Retrieves the current text in the text area as a single string
147    pub fn lines_as_string(&self) -> String {
148        self.textarea.lines().join("\n")
149    }
150
151    /// Moves the cursor to the left, optionally by word
152    pub fn move_cursor_left(&mut self, word: bool) {
153        if self.focus {
154            self.textarea
155                .move_cursor(if word { CursorMove::WordBack } else { CursorMove::Back });
156        }
157    }
158
159    /// Moves the cursor to the right, optionally by word
160    pub fn move_cursor_right(&mut self, word: bool) {
161        if self.focus {
162            self.textarea.move_cursor(if word {
163                CursorMove::WordForward
164            } else {
165                CursorMove::Forward
166            });
167        }
168    }
169
170    /// Moves the cursor to the head of the line, or the absolute head
171    pub fn move_home(&mut self, absolute: bool) {
172        if self.focus {
173            self.textarea.move_cursor(if absolute {
174                CursorMove::Jump(0, 0)
175            } else {
176                CursorMove::Head
177            });
178        }
179    }
180
181    /// Moves the cursor to the end of the line, or the absolute end
182    pub fn move_end(&mut self, absolute: bool) {
183        if self.focus {
184            self.textarea.move_cursor(if absolute {
185                CursorMove::Jump(u16::MAX, u16::MAX)
186            } else {
187                CursorMove::End
188            });
189        }
190    }
191
192    /// Inserts a char at the current cursor position
193    pub fn insert_char(&mut self, c: char) {
194        if self.focus && self.multiline || c != '\n' {
195            self.textarea.insert_char(c);
196        }
197    }
198
199    /// Inserts a text at the current cursor position
200    pub fn insert_str<S>(&mut self, text: S)
201    where
202        S: AsRef<str>,
203    {
204        if self.focus {
205            if self.multiline {
206                self.textarea.insert_str(text);
207            } else {
208                self.textarea.insert_str(remove_newlines(text.as_ref()));
209            };
210        }
211    }
212
213    /// Inserts a newline at the current cursor position, if multiline is enabled
214    pub fn insert_newline(&mut self) {
215        if self.focus && self.multiline {
216            self.textarea.insert_newline();
217        }
218    }
219
220    /// Delete characters at the cursor position based on the backspace and word flags
221    pub fn delete(&mut self, backspace: bool, word: bool) {
222        if self.focus {
223            match (backspace, word) {
224                (true, true) => self.textarea.delete_word(),
225                (true, false) => self.textarea.delete_char(),
226                (false, true) => self.textarea.delete_next_word(),
227                (false, false) => self.textarea.delete_next_char(),
228            };
229        }
230    }
231}
232
233impl<'a> Widget for &CustomTextArea<'a> {
234    fn render(self, area: Rect, buf: &mut Buffer) {
235        if let Some(ref inline_title) = self.inline_title {
236            let layout = Layout::horizontal([Constraint::Length(inline_title.width() as u16 + 1), Constraint::Min(1)]);
237            let [inline_title_area, textarea_area] = layout.areas(area);
238            inline_title.render(inline_title_area, buf);
239            self.textarea.render(textarea_area, buf);
240        } else {
241            self.textarea.render(area, buf);
242        }
243    }
244}
245
246impl<'a> Deref for CustomTextArea<'a> {
247    type Target = TextArea<'a>;
248
249    fn deref(&self) -> &Self::Target {
250        &self.textarea
251    }
252}
253
254impl<'a> DerefMut for CustomTextArea<'a> {
255    fn deref_mut(&mut self) -> &mut Self::Target {
256        &mut self.textarea
257    }
258}