intelli_shell/widgets/
textarea.rs1use 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#[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 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 pub fn title(mut self, title: impl Into<Cow<'a, str>>) -> Self {
67 self.set_title(title);
68 self
69 }
70
71 pub fn focused(mut self) -> Self {
73 self.set_focus(true);
74 self
75 }
76
77 pub fn secret(mut self, secret: bool) -> Self {
79 self.set_secret(secret);
80 self
81 }
82
83 pub fn is_multiline(&self) -> bool {
85 self.multiline
86 }
87
88 pub fn is_focused(&self) -> bool {
90 self.focus
91 }
92
93 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 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 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 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 pub fn lines_as_string(&self) -> String {
148 self.textarea.lines().join("\n")
149 }
150
151 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 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 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 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 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 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 pub fn insert_newline(&mut self) {
215 if self.focus && self.multiline {
216 self.textarea.insert_newline();
217 }
218 }
219
220 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}