tui_realm_stdlib/components/
list.rs1use tuirealm::command::{Cmd, CmdResult, Direction, Position};
6use tuirealm::props::{
7 Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style, Table,
8 TextModifiers,
9};
10use tuirealm::ratatui::text::Line as Spans;
11use tuirealm::ratatui::{
12 layout::Rect,
13 text::Span,
14 widgets::{List as TuiList, ListItem, ListState},
15};
16use tuirealm::{Frame, MockComponent, State, StateValue};
17
18#[derive(Default)]
21pub struct ListStates {
22 pub list_index: usize, pub list_len: usize, }
25
26impl ListStates {
27 pub fn set_list_len(&mut self, len: usize) {
31 self.list_len = len;
32 }
33
34 pub fn incr_list_index(&mut self, rewind: bool) {
38 if self.list_index + 1 < self.list_len {
40 self.list_index += 1;
41 } else if rewind {
42 self.list_index = 0;
43 }
44 }
45
46 pub fn decr_list_index(&mut self, rewind: bool) {
50 if self.list_index > 0 {
52 self.list_index -= 1;
53 } else if rewind && self.list_len > 0 {
54 self.list_index = self.list_len - 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 pub 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 {
96 max
97 } else {
98 remaining
99 }
100 }
101
102 pub fn calc_max_step_behind(&self, max: usize) -> usize {
106 if self.list_index > max {
107 max
108 } else {
109 self.list_index
110 }
111 }
112}
113
114#[derive(Default)]
120pub struct List {
121 props: Props,
122 pub states: ListStates,
123 hg_str: Option<String>, }
125
126impl List {
127 pub fn foreground(mut self, fg: Color) -> Self {
128 self.attr(Attribute::Foreground, AttrValue::Color(fg));
129 self
130 }
131
132 pub fn background(mut self, bg: Color) -> Self {
133 self.attr(Attribute::Background, AttrValue::Color(bg));
134 self
135 }
136
137 pub fn modifiers(mut self, m: TextModifiers) -> Self {
138 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
139 self
140 }
141
142 pub fn borders(mut self, b: Borders) -> Self {
143 self.attr(Attribute::Borders, AttrValue::Borders(b));
144 self
145 }
146
147 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
148 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
149 self
150 }
151
152 pub fn inactive(mut self, s: Style) -> Self {
153 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
154 self
155 }
156
157 pub fn rewind(mut self, r: bool) -> Self {
158 self.attr(Attribute::Rewind, AttrValue::Flag(r));
159 self
160 }
161
162 pub fn step(mut self, step: usize) -> Self {
163 self.attr(Attribute::ScrollStep, AttrValue::Length(step));
164 self
165 }
166
167 pub fn scroll(mut self, scrollable: bool) -> Self {
168 self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
169 self
170 }
171
172 pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
173 self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
174 self
175 }
176
177 pub fn highlighted_color(mut self, c: Color) -> Self {
178 self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
179 self
180 }
181
182 pub fn rows(mut self, rows: Table) -> Self {
183 self.attr(Attribute::Content, AttrValue::Table(rows));
184 self
185 }
186
187 pub fn selected_line(mut self, line: usize) -> Self {
190 self.attr(
191 Attribute::Value,
192 AttrValue::Payload(PropPayload::One(PropValue::Usize(line))),
193 );
194 self
195 }
196
197 fn scrollable(&self) -> bool {
198 self.props
199 .get_or(Attribute::Scroll, AttrValue::Flag(false))
200 .unwrap_flag()
201 }
202
203 fn rewindable(&self) -> bool {
204 self.props
205 .get_or(Attribute::Rewind, AttrValue::Flag(false))
206 .unwrap_flag()
207 }
208}
209
210impl MockComponent for List {
211 fn view(&mut self, render: &mut Frame, area: Rect) {
212 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
213 let foreground = self
214 .props
215 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
216 .unwrap_color();
217 let background = self
218 .props
219 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
220 .unwrap_color();
221 let modifiers = self
222 .props
223 .get_or(
224 Attribute::TextProps,
225 AttrValue::TextModifiers(TextModifiers::empty()),
226 )
227 .unwrap_text_modifiers();
228 let title = self
229 .props
230 .get_or(
231 Attribute::Title,
232 AttrValue::Title((String::default(), Alignment::Center)),
233 )
234 .unwrap_title();
235 let borders = self
236 .props
237 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
238 .unwrap_borders();
239 let focus = self
240 .props
241 .get_or(Attribute::Focus, AttrValue::Flag(false))
242 .unwrap_flag();
243 let inactive_style = self
244 .props
245 .get(Attribute::FocusStyle)
246 .map(|x| x.unwrap_style());
247 let active: bool = match self.scrollable() {
248 true => focus,
249 false => true,
250 };
251 let div = crate::utils::get_block(borders, Some(title), active, inactive_style);
252 let list_items: Vec<ListItem> =
254 match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
255 Some(table) => table
256 .iter()
257 .map(|row| {
258 let columns: Vec<Span> = row
259 .iter()
260 .map(|col| {
261 let (fg, bg, modifiers) =
262 crate::utils::use_or_default_styles(&self.props, col);
263 Span::styled(
264 col.content.clone(),
265 Style::default().add_modifier(modifiers).fg(fg).bg(bg),
266 )
267 })
268 .collect();
269 ListItem::new(Spans::from(columns))
270 })
271 .collect(), _ => Vec::new(),
273 };
274 let highlighted_color = self
275 .props
276 .get(Attribute::HighlightedColor)
277 .map(|x| x.unwrap_color());
278 let modifiers = match focus {
279 true => modifiers | TextModifiers::REVERSED,
280 false => modifiers,
281 };
282 let mut list = TuiList::new(list_items)
285 .block(div)
286 .style(Style::default().fg(foreground).bg(background))
287 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom);
288 if let Some(highlighted_color) = highlighted_color {
289 list = list.highlight_style(
290 Style::default()
291 .fg(highlighted_color)
292 .add_modifier(modifiers),
293 );
294 }
295 self.hg_str = self
297 .props
298 .get(Attribute::HighlightedStr)
299 .map(|x| x.unwrap_string());
300 if let Some(hg_str) = &self.hg_str {
301 list = list.highlight_symbol(hg_str);
302 }
303 if self.scrollable() {
304 let mut state: ListState = ListState::default();
305 state.select(Some(self.states.list_index));
306 render.render_stateful_widget(list, area, &mut state);
307 } else {
308 render.render_widget(list, area);
309 }
310 }
311 }
312
313 fn query(&self, attr: Attribute) -> Option<AttrValue> {
314 self.props.get(attr)
315 }
316
317 fn attr(&mut self, attr: Attribute, value: AttrValue) {
318 self.props.set(attr, value);
319 if matches!(attr, Attribute::Content) {
320 self.states.set_list_len(
322 match self.props.get(Attribute::Content).map(|x| x.unwrap_table()) {
323 Some(spans) => spans.len(),
324 _ => 0,
325 },
326 );
327 self.states.fix_list_index();
328 } else if matches!(attr, Attribute::Value) && self.scrollable() {
329 self.states.list_index = self
330 .props
331 .get(Attribute::Value)
332 .map(|x| x.unwrap_payload().unwrap_one().unwrap_usize())
333 .unwrap_or(0);
334 self.states.fix_list_index();
335 }
336 }
337
338 fn state(&self) -> State {
339 match self.scrollable() {
340 true => State::One(StateValue::Usize(self.states.list_index)),
341 false => State::None,
342 }
343 }
344
345 fn perform(&mut self, cmd: Cmd) -> CmdResult {
346 match cmd {
347 Cmd::Move(Direction::Down) => {
348 let prev = self.states.list_index;
349 self.states.incr_list_index(self.rewindable());
350 if prev != self.states.list_index {
351 CmdResult::Changed(self.state())
352 } else {
353 CmdResult::None
354 }
355 }
356 Cmd::Move(Direction::Up) => {
357 let prev = self.states.list_index;
358 self.states.decr_list_index(self.rewindable());
359 if prev != self.states.list_index {
360 CmdResult::Changed(self.state())
361 } else {
362 CmdResult::None
363 }
364 }
365 Cmd::Scroll(Direction::Down) => {
366 let prev = self.states.list_index;
367 let step = self
368 .props
369 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
370 .unwrap_length();
371 let step: usize = self.states.calc_max_step_ahead(step);
372 (0..step).for_each(|_| self.states.incr_list_index(false));
373 if prev != self.states.list_index {
374 CmdResult::Changed(self.state())
375 } else {
376 CmdResult::None
377 }
378 }
379 Cmd::Scroll(Direction::Up) => {
380 let prev = self.states.list_index;
381 let step = self
382 .props
383 .get_or(Attribute::ScrollStep, AttrValue::Length(8))
384 .unwrap_length();
385 let step: usize = self.states.calc_max_step_behind(step);
386 (0..step).for_each(|_| self.states.decr_list_index(false));
387 if prev != self.states.list_index {
388 CmdResult::Changed(self.state())
389 } else {
390 CmdResult::None
391 }
392 }
393 Cmd::GoTo(Position::Begin) => {
394 let prev = self.states.list_index;
395 self.states.list_index_at_first();
396 if prev != self.states.list_index {
397 CmdResult::Changed(self.state())
398 } else {
399 CmdResult::None
400 }
401 }
402 Cmd::GoTo(Position::End) => {
403 let prev = self.states.list_index;
404 self.states.list_index_at_last();
405 if prev != self.states.list_index {
406 CmdResult::Changed(self.state())
407 } else {
408 CmdResult::None
409 }
410 }
411 _ => CmdResult::None,
412 }
413 }
414}
415
416#[cfg(test)]
417mod tests {
418
419 use super::*;
420 use pretty_assertions::assert_eq;
421 use tuirealm::props::{TableBuilder, TextSpan};
422
423 #[test]
424 fn list_states() {
425 let mut states = ListStates::default();
426 assert_eq!(states.list_index, 0);
427 assert_eq!(states.list_len, 0);
428 states.set_list_len(5);
429 assert_eq!(states.list_index, 0);
430 assert_eq!(states.list_len, 5);
431 states.incr_list_index(true);
433 assert_eq!(states.list_index, 1);
434 states.list_index = 4;
435 states.incr_list_index(false);
436 assert_eq!(states.list_index, 4);
437 states.incr_list_index(true);
438 assert_eq!(states.list_index, 0);
439 states.decr_list_index(false);
441 assert_eq!(states.list_index, 0);
442 states.decr_list_index(true);
443 assert_eq!(states.list_index, 4);
444 states.decr_list_index(true);
445 assert_eq!(states.list_index, 3);
446 states.list_index_at_first();
448 assert_eq!(states.list_index, 0);
449 states.list_index_at_last();
450 assert_eq!(states.list_index, 4);
451 states.set_list_len(3);
453 states.fix_list_index();
454 assert_eq!(states.list_index, 2);
455 }
456
457 #[test]
458 fn test_components_list_scrollable() {
459 let mut component = List::default()
460 .foreground(Color::Red)
461 .background(Color::Blue)
462 .highlighted_color(Color::Yellow)
463 .highlighted_str("🚀")
464 .modifiers(TextModifiers::BOLD)
465 .scroll(true)
466 .step(4)
467 .borders(Borders::default())
468 .title("events", Alignment::Center)
469 .rewind(true)
470 .rows(
471 TableBuilder::default()
472 .add_col(TextSpan::from("KeyCode::Down"))
473 .add_col(TextSpan::from("OnKey"))
474 .add_col(TextSpan::from("Move cursor down"))
475 .add_row()
476 .add_col(TextSpan::from("KeyCode::Up"))
477 .add_col(TextSpan::from("OnKey"))
478 .add_col(TextSpan::from("Move cursor up"))
479 .add_row()
480 .add_col(TextSpan::from("KeyCode::PageDown"))
481 .add_col(TextSpan::from("OnKey"))
482 .add_col(TextSpan::from("Move cursor down by 8"))
483 .add_row()
484 .add_col(TextSpan::from("KeyCode::PageUp"))
485 .add_col(TextSpan::from("OnKey"))
486 .add_col(TextSpan::from("ove cursor up by 8"))
487 .add_row()
488 .add_col(TextSpan::from("KeyCode::End"))
489 .add_col(TextSpan::from("OnKey"))
490 .add_col(TextSpan::from("Move cursor to last item"))
491 .add_row()
492 .add_col(TextSpan::from("KeyCode::Home"))
493 .add_col(TextSpan::from("OnKey"))
494 .add_col(TextSpan::from("Move cursor to first item"))
495 .add_row()
496 .add_col(TextSpan::from("KeyCode::Char(_)"))
497 .add_col(TextSpan::from("OnKey"))
498 .add_col(TextSpan::from("Return pressed key"))
499 .add_col(TextSpan::from("4th mysterious columns"))
500 .build(),
501 );
502 assert_eq!(component.states.list_len, 7);
503 assert_eq!(component.states.list_index, 0);
504 component.states.list_index += 1;
506 assert_eq!(component.states.list_index, 1);
507 assert_eq!(
510 component.perform(Cmd::Move(Direction::Down)),
511 CmdResult::Changed(State::One(StateValue::Usize(2)))
512 );
513 assert_eq!(component.states.list_index, 2);
515 assert_eq!(
517 component.perform(Cmd::Move(Direction::Up)),
518 CmdResult::Changed(State::One(StateValue::Usize(1)))
519 );
520 assert_eq!(component.states.list_index, 1);
522 assert_eq!(
524 component.perform(Cmd::Scroll(Direction::Down)),
525 CmdResult::Changed(State::One(StateValue::Usize(5)))
526 );
527 assert_eq!(component.states.list_index, 5);
529 assert_eq!(
530 component.perform(Cmd::Scroll(Direction::Down)),
531 CmdResult::Changed(State::One(StateValue::Usize(6)))
532 );
533 assert_eq!(component.states.list_index, 6);
535 assert_eq!(
537 component.perform(Cmd::Scroll(Direction::Up)),
538 CmdResult::Changed(State::One(StateValue::Usize(2)))
539 );
540 assert_eq!(component.states.list_index, 2);
541 assert_eq!(
542 component.perform(Cmd::Scroll(Direction::Up)),
543 CmdResult::Changed(State::One(StateValue::Usize(0)))
544 );
545 assert_eq!(component.states.list_index, 0);
546 assert_eq!(
548 component.perform(Cmd::GoTo(Position::End)),
549 CmdResult::Changed(State::One(StateValue::Usize(6)))
550 );
551 assert_eq!(component.states.list_index, 6);
552 assert_eq!(
554 component.perform(Cmd::GoTo(Position::Begin)),
555 CmdResult::Changed(State::One(StateValue::Usize(0)))
556 );
557 assert_eq!(component.states.list_index, 0);
558 component.attr(
560 Attribute::Content,
561 AttrValue::Table(
562 TableBuilder::default()
563 .add_col(TextSpan::from("name"))
564 .add_col(TextSpan::from("age"))
565 .add_col(TextSpan::from("birthdate"))
566 .build(),
567 ),
568 );
569 assert_eq!(component.states.list_len, 1);
570 assert_eq!(component.states.list_index, 0);
571 assert_eq!(component.state(), State::One(StateValue::Usize(0)));
573 }
574
575 #[test]
576 fn test_components_list() {
577 let component = List::default()
578 .foreground(Color::Red)
579 .background(Color::Blue)
580 .highlighted_color(Color::Yellow)
581 .highlighted_str("🚀")
582 .modifiers(TextModifiers::BOLD)
583 .borders(Borders::default())
584 .title("events", Alignment::Center)
585 .rows(
586 TableBuilder::default()
587 .add_col(TextSpan::from("KeyCode::Down"))
588 .add_col(TextSpan::from("OnKey"))
589 .add_col(TextSpan::from("Move cursor down"))
590 .add_row()
591 .add_col(TextSpan::from("KeyCode::Up"))
592 .add_col(TextSpan::from("OnKey"))
593 .add_col(TextSpan::from("Move cursor up"))
594 .add_row()
595 .add_col(TextSpan::from("KeyCode::PageDown"))
596 .add_col(TextSpan::from("OnKey"))
597 .add_col(TextSpan::from("Move cursor down by 8"))
598 .add_row()
599 .add_col(TextSpan::from("KeyCode::PageUp"))
600 .add_col(TextSpan::from("OnKey"))
601 .add_col(TextSpan::from("ove cursor up by 8"))
602 .add_row()
603 .add_col(TextSpan::from("KeyCode::End"))
604 .add_col(TextSpan::from("OnKey"))
605 .add_col(TextSpan::from("Move cursor to last item"))
606 .add_row()
607 .add_col(TextSpan::from("KeyCode::Home"))
608 .add_col(TextSpan::from("OnKey"))
609 .add_col(TextSpan::from("Move cursor to first item"))
610 .add_row()
611 .add_col(TextSpan::from("KeyCode::Char(_)"))
612 .add_col(TextSpan::from("OnKey"))
613 .add_col(TextSpan::from("Return pressed key"))
614 .build(),
615 );
616 assert_eq!(component.state(), State::None);
618 }
619
620 #[test]
621 fn should_init_list_value() {
622 let mut component = List::default()
623 .foreground(Color::Red)
624 .background(Color::Blue)
625 .highlighted_color(Color::Yellow)
626 .highlighted_str("🚀")
627 .modifiers(TextModifiers::BOLD)
628 .borders(Borders::default())
629 .title("events", Alignment::Center)
630 .rows(
631 TableBuilder::default()
632 .add_col(TextSpan::from("KeyCode::Down"))
633 .add_col(TextSpan::from("OnKey"))
634 .add_col(TextSpan::from("Move cursor down"))
635 .add_row()
636 .add_col(TextSpan::from("KeyCode::Up"))
637 .add_col(TextSpan::from("OnKey"))
638 .add_col(TextSpan::from("Move cursor up"))
639 .add_row()
640 .add_col(TextSpan::from("KeyCode::PageDown"))
641 .add_col(TextSpan::from("OnKey"))
642 .add_col(TextSpan::from("Move cursor down by 8"))
643 .add_row()
644 .add_col(TextSpan::from("KeyCode::PageUp"))
645 .add_col(TextSpan::from("OnKey"))
646 .add_col(TextSpan::from("ove cursor up by 8"))
647 .add_row()
648 .add_col(TextSpan::from("KeyCode::End"))
649 .add_col(TextSpan::from("OnKey"))
650 .add_col(TextSpan::from("Move cursor to last item"))
651 .add_row()
652 .add_col(TextSpan::from("KeyCode::Home"))
653 .add_col(TextSpan::from("OnKey"))
654 .add_col(TextSpan::from("Move cursor to first item"))
655 .add_row()
656 .add_col(TextSpan::from("KeyCode::Char(_)"))
657 .add_col(TextSpan::from("OnKey"))
658 .add_col(TextSpan::from("Return pressed key"))
659 .build(),
660 )
661 .scroll(true)
662 .selected_line(2);
663 assert_eq!(component.states.list_index, 2);
664 component.attr(
666 Attribute::Value,
667 AttrValue::Payload(PropPayload::One(PropValue::Usize(50))),
668 );
669 assert_eq!(component.states.list_index, 6);
670 }
671}