tui_realm_stdlib/components/
textarea.rs1extern crate unicode_width;
8
9use tuirealm::command::{Cmd, CmdResult, Direction, Position};
10use tuirealm::props::{
11 Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
12 TextModifiers, TextSpan,
13};
14use tuirealm::ratatui::{
15 layout::Rect,
16 widgets::{List, ListItem, ListState},
17};
18use tuirealm::{Frame, MockComponent, State};
19use unicode_width::UnicodeWidthStr;
20
21#[derive(Default)]
24pub struct TextareaStates {
25 pub list_index: usize, pub list_len: usize, }
28
29impl TextareaStates {
30 pub fn set_list_len(&mut self, len: usize) {
34 self.list_len = len;
35 self.fix_list_index();
36 }
37
38 pub fn incr_list_index(&mut self) {
42 if self.list_index + 1 < self.list_len {
44 self.list_index += 1;
45 }
46 }
47
48 pub fn decr_list_index(&mut self) {
52 if self.list_index > 0 {
54 self.list_index -= 1;
55 }
56 }
57
58 pub fn fix_list_index(&mut self) {
62 if self.list_index >= self.list_len && self.list_len > 0 {
63 self.list_index = self.list_len - 1;
64 } else if self.list_len == 0 {
65 self.list_index = 0;
66 }
67 }
68
69 pub fn list_index_at_first(&mut self) {
73 self.list_index = 0;
74 }
75
76 pub fn list_index_at_last(&mut self) {
80 if self.list_len > 0 {
81 self.list_index = self.list_len - 1;
82 } else {
83 self.list_index = 0;
84 }
85 }
86
87 fn calc_max_step_ahead(&self, max: usize) -> usize {
91 let remaining: usize = match self.list_len {
92 0 => 0,
93 len => len - 1 - self.list_index,
94 };
95 if remaining > max { max } else { remaining }
96 }
97
98 fn calc_max_step_behind(&self, max: usize) -> usize {
102 if self.list_index > max {
103 max
104 } else {
105 self.list_index
106 }
107 }
108}
109
110#[derive(Default)]
116#[must_use]
117pub struct Textarea {
118 props: Props,
119 pub states: TextareaStates,
120}
121
122impl Textarea {
123 pub fn foreground(mut self, fg: Color) -> Self {
124 self.attr(Attribute::Foreground, AttrValue::Color(fg));
125 self
126 }
127
128 pub fn background(mut self, bg: Color) -> Self {
129 self.attr(Attribute::Background, AttrValue::Color(bg));
130 self
131 }
132
133 pub fn inactive(mut self, s: Style) -> Self {
134 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
135 self
136 }
137
138 pub fn modifiers(mut self, m: TextModifiers) -> Self {
139 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
140 self
141 }
142
143 pub fn borders(mut self, b: Borders) -> Self {
144 self.attr(Attribute::Borders, AttrValue::Borders(b));
145 self
146 }
147
148 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
149 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
150 self
151 }
152
153 pub fn step(mut self, step: usize) -> Self {
154 self.attr(Attribute::ScrollStep, AttrValue::Length(step));
155 self
156 }
157
158 pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
159 self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
160 self
161 }
162
163 pub fn text_rows(mut self, s: impl IntoIterator<Item = TextSpan>) -> Self {
164 let rows: Vec<PropValue> = s.into_iter().map(PropValue::TextSpan).collect();
165 self.states.set_list_len(rows.len());
166 self.attr(Attribute::Text, AttrValue::Payload(PropPayload::Vec(rows)));
167 self
168 }
169}
170
171impl MockComponent for Textarea {
172 fn view(&mut self, render: &mut Frame, area: Rect) {
173 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
175 let hg_str = self
178 .props
179 .get_ref(Attribute::HighlightedStr)
180 .and_then(|x| x.as_string());
181 let wrap_width = (area.width as usize) - hg_str.as_ref().map_or(0, |x| x.width()) - 2;
183 let lines: Vec<ListItem> = match self
184 .props
185 .get_ref(Attribute::Text)
186 .and_then(|x| x.as_payload())
187 {
188 Some(PropPayload::Vec(spans)) => spans
189 .iter()
190 .filter_map(|x| x.as_text_span())
192 .map(|x| crate::utils::wrap_spans(&[x], wrap_width, &self.props))
193 .map(ListItem::new)
194 .collect(),
195 _ => Vec::new(),
196 };
197 let foreground = self
198 .props
199 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
200 .unwrap_color();
201 let background = self
202 .props
203 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
204 .unwrap_color();
205 let modifiers = self
206 .props
207 .get_or(
208 Attribute::TextProps,
209 AttrValue::TextModifiers(TextModifiers::empty()),
210 )
211 .unwrap_text_modifiers();
212 let title = crate::utils::get_title_or_center(&self.props);
213 let borders = self
214 .props
215 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
216 .unwrap_borders();
217 let focus = self
218 .props
219 .get_or(Attribute::Focus, AttrValue::Flag(false))
220 .unwrap_flag();
221 let inactive_style = self
222 .props
223 .get(Attribute::FocusStyle)
224 .map(|x| x.unwrap_style());
225 let mut state: ListState = ListState::default();
226 state.select(Some(self.states.list_index));
227 let mut list = List::new(lines)
230 .block(crate::utils::get_block(
231 borders,
232 Some(&title),
233 focus,
234 inactive_style,
235 ))
236 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
237 .style(
238 Style::default()
239 .fg(foreground)
240 .bg(background)
241 .add_modifier(modifiers),
242 );
243
244 if let Some(hg_str) = hg_str {
245 list = list.highlight_symbol(hg_str);
246 }
247 render.render_stateful_widget(list, area, &mut state);
248 }
249 }
250
251 fn query(&self, attr: Attribute) -> Option<AttrValue> {
252 self.props.get(attr)
253 }
254
255 fn attr(&mut self, attr: Attribute, value: AttrValue) {
256 self.props.set(attr, value);
257 self.states.set_list_len(
259 match self.props.get(Attribute::Text).map(|x| x.unwrap_payload()) {
260 Some(PropPayload::Vec(spans)) => spans.len(),
261 _ => 0,
262 },
263 );
264 self.states.fix_list_index();
265 }
266
267 fn state(&self) -> State {
268 State::None
269 }
270
271 fn perform(&mut self, cmd: Cmd) -> CmdResult {
272 match cmd {
273 Cmd::Move(Direction::Down) => {
274 self.states.incr_list_index();
275 }
276 Cmd::Move(Direction::Up) => {
277 self.states.decr_list_index();
278 }
279 Cmd::Scroll(Direction::Down) => {
280 let step = self
281 .props
282 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
283 .unwrap_length();
284 let step = self.states.calc_max_step_ahead(step);
285 (0..step).for_each(|_| self.states.incr_list_index());
286 }
287 Cmd::Scroll(Direction::Up) => {
288 let step = self
289 .props
290 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
291 .unwrap_length();
292 let step = self.states.calc_max_step_behind(step);
293 (0..step).for_each(|_| self.states.decr_list_index());
294 }
295 Cmd::GoTo(Position::Begin) => {
296 self.states.list_index_at_first();
297 }
298 Cmd::GoTo(Position::End) => {
299 self.states.list_index_at_last();
300 }
301 _ => {}
302 }
303 CmdResult::None
304 }
305}
306
307#[cfg(test)]
308mod tests {
309
310 use super::*;
311
312 use pretty_assertions::assert_eq;
313
314 #[test]
315 fn test_components_textarea() {
316 let mut component = Textarea::default()
318 .foreground(Color::Red)
319 .background(Color::Blue)
320 .modifiers(TextModifiers::BOLD)
321 .borders(Borders::default())
322 .highlighted_str("🚀")
323 .step(4)
324 .title("textarea", Alignment::Center)
325 .text_rows([TextSpan::from("welcome to "), TextSpan::from("tui-realm")]);
326 component.states.list_index += 1;
328 assert_eq!(component.states.list_index, 1);
329 component.attr(
331 Attribute::Text,
332 AttrValue::Payload(PropPayload::Vec(vec![
333 PropValue::TextSpan(TextSpan::from("welcome")),
334 PropValue::TextSpan(TextSpan::from("to")),
335 PropValue::TextSpan(TextSpan::from("tui-realm")),
336 ])),
337 );
338 assert_eq!(component.states.list_index, 1); assert_eq!(component.states.list_len, 3);
341 assert_eq!(component.state(), State::None);
343 assert_eq!(component.states.list_index, 1);
345 assert_eq!(
347 component.perform(Cmd::Move(Direction::Down)),
348 CmdResult::None
349 );
350 assert_eq!(component.states.list_index, 2);
352 assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
354 assert_eq!(component.states.list_index, 1);
356 assert_eq!(
358 component.perform(Cmd::Scroll(Direction::Down)),
359 CmdResult::None
360 );
361 assert_eq!(component.states.list_index, 2);
363 assert_eq!(
365 component.perform(Cmd::Scroll(Direction::Up)),
366 CmdResult::None
367 );
368 assert_eq!(component.perform(Cmd::GoTo(Position::End)), CmdResult::None);
370 assert_eq!(component.states.list_index, 2);
371 assert_eq!(
373 component.perform(Cmd::GoTo(Position::Begin)),
374 CmdResult::None
375 );
376 assert_eq!(component.states.list_index, 0);
378 assert_eq!(component.perform(Cmd::Delete), CmdResult::None);
380 }
381
382 #[test]
383 fn various_textrows_types() {
384 let _ = Textarea::default().text_rows(vec![TextSpan::new("hello")]);
386 let _ = Textarea::default().text_rows([TextSpan::new("hello")]);
388 let _ = Textarea::default().text_rows(vec![TextSpan::new("hello")].into_boxed_slice());
390 let _ = Textarea::default().text_rows(["Hello"].map(TextSpan::new));
392 }
393}