tui_realm_stdlib/components/
textarea.rs1use tuirealm::command::{Cmd, CmdResult, Direction, Position};
2use tuirealm::component::Component;
3use tuirealm::props::{
4 AttrValue, Attribute, Borders, Color, LineStatic, Props, QueryResult, Style, TextModifiers,
5 TextStatic, Title,
6};
7use tuirealm::ratatui::Frame;
8use tuirealm::ratatui::layout::Rect;
9use tuirealm::ratatui::widgets::{List, ListItem, ListState};
10use tuirealm::state::{State, StateValue};
11
12use crate::prop_ext::CommonProps;
13use crate::utils::borrow_clone_line;
14
15#[derive(Default)]
19pub struct TextareaStates {
20 pub list_index: usize,
22 pub list_len: usize,
24}
25
26impl TextareaStates {
27 pub fn set_list_len(&mut self, len: usize) {
29 self.list_len = len;
30 self.fix_list_index();
31 }
32
33 pub fn incr_list_index(&mut self) {
35 if self.list_index + 1 < self.list_len {
37 self.list_index += 1;
38 }
39 }
40
41 pub fn decr_list_index(&mut self) {
43 if self.list_index > 0 {
45 self.list_index -= 1;
46 }
47 }
48
49 pub fn fix_list_index(&mut self) {
51 if self.list_index >= self.list_len && self.list_len > 0 {
52 self.list_index = self.list_len - 1;
53 } else if self.list_len == 0 {
54 self.list_index = 0;
55 }
56 }
57
58 pub fn list_index_at_first(&mut self) {
60 self.list_index = 0;
61 }
62
63 pub fn list_index_at_last(&mut self) {
65 if self.list_len > 0 {
66 self.list_index = self.list_len - 1;
67 } else {
68 self.list_index = 0;
69 }
70 }
71
72 fn calc_max_step_ahead(&self, max: usize) -> usize {
74 let remaining: usize = match self.list_len {
75 0 => 0,
76 len => len - 1 - self.list_index,
77 };
78 if remaining > max { max } else { remaining }
79 }
80
81 fn calc_max_step_behind(&self, max: usize) -> usize {
83 if self.list_index > max {
84 max
85 } else {
86 self.list_index
87 }
88 }
89}
90
91#[derive(Default)]
98#[must_use]
99pub struct Textarea {
100 common: CommonProps,
101 props: Props,
102 pub states: TextareaStates,
103}
104
105impl Textarea {
106 pub fn foreground(mut self, fg: Color) -> Self {
108 self.attr(Attribute::Foreground, AttrValue::Color(fg));
109 self
110 }
111
112 pub fn background(mut self, bg: Color) -> Self {
114 self.attr(Attribute::Background, AttrValue::Color(bg));
115 self
116 }
117
118 pub fn modifiers(mut self, m: TextModifiers) -> Self {
120 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
121 self
122 }
123
124 pub fn style(mut self, style: Style) -> Self {
128 self.attr(Attribute::Style, AttrValue::Style(style));
129 self
130 }
131
132 pub fn inactive(mut self, s: Style) -> Self {
134 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
135 self
136 }
137
138 pub fn borders(mut self, b: Borders) -> Self {
140 self.attr(Attribute::Borders, AttrValue::Borders(b));
141 self
142 }
143
144 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
146 self.attr(Attribute::Title, AttrValue::Title(title.into()));
147 self
148 }
149
150 pub fn step(mut self, step: usize) -> Self {
152 self.attr(Attribute::ScrollStep, AttrValue::Length(step));
153 self
154 }
155
156 pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
158 self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
159 self
160 }
161
162 pub fn text_rows<T>(self, text: impl IntoIterator<Item = T>) -> Self
176 where
177 T: Into<LineStatic>,
178 {
179 let text = TextStatic::from_iter(text);
180 self.text(text)
181 }
182
183 pub fn text(mut self, text: impl Into<TextStatic>) -> Self {
196 let text = text.into();
197 self.states.set_list_len(text.lines.len());
198 self.attr(Attribute::Text, AttrValue::Text(text));
199 self
200 }
201}
202
203impl Component for Textarea {
204 fn view(&mut self, render: &mut Frame, area: Rect) {
205 if !self.common.display {
206 return;
207 }
208
209 let hg_str = self
211 .props
212 .get(Attribute::HighlightedStr)
213 .and_then(|x| x.as_textline());
214 let wrap_width = (area.width as usize) - hg_str.as_ref().map_or(0, |x| x.width()) - 2;
216 let lines: Vec<ListItem> = self
217 .props
218 .get(Attribute::Text)
219 .and_then(AttrValue::as_text)
220 .map(|text| {
221 text.iter()
222 .map(|x| crate::utils::wrap_lines(&[x], wrap_width))
223 .map(ListItem::new)
224 .collect()
225 })
226 .unwrap_or_default();
227
228 let mut state: ListState = ListState::default();
229 state.select(Some(self.states.list_index));
230 let mut list = List::new(lines)
233 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
234 .style(self.common.style);
235
236 if let Some(block) = self.common.get_block() {
237 list = list.block(block);
238 }
239 if let Some(hg_str) = hg_str {
240 list = list.highlight_symbol(borrow_clone_line(hg_str));
241 }
242
243 render.render_stateful_widget(list, area, &mut state);
244 }
245
246 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
247 if let Some(value) = self.common.get_for_query(attr) {
248 return Some(value);
249 }
250
251 self.props.get_for_query(attr)
252 }
253
254 fn attr(&mut self, attr: Attribute, value: AttrValue) {
255 if let Some(value) = self.common.set(attr, value) {
256 self.props.set(attr, value);
257 self.states.set_list_len(
259 self.props
260 .get(Attribute::Text)
261 .and_then(AttrValue::as_text)
262 .map_or(0, |text| text.lines.len()),
263 );
264 self.states.fix_list_index();
265 }
266 }
267
268 fn state(&self) -> State {
269 State::Single(StateValue::Usize(self.states.list_index))
270 }
271
272 fn perform(&mut self, cmd: Cmd) -> CmdResult {
273 let prev = self.states.list_index;
274 match cmd {
275 Cmd::Move(Direction::Down) => {
276 self.states.incr_list_index();
277 }
278 Cmd::Move(Direction::Up) => {
279 self.states.decr_list_index();
280 }
281 Cmd::Scroll(Direction::Down) => {
282 let step = self
283 .props
284 .get(Attribute::ScrollStep)
285 .and_then(AttrValue::as_length)
286 .unwrap_or(8);
287 let step = self.states.calc_max_step_ahead(step);
288 (0..step).for_each(|_| self.states.incr_list_index());
289 }
290 Cmd::Scroll(Direction::Up) => {
291 let step = self
292 .props
293 .get(Attribute::ScrollStep)
294 .and_then(AttrValue::as_length)
295 .unwrap_or(8);
296 let step = self.states.calc_max_step_behind(step);
297 (0..step).for_each(|_| self.states.decr_list_index());
298 }
299 Cmd::GoTo(Position::Begin) => {
300 self.states.list_index_at_first();
301 }
302 Cmd::GoTo(Position::End) => {
303 self.states.list_index_at_last();
304 }
305 _ => return CmdResult::Invalid(cmd),
306 }
307 if prev != self.states.list_index {
308 CmdResult::Changed(self.state())
309 } else {
310 CmdResult::NoChange
311 }
312 }
313}
314
315#[cfg(test)]
316mod tests {
317
318 use pretty_assertions::assert_eq;
319 use tuirealm::props::HorizontalAlignment;
320 use tuirealm::ratatui::text::{Line, Span, Text};
321 use tuirealm::state::StateValue;
322
323 use super::*;
324
325 #[test]
326 fn test_components_textarea() {
327 let mut component = Textarea::default()
329 .foreground(Color::Red)
330 .background(Color::Blue)
331 .modifiers(TextModifiers::BOLD)
332 .borders(Borders::default())
333 .highlight_str("🚀")
334 .step(4)
335 .title(Title::from("textarea").alignment(HorizontalAlignment::Center))
336 .text_rows([Line::from("welcome to "), Line::from("tui-realm")]);
337 component.states.list_index += 1;
339 assert_eq!(component.states.list_index, 1);
340 component.attr(
342 Attribute::Text,
343 AttrValue::Text(TextStatic::from_iter([
344 Line::from("welcome"),
345 Line::from("to"),
346 Line::from("tui-realm"),
347 ])),
348 );
349 assert_eq!(component.states.list_index, 1); assert_eq!(component.states.list_len, 3);
352 assert_eq!(component.state(), State::Single(StateValue::Usize(1)));
354 assert_eq!(component.states.list_index, 1);
356 assert_eq!(
358 component.perform(Cmd::Move(Direction::Down)),
359 CmdResult::Changed(State::Single(StateValue::Usize(2)))
360 );
361 assert_eq!(component.states.list_index, 2);
363 assert_eq!(
365 component.perform(Cmd::Move(Direction::Up)),
366 CmdResult::Changed(State::Single(StateValue::Usize(1)))
367 );
368 assert_eq!(component.states.list_index, 1);
370 assert_eq!(
372 component.perform(Cmd::Scroll(Direction::Down)),
373 CmdResult::Changed(State::Single(StateValue::Usize(2)))
374 );
375 assert_eq!(component.states.list_index, 2);
377 assert_eq!(
379 component.perform(Cmd::Scroll(Direction::Up)),
380 CmdResult::Changed(State::Single(StateValue::Usize(0)))
381 );
382 assert_eq!(component.states.list_index, 0);
383 assert_eq!(
385 component.perform(Cmd::GoTo(Position::End)),
386 CmdResult::Changed(State::Single(StateValue::Usize(2)))
387 );
388 assert_eq!(component.states.list_index, 2);
389 assert_eq!(
391 component.perform(Cmd::GoTo(Position::Begin)),
392 CmdResult::Changed(State::Single(StateValue::Usize(0)))
393 );
394 assert_eq!(component.states.list_index, 0);
395 assert_eq!(
397 component.perform(Cmd::GoTo(Position::Begin)),
398 CmdResult::NoChange
399 );
400 assert_eq!(
402 component.perform(Cmd::Delete),
403 CmdResult::Invalid(Cmd::Delete)
404 );
405 }
406
407 #[test]
408 fn various_textrows_types() {
409 let _ = Textarea::default().text_rows(vec![Span::raw("hello")]);
411 let _ = Textarea::default().text_rows([Span::raw("hello")]);
413 let _ = Textarea::default().text_rows(vec![Span::raw("hello")].into_boxed_slice());
415 let _ = Textarea::default().text_rows(["Hello"].map(Span::raw));
417
418 let _ = Textarea::default().text_rows(vec![Line::raw("hello")]);
420 let _ = Textarea::default().text_rows([Line::raw("hello")]);
422 let _ = Textarea::default().text_rows(vec![Line::raw("hello")].into_boxed_slice());
424 let _ = Textarea::default().text_rows(["Hello"].map(Line::raw));
426 }
427
428 #[test]
429 fn various_text_types() {
430 let _ = Textarea::default().text(Text::raw("hello"));
432 let _ = Textarea::default().text(Line::raw("hello"));
434 let _ = Textarea::default().text(Span::raw("hello"));
436 let _ = Textarea::default().text("hello");
438 let _ = Textarea::default().text("hello".to_string());
440 }
441}