1use tuirealm::command::{Cmd, CmdResult, Direction, Position};
4use tuirealm::component::Component;
5use tuirealm::props::{
6 AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, QueryResult,
7 Style, TextModifiers, Title,
8};
9use tuirealm::ratatui::Frame;
10use tuirealm::ratatui::layout::Rect;
11use tuirealm::ratatui::widgets::{List as TuiList, ListItem, ListState};
12use tuirealm::state::{State, StateValue};
13
14use crate::prop_ext::{CommonHighlight, CommonProps};
15use crate::utils;
16
17#[derive(Default)]
21pub struct ListStates {
22 pub list_index: usize,
24 pub list_len: usize,
26}
27
28impl ListStates {
29 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) {
36 if self.list_index + 1 < self.list_len {
38 self.list_index += 1;
39 } else if rewind {
40 self.list_index = 0;
41 }
42 }
43
44 pub fn decr_list_index(&mut self, rewind: bool) {
46 if self.list_index > 0 {
48 self.list_index -= 1;
49 } else if rewind && self.list_len > 0 {
50 self.list_index = self.list_len - 1;
51 }
52 }
53
54 pub fn fix_list_index(&mut self) {
56 if self.list_index >= self.list_len && self.list_len > 0 {
57 self.list_index = self.list_len - 1;
58 } else if self.list_len == 0 {
59 self.list_index = 0;
60 }
61 }
62
63 pub fn list_index_at_first(&mut self) {
65 self.list_index = 0;
66 }
67
68 pub fn list_index_at_last(&mut self) {
70 if self.list_len > 0 {
71 self.list_index = self.list_len - 1;
72 } else {
73 self.list_index = 0;
74 }
75 }
76
77 #[must_use]
79 pub fn calc_max_step_ahead(&self, max: usize) -> usize {
80 let remaining: usize = match self.list_len {
81 0 => 0,
82 len => len - 1 - self.list_index,
83 };
84 if remaining > max { max } else { remaining }
85 }
86
87 #[must_use]
89 pub fn calc_max_step_behind(&self, max: usize) -> usize {
90 if self.list_index > max {
91 max
92 } else {
93 self.list_index
94 }
95 }
96}
97
98#[derive(Default)]
102#[must_use]
103pub struct List {
104 common: CommonProps,
105 common_hg: CommonHighlight,
106 props: Props,
107 pub states: ListStates,
108}
109
110impl List {
111 pub fn foreground(mut self, fg: Color) -> Self {
113 self.attr(Attribute::Foreground, AttrValue::Color(fg));
114 self
115 }
116
117 pub fn background(mut self, bg: Color) -> Self {
119 self.attr(Attribute::Background, AttrValue::Color(bg));
120 self
121 }
122
123 pub fn modifiers(mut self, m: TextModifiers) -> Self {
125 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
126 self
127 }
128
129 pub fn style(mut self, style: Style) -> Self {
133 self.attr(Attribute::Style, AttrValue::Style(style));
134 self
135 }
136
137 pub fn inactive(mut self, s: Style) -> Self {
139 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
140 self
141 }
142
143 pub fn borders(mut self, b: Borders) -> Self {
145 self.attr(Attribute::Borders, AttrValue::Borders(b));
146 self
147 }
148
149 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
151 self.attr(Attribute::Title, AttrValue::Title(title.into()));
152 self
153 }
154
155 pub fn rewind(mut self, r: bool) -> Self {
157 self.attr(Attribute::Rewind, AttrValue::Flag(r));
158 self
159 }
160
161 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 {
169 self.attr(Attribute::Scroll, AttrValue::Flag(scrollable));
170 self
171 }
172
173 pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
175 self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
176 self
177 }
178
179 pub fn highlight_style(mut self, s: Style) -> Self {
183 self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
184 self
185 }
186
187 pub fn rows<T>(mut self, rows: impl IntoIterator<Item = T>) -> Self
189 where
190 T: Into<LineStatic>,
191 {
192 self.attr(
193 Attribute::Text,
194 AttrValue::Payload(PropPayload::Vec(
195 rows.into_iter()
196 .map(Into::into)
197 .map(PropValue::TextLine)
198 .collect(),
199 )),
200 );
201 self
202 }
203
204 pub fn selected_line(mut self, line: usize) -> Self {
206 self.attr(
207 Attribute::Value,
208 AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
209 );
210 self
211 }
212
213 pub fn always_active(mut self) -> Self {
215 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
216 self
217 }
218
219 fn scrollable(&self) -> bool {
220 self.props
221 .get(Attribute::Scroll)
222 .and_then(AttrValue::as_flag)
223 .unwrap_or_default()
224 }
225
226 fn rewindable(&self) -> bool {
227 self.props
228 .get(Attribute::Rewind)
229 .and_then(AttrValue::as_flag)
230 .unwrap_or_default()
231 }
232}
233
234impl Component for List {
235 fn view(&mut self, render: &mut Frame, area: Rect) {
236 if !self.common.display {
237 return;
238 }
239
240 let payload = self.props.get(Attribute::Text).and_then(|x| x.as_payload());
242 let list_items: Vec<ListItem> = match payload {
243 Some(PropPayload::Vec(lines)) => {
244 lines
245 .iter()
246 .filter_map(|x| x.as_textline())
248 .map(utils::borrow_clone_line)
249 .map(ListItem::from)
250 .collect()
251 }
252 _ => Vec::new(),
253 };
254
255 let mut widget = TuiList::new(list_items)
257 .style(self.common.style)
258 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom);
259
260 if self.common.is_active() {
261 widget = widget.highlight_style(self.common_hg.get_style(self.common.style));
262 }
263
264 if let Some(block) = self.common.get_block() {
265 widget = widget.block(block);
266 }
267
268 if let Some(symbol) = self.common_hg.get_symbol() {
270 widget = widget.highlight_symbol(symbol);
271 }
272
273 if self.scrollable() {
274 let mut state: ListState = ListState::default();
275 state.select(Some(self.states.list_index));
276 render.render_stateful_widget(widget, area, &mut state);
277 } else {
278 render.render_widget(widget, area);
279 }
280 }
281
282 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
283 if let Some(value) = self
284 .common
285 .get_for_query(attr)
286 .or_else(|| self.common_hg.get_for_query(attr))
287 {
288 return Some(value);
289 }
290
291 self.props.get_for_query(attr)
292 }
293
294 fn attr(&mut self, attr: Attribute, value: AttrValue) {
295 if let Some(value) = self
296 .common
297 .set(attr, value)
298 .and_then(|value| self.common_hg.set(attr, value))
299 {
300 self.props.set(attr, value);
301 if matches!(attr, Attribute::Text) {
302 self.states.set_list_len(
304 match self
305 .props
306 .get(Attribute::Text)
307 .and_then(AttrValue::as_payload)
308 .and_then(PropPayload::as_vec)
309 {
310 Some(rows) => rows.len(),
311 _ => 0,
312 },
313 );
314 self.states.fix_list_index();
315 } else if matches!(attr, Attribute::Value) && self.scrollable() {
316 self.states.list_index = self
317 .props
318 .get(Attribute::Value)
319 .and_then(AttrValue::as_payload)
320 .and_then(PropPayload::as_single)
321 .and_then(PropValue::as_usize)
322 .unwrap_or_default();
323 self.states.fix_list_index();
324 }
325 }
326 }
327
328 fn state(&self) -> State {
329 if self.scrollable() {
330 State::Single(StateValue::Usize(self.states.list_index))
331 } else {
332 State::None
333 }
334 }
335
336 fn perform(&mut self, cmd: Cmd) -> CmdResult {
337 match cmd {
338 Cmd::Move(Direction::Down) => {
339 let prev = self.states.list_index;
340 self.states.incr_list_index(self.rewindable());
341 if prev == self.states.list_index {
342 CmdResult::NoChange
343 } else {
344 CmdResult::Changed(self.state())
345 }
346 }
347 Cmd::Move(Direction::Up) => {
348 let prev = self.states.list_index;
349 self.states.decr_list_index(self.rewindable());
350 if prev == self.states.list_index {
351 CmdResult::NoChange
352 } else {
353 CmdResult::Changed(self.state())
354 }
355 }
356 Cmd::Scroll(Direction::Down) => {
357 let prev = self.states.list_index;
358 let step = self
359 .props
360 .get(Attribute::ScrollStep)
361 .and_then(AttrValue::as_length)
362 .unwrap_or(8);
363 let step: usize = self.states.calc_max_step_ahead(step);
364 (0..step).for_each(|_| self.states.incr_list_index(false));
365 if prev == self.states.list_index {
366 CmdResult::NoChange
367 } else {
368 CmdResult::Changed(self.state())
369 }
370 }
371 Cmd::Scroll(Direction::Up) => {
372 let prev = self.states.list_index;
373 let step = self
374 .props
375 .get(Attribute::ScrollStep)
376 .and_then(AttrValue::as_length)
377 .unwrap_or(8);
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::NoChange
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::NoChange
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::NoChange
400 } else {
401 CmdResult::Changed(self.state())
402 }
403 }
404 _ => CmdResult::Invalid(cmd),
405 }
406 }
407}
408
409#[cfg(test)]
410mod tests {
411
412 use pretty_assertions::assert_eq;
413 use tuirealm::props::HorizontalAlignment;
414 use tuirealm::ratatui::text::{Line, Span};
415
416 use super::*;
417
418 #[test]
419 fn list_states() {
420 let mut states = ListStates::default();
421 assert_eq!(states.list_index, 0);
422 assert_eq!(states.list_len, 0);
423 states.set_list_len(5);
424 assert_eq!(states.list_index, 0);
425 assert_eq!(states.list_len, 5);
426 states.incr_list_index(true);
428 assert_eq!(states.list_index, 1);
429 states.list_index = 4;
430 states.incr_list_index(false);
431 assert_eq!(states.list_index, 4);
432 states.incr_list_index(true);
433 assert_eq!(states.list_index, 0);
434 states.decr_list_index(false);
436 assert_eq!(states.list_index, 0);
437 states.decr_list_index(true);
438 assert_eq!(states.list_index, 4);
439 states.decr_list_index(true);
440 assert_eq!(states.list_index, 3);
441 states.list_index_at_first();
443 assert_eq!(states.list_index, 0);
444 states.list_index_at_last();
445 assert_eq!(states.list_index, 4);
446 states.set_list_len(3);
448 states.fix_list_index();
449 assert_eq!(states.list_index, 2);
450 }
451
452 #[test]
453 fn test_components_list_scrollable() {
454 let mut component = List::default()
455 .foreground(Color::Red)
456 .background(Color::Blue)
457 .highlight_style(Style::new().fg(Color::Yellow))
458 .highlight_str("🚀")
459 .modifiers(TextModifiers::BOLD)
460 .scroll(true)
461 .step(4)
462 .borders(Borders::default())
463 .title(Title::from("events").alignment(HorizontalAlignment::Center))
464 .rewind(true)
465 .rows([
466 vec![
469 Span::from("KeyCode::Down"),
470 Span::from("OnKey"),
471 Span::from("Move cursor down"),
472 ],
473 vec![
474 Span::from("KeyCode::Up"),
475 Span::from("OnKey"),
476 Span::from("Move cursor up"),
477 ],
478 vec![
479 Span::from("KeyCode::PageDown"),
480 Span::from("OnKey"),
481 Span::from("Move cursor down by 8"),
482 ],
483 vec![
484 Span::from("KeyCode::PageUp"),
485 Span::from("OnKey"),
486 Span::from("ove cursor up by 8"),
487 ],
488 vec![
489 Span::from("KeyCode::End"),
490 Span::from("OnKey"),
491 Span::from("Move cursor to last item"),
492 ],
493 vec![
494 Span::from("KeyCode::Home"),
495 Span::from("OnKey"),
496 Span::from("Move cursor to first item"),
497 ],
498 vec![
499 Span::from("KeyCode::Char(_)"),
500 Span::from("OnKey"),
501 Span::from("Return pressed key"),
502 Span::from("4th mysterious columns"),
503 ],
504 ]);
505 assert_eq!(component.states.list_len, 7);
506 assert_eq!(component.states.list_index, 0);
507 component.states.list_index += 1;
509 assert_eq!(component.states.list_index, 1);
510 assert_eq!(
513 component.perform(Cmd::Move(Direction::Down)),
514 CmdResult::Changed(State::Single(StateValue::Usize(2)))
515 );
516 assert_eq!(component.states.list_index, 2);
518 assert_eq!(
520 component.perform(Cmd::Move(Direction::Up)),
521 CmdResult::Changed(State::Single(StateValue::Usize(1)))
522 );
523 assert_eq!(component.states.list_index, 1);
525 assert_eq!(
527 component.perform(Cmd::Scroll(Direction::Down)),
528 CmdResult::Changed(State::Single(StateValue::Usize(5)))
529 );
530 assert_eq!(component.states.list_index, 5);
532 assert_eq!(
533 component.perform(Cmd::Scroll(Direction::Down)),
534 CmdResult::Changed(State::Single(StateValue::Usize(6)))
535 );
536 assert_eq!(component.states.list_index, 6);
538 assert_eq!(
540 component.perform(Cmd::Scroll(Direction::Up)),
541 CmdResult::Changed(State::Single(StateValue::Usize(2)))
542 );
543 assert_eq!(component.states.list_index, 2);
544 assert_eq!(
545 component.perform(Cmd::Scroll(Direction::Up)),
546 CmdResult::Changed(State::Single(StateValue::Usize(0)))
547 );
548 assert_eq!(component.states.list_index, 0);
549 assert_eq!(
551 component.perform(Cmd::GoTo(Position::End)),
552 CmdResult::Changed(State::Single(StateValue::Usize(6)))
553 );
554 assert_eq!(component.states.list_index, 6);
555 assert_eq!(
557 component.perform(Cmd::GoTo(Position::Begin)),
558 CmdResult::Changed(State::Single(StateValue::Usize(0)))
559 );
560 assert_eq!(component.states.list_index, 0);
561 component.attr(
563 Attribute::Text,
564 AttrValue::Payload(PropPayload::Vec(vec![PropValue::TextLine(Line::from(
565 "name age birthdate",
566 ))])),
567 );
568 assert_eq!(component.states.list_len, 1);
569 assert_eq!(component.states.list_index, 0);
570 assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
572 }
573
574 #[test]
575 fn test_components_list() {
576 let component = List::default()
577 .foreground(Color::Red)
578 .background(Color::Blue)
579 .highlight_style(Style::new().fg(Color::Yellow))
580 .highlight_str("🚀")
581 .modifiers(TextModifiers::BOLD)
582 .borders(Borders::default())
583 .title(Title::from("events").alignment(HorizontalAlignment::Center))
584 .rows([
585 Line::from("KeyCode::Down OnKey Move cursor down"),
586 Line::from("KeyCode::Up OnKey Move cursor up"),
587 Line::from("KeyCode::PageDown OnKey Move cursor down by 8"),
588 Line::from("KeyCode::PageUp OnKey Move cursor up by 8"),
589 Line::from("KeyCode::End OnKey Move cursor to last item"),
590 Line::from("KeyCode::Home OnKey Move cursor to first item"),
591 Line::from("KeyCode::Char(_) OnKey Return pressed key"),
592 ]);
593 assert_eq!(component.state(), State::None);
595 }
596
597 #[test]
598 fn should_init_list_value() {
599 let mut component = List::default()
600 .foreground(Color::Red)
601 .background(Color::Blue)
602 .highlight_style(Style::new().fg(Color::Yellow))
603 .highlight_str("🚀")
604 .modifiers(TextModifiers::BOLD)
605 .borders(Borders::default())
606 .title(Title::from("events").alignment(HorizontalAlignment::Center))
607 .rows([
608 "KeyCode::Down OnKey Move cursor down",
609 "KeyCode::Up OnKey Move cursor up",
610 "KeyCode::PageDown OnKey Move cursor down by 8",
611 "KeyCode::PageUp OnKey Move cursor up by 8",
612 "KeyCode::End OnKey Move cursor to last item",
613 "KeyCode::Home OnKey Move cursor to first item",
614 "KeyCode::Char(_) OnKey Return pressed key",
615 ])
616 .scroll(true)
617 .selected_line(2);
618 assert_eq!(component.states.list_index, 2);
619 component.attr(
621 Attribute::Value,
622 AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
623 );
624 assert_eq!(component.states.list_index, 6);
625 }
626}