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