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 highlight_style_inactive(mut self, s: Style) -> Self {
189 self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
190 self
191 }
192
193 pub fn rows<T>(mut self, rows: impl IntoIterator<Item = T>) -> Self
195 where
196 T: Into<LineStatic>,
197 {
198 self.attr(
199 Attribute::Text,
200 AttrValue::Payload(PropPayload::Vec(
201 rows.into_iter()
202 .map(Into::into)
203 .map(PropValue::TextLine)
204 .collect(),
205 )),
206 );
207 self
208 }
209
210 pub fn selected_line(mut self, line: usize) -> Self {
212 self.attr(
213 Attribute::Value,
214 AttrValue::Payload(PropPayload::Single(PropValue::Usize(line))),
215 );
216 self
217 }
218
219 pub fn always_active(mut self) -> Self {
221 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
222 self
223 }
224
225 fn scrollable(&self) -> bool {
226 self.props
227 .get(Attribute::Scroll)
228 .and_then(AttrValue::as_flag)
229 .unwrap_or_default()
230 }
231
232 fn rewindable(&self) -> bool {
233 self.props
234 .get(Attribute::Rewind)
235 .and_then(AttrValue::as_flag)
236 .unwrap_or_default()
237 }
238}
239
240impl Component for List {
241 fn view(&mut self, render: &mut Frame, area: Rect) {
242 if !self.common.display {
243 return;
244 }
245
246 let payload = self.props.get(Attribute::Text).and_then(|x| x.as_payload());
248 let list_items: Vec<ListItem> = match payload {
249 Some(PropPayload::Vec(lines)) => {
250 lines
251 .iter()
252 .filter_map(|x| x.as_textline())
254 .map(utils::borrow_clone_line)
255 .map(ListItem::from)
256 .collect()
257 }
258 _ => Vec::new(),
259 };
260
261 let mut widget = TuiList::new(list_items)
263 .style(self.common.style)
264 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
265 .highlight_style(
266 self.common_hg
267 .get_style_focus(self.common.style, self.common.is_active()),
268 );
269
270 if let Some(block) = self.common.get_block() {
271 widget = widget.block(block);
272 }
273
274 if let Some(symbol) = self.common_hg.get_symbol() {
276 widget = widget.highlight_symbol(symbol);
277 }
278
279 if self.scrollable() {
280 let mut state: ListState = ListState::default();
281 state.select(Some(self.states.list_index));
282 render.render_stateful_widget(widget, area, &mut state);
283 } else {
284 render.render_widget(widget, area);
285 }
286 }
287
288 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
289 if let Some(value) = self
290 .common
291 .get_for_query(attr)
292 .or_else(|| self.common_hg.get_for_query(attr))
293 {
294 return Some(value);
295 }
296
297 self.props.get_for_query(attr)
298 }
299
300 fn attr(&mut self, attr: Attribute, value: AttrValue) {
301 if let Some(value) = self
302 .common
303 .set(attr, value)
304 .and_then(|value| self.common_hg.set(attr, value))
305 {
306 self.props.set(attr, value);
307 if matches!(attr, Attribute::Text) {
308 self.states.set_list_len(
310 match self
311 .props
312 .get(Attribute::Text)
313 .and_then(AttrValue::as_payload)
314 .and_then(PropPayload::as_vec)
315 {
316 Some(rows) => rows.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 .and_then(AttrValue::as_payload)
326 .and_then(PropPayload::as_single)
327 .and_then(PropValue::as_usize)
328 .unwrap_or_default();
329 self.states.fix_list_index();
330 }
331 }
332 }
333
334 fn state(&self) -> State {
335 if self.scrollable() {
336 State::Single(StateValue::Usize(self.states.list_index))
337 } else {
338 State::None
339 }
340 }
341
342 fn perform(&mut self, cmd: Cmd) -> CmdResult {
343 match cmd {
344 Cmd::Move(Direction::Down) => {
345 let prev = self.states.list_index;
346 self.states.incr_list_index(self.rewindable());
347 if prev == self.states.list_index {
348 CmdResult::NoChange
349 } else {
350 CmdResult::Changed(self.state())
351 }
352 }
353 Cmd::Move(Direction::Up) => {
354 let prev = self.states.list_index;
355 self.states.decr_list_index(self.rewindable());
356 if prev == self.states.list_index {
357 CmdResult::NoChange
358 } else {
359 CmdResult::Changed(self.state())
360 }
361 }
362 Cmd::Scroll(Direction::Down) => {
363 let prev = self.states.list_index;
364 let step = self
365 .props
366 .get(Attribute::ScrollStep)
367 .and_then(AttrValue::as_length)
368 .unwrap_or(8);
369 let step: usize = self.states.calc_max_step_ahead(step);
370 (0..step).for_each(|_| self.states.incr_list_index(false));
371 if prev == self.states.list_index {
372 CmdResult::NoChange
373 } else {
374 CmdResult::Changed(self.state())
375 }
376 }
377 Cmd::Scroll(Direction::Up) => {
378 let prev = self.states.list_index;
379 let step = self
380 .props
381 .get(Attribute::ScrollStep)
382 .and_then(AttrValue::as_length)
383 .unwrap_or(8);
384 let step: usize = self.states.calc_max_step_behind(step);
385 (0..step).for_each(|_| self.states.decr_list_index(false));
386 if prev == self.states.list_index {
387 CmdResult::NoChange
388 } else {
389 CmdResult::Changed(self.state())
390 }
391 }
392 Cmd::GoTo(Position::Begin) => {
393 let prev = self.states.list_index;
394 self.states.list_index_at_first();
395 if prev == self.states.list_index {
396 CmdResult::NoChange
397 } else {
398 CmdResult::Changed(self.state())
399 }
400 }
401 Cmd::GoTo(Position::End) => {
402 let prev = self.states.list_index;
403 self.states.list_index_at_last();
404 if prev == self.states.list_index {
405 CmdResult::NoChange
406 } else {
407 CmdResult::Changed(self.state())
408 }
409 }
410 _ => CmdResult::Invalid(cmd),
411 }
412 }
413}
414
415#[cfg(test)]
416mod tests {
417
418 use pretty_assertions::assert_eq;
419 use tuirealm::props::HorizontalAlignment;
420 use tuirealm::ratatui::text::{Line, Span};
421
422 use super::*;
423
424 #[test]
425 fn list_states() {
426 let mut states = ListStates::default();
427 assert_eq!(states.list_index, 0);
428 assert_eq!(states.list_len, 0);
429 states.set_list_len(5);
430 assert_eq!(states.list_index, 0);
431 assert_eq!(states.list_len, 5);
432 states.incr_list_index(true);
434 assert_eq!(states.list_index, 1);
435 states.list_index = 4;
436 states.incr_list_index(false);
437 assert_eq!(states.list_index, 4);
438 states.incr_list_index(true);
439 assert_eq!(states.list_index, 0);
440 states.decr_list_index(false);
442 assert_eq!(states.list_index, 0);
443 states.decr_list_index(true);
444 assert_eq!(states.list_index, 4);
445 states.decr_list_index(true);
446 assert_eq!(states.list_index, 3);
447 states.list_index_at_first();
449 assert_eq!(states.list_index, 0);
450 states.list_index_at_last();
451 assert_eq!(states.list_index, 4);
452 states.set_list_len(3);
454 states.fix_list_index();
455 assert_eq!(states.list_index, 2);
456 }
457
458 #[test]
459 fn test_components_list_scrollable() {
460 let mut component = List::default()
461 .foreground(Color::Red)
462 .background(Color::Blue)
463 .highlight_style(Style::new().fg(Color::Yellow))
464 .highlight_str("🚀")
465 .modifiers(TextModifiers::BOLD)
466 .scroll(true)
467 .step(4)
468 .borders(Borders::default())
469 .title(Title::from("events").alignment(HorizontalAlignment::Center))
470 .rewind(true)
471 .rows([
472 vec![
475 Span::from("KeyCode::Down"),
476 Span::from("OnKey"),
477 Span::from("Move cursor down"),
478 ],
479 vec![
480 Span::from("KeyCode::Up"),
481 Span::from("OnKey"),
482 Span::from("Move cursor up"),
483 ],
484 vec![
485 Span::from("KeyCode::PageDown"),
486 Span::from("OnKey"),
487 Span::from("Move cursor down by 8"),
488 ],
489 vec![
490 Span::from("KeyCode::PageUp"),
491 Span::from("OnKey"),
492 Span::from("ove cursor up by 8"),
493 ],
494 vec![
495 Span::from("KeyCode::End"),
496 Span::from("OnKey"),
497 Span::from("Move cursor to last item"),
498 ],
499 vec![
500 Span::from("KeyCode::Home"),
501 Span::from("OnKey"),
502 Span::from("Move cursor to first item"),
503 ],
504 vec![
505 Span::from("KeyCode::Char(_)"),
506 Span::from("OnKey"),
507 Span::from("Return pressed key"),
508 Span::from("4th mysterious columns"),
509 ],
510 ]);
511 assert_eq!(component.states.list_len, 7);
512 assert_eq!(component.states.list_index, 0);
513 component.states.list_index += 1;
515 assert_eq!(component.states.list_index, 1);
516 assert_eq!(
519 component.perform(Cmd::Move(Direction::Down)),
520 CmdResult::Changed(State::Single(StateValue::Usize(2)))
521 );
522 assert_eq!(component.states.list_index, 2);
524 assert_eq!(
526 component.perform(Cmd::Move(Direction::Up)),
527 CmdResult::Changed(State::Single(StateValue::Usize(1)))
528 );
529 assert_eq!(component.states.list_index, 1);
531 assert_eq!(
533 component.perform(Cmd::Scroll(Direction::Down)),
534 CmdResult::Changed(State::Single(StateValue::Usize(5)))
535 );
536 assert_eq!(component.states.list_index, 5);
538 assert_eq!(
539 component.perform(Cmd::Scroll(Direction::Down)),
540 CmdResult::Changed(State::Single(StateValue::Usize(6)))
541 );
542 assert_eq!(component.states.list_index, 6);
544 assert_eq!(
546 component.perform(Cmd::Scroll(Direction::Up)),
547 CmdResult::Changed(State::Single(StateValue::Usize(2)))
548 );
549 assert_eq!(component.states.list_index, 2);
550 assert_eq!(
551 component.perform(Cmd::Scroll(Direction::Up)),
552 CmdResult::Changed(State::Single(StateValue::Usize(0)))
553 );
554 assert_eq!(component.states.list_index, 0);
555 assert_eq!(
557 component.perform(Cmd::GoTo(Position::End)),
558 CmdResult::Changed(State::Single(StateValue::Usize(6)))
559 );
560 assert_eq!(component.states.list_index, 6);
561 assert_eq!(
563 component.perform(Cmd::GoTo(Position::Begin)),
564 CmdResult::Changed(State::Single(StateValue::Usize(0)))
565 );
566 assert_eq!(component.states.list_index, 0);
567 component.attr(
569 Attribute::Text,
570 AttrValue::Payload(PropPayload::Vec(vec![PropValue::TextLine(Line::from(
571 "name age birthdate",
572 ))])),
573 );
574 assert_eq!(component.states.list_len, 1);
575 assert_eq!(component.states.list_index, 0);
576 assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
578 }
579
580 #[test]
581 fn test_components_list() {
582 let component = List::default()
583 .foreground(Color::Red)
584 .background(Color::Blue)
585 .highlight_style(Style::new().fg(Color::Yellow))
586 .highlight_str("🚀")
587 .modifiers(TextModifiers::BOLD)
588 .borders(Borders::default())
589 .title(Title::from("events").alignment(HorizontalAlignment::Center))
590 .rows([
591 Line::from("KeyCode::Down OnKey Move cursor down"),
592 Line::from("KeyCode::Up OnKey Move cursor up"),
593 Line::from("KeyCode::PageDown OnKey Move cursor down by 8"),
594 Line::from("KeyCode::PageUp OnKey Move cursor up by 8"),
595 Line::from("KeyCode::End OnKey Move cursor to last item"),
596 Line::from("KeyCode::Home OnKey Move cursor to first item"),
597 Line::from("KeyCode::Char(_) OnKey Return pressed key"),
598 ]);
599 assert_eq!(component.state(), State::None);
601 }
602
603 #[test]
604 fn should_init_list_value() {
605 let mut component = List::default()
606 .foreground(Color::Red)
607 .background(Color::Blue)
608 .highlight_style(Style::new().fg(Color::Yellow))
609 .highlight_str("🚀")
610 .modifiers(TextModifiers::BOLD)
611 .borders(Borders::default())
612 .title(Title::from("events").alignment(HorizontalAlignment::Center))
613 .rows([
614 "KeyCode::Down OnKey Move cursor down",
615 "KeyCode::Up OnKey Move cursor up",
616 "KeyCode::PageDown OnKey Move cursor down by 8",
617 "KeyCode::PageUp OnKey Move cursor up by 8",
618 "KeyCode::End OnKey Move cursor to last item",
619 "KeyCode::Home OnKey Move cursor to first item",
620 "KeyCode::Char(_) OnKey Return pressed key",
621 ])
622 .scroll(true)
623 .selected_line(2);
624 assert_eq!(component.states.list_index, 2);
625 component.attr(
627 Attribute::Value,
628 AttrValue::Payload(PropPayload::Single(PropValue::Usize(50))),
629 );
630 assert_eq!(component.states.list_index, 6);
631 }
632}