1use tuirealm::command::{Cmd, CmdResult, Direction};
7use tuirealm::props::{
8 Alignment, AttrValue, Attribute, BorderSides, Borders, Color, PropPayload, PropValue, Props,
9 Style, TextModifiers,
10};
11use tuirealm::ratatui::text::Line as Spans;
12use tuirealm::ratatui::{
13 layout::{Constraint, Direction as LayoutDirection, Layout, Rect},
14 widgets::{Block, List, ListItem, ListState, Paragraph},
15};
16use tuirealm::{Frame, MockComponent, State, StateValue};
17
18#[derive(Default)]
24pub struct SelectStates {
25 pub choices: Vec<String>,
27 pub selected: usize,
29 pub previously_selected: usize,
31 pub tab_open: bool,
32}
33
34impl SelectStates {
35 pub fn next_choice(&mut self, rewind: bool) {
39 if self.tab_open {
40 if rewind && self.selected + 1 >= self.choices.len() {
41 self.selected = 0;
42 } else if self.selected + 1 < self.choices.len() {
43 self.selected += 1;
44 }
45 }
46 }
47
48 pub fn prev_choice(&mut self, rewind: bool) {
52 if self.tab_open {
53 if rewind && self.selected == 0 && !self.choices.is_empty() {
54 self.selected = self.choices.len() - 1;
55 } else if self.selected > 0 {
56 self.selected -= 1;
57 }
58 }
59 }
60
61 pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
67 self.choices = choices.into();
68 if self.selected >= self.choices.len() {
70 self.selected = match self.choices.len() {
71 0 => 0,
72 l => l - 1,
73 };
74 }
75 }
76
77 pub fn select(&mut self, i: usize) {
78 if i < self.choices.len() {
79 self.selected = i;
80 }
81 }
82
83 pub fn close_tab(&mut self) {
87 self.tab_open = false;
88 }
89
90 pub fn open_tab(&mut self) {
94 self.previously_selected = self.selected;
95 self.tab_open = true;
96 }
97
98 pub fn cancel_tab(&mut self) {
100 self.close_tab();
101 self.selected = self.previously_selected;
102 }
103
104 #[must_use]
108 pub fn is_tab_open(&self) -> bool {
109 self.tab_open
110 }
111}
112
113#[derive(Default)]
116#[must_use]
117pub struct Select {
118 props: Props,
119 pub states: SelectStates,
120 hg_str: Option<String>, }
122
123impl Select {
124 pub fn foreground(mut self, fg: Color) -> Self {
125 self.attr(Attribute::Foreground, AttrValue::Color(fg));
126 self
127 }
128
129 pub fn background(mut self, bg: Color) -> Self {
130 self.attr(Attribute::Background, AttrValue::Color(bg));
131 self
132 }
133
134 pub fn borders(mut self, b: Borders) -> Self {
135 self.attr(Attribute::Borders, AttrValue::Borders(b));
136 self
137 }
138
139 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
140 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
141 self
142 }
143
144 pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
145 self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
146 self
147 }
148
149 pub fn highlighted_color(mut self, c: Color) -> Self {
150 self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
151 self
152 }
153
154 pub fn inactive(mut self, s: Style) -> Self {
155 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
156 self
157 }
158
159 pub fn rewind(mut self, r: bool) -> Self {
160 self.attr(Attribute::Rewind, AttrValue::Flag(r));
161 self
162 }
163
164 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
165 self.attr(
166 Attribute::Content,
167 AttrValue::Payload(PropPayload::Vec(
168 choices
169 .into_iter()
170 .map(|v| PropValue::Str(v.into()))
171 .collect(),
172 )),
173 );
174 self
175 }
176
177 pub fn value(mut self, i: usize) -> Self {
178 self.attr(
180 Attribute::Value,
181 AttrValue::Payload(PropPayload::One(PropValue::Usize(i))),
182 );
183 self
184 }
185
186 fn render_open_tab(&mut self, render: &mut Frame, area: Rect) {
190 let choices: Vec<ListItem> = self
192 .states
193 .choices
194 .iter()
195 .map(|x| ListItem::new(Spans::from(x.as_str())))
196 .collect();
197 let foreground = self
198 .props
199 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
200 .unwrap_color();
201 let background = self
202 .props
203 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
204 .unwrap_color();
205 let hg: Color = self
206 .props
207 .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground))
208 .unwrap_color();
209 let chunks = Layout::default()
211 .direction(LayoutDirection::Vertical)
212 .margin(0)
213 .constraints([Constraint::Length(2), Constraint::Min(1)].as_ref())
214 .split(area);
215 let selected_text: String = match self.states.choices.get(self.states.selected) {
217 None => String::default(),
218 Some(s) => s.clone(),
219 };
220 let borders = self
221 .props
222 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
223 .unwrap_borders();
224 let block: Block = Block::default()
225 .borders(BorderSides::LEFT | BorderSides::TOP | BorderSides::RIGHT)
226 .border_style(borders.style())
227 .border_type(borders.modifiers)
228 .style(Style::default().bg(background));
229 let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
230 let block = match title {
231 Some((text, alignment)) => block.title(text).title_alignment(alignment),
232 None => block,
233 };
234 let focus = self
235 .props
236 .get_or(Attribute::Focus, AttrValue::Flag(false))
237 .unwrap_flag();
238 let inactive_style = self
239 .props
240 .get(Attribute::FocusStyle)
241 .map(|x| x.unwrap_style());
242 let p: Paragraph = Paragraph::new(selected_text)
243 .style(if focus {
244 borders.style()
245 } else {
246 inactive_style.unwrap_or_default()
247 })
248 .block(block);
249 render.render_widget(p, chunks[0]);
250 let mut list = List::new(choices)
253 .block(
254 Block::default()
255 .borders(BorderSides::LEFT | BorderSides::BOTTOM | BorderSides::RIGHT)
256 .border_style(if focus {
257 borders.style()
258 } else {
259 Style::default()
260 })
261 .border_type(borders.modifiers)
262 .style(Style::default().bg(background)),
263 )
264 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
265 .style(Style::default().fg(foreground).bg(background))
266 .highlight_style(
267 Style::default()
268 .fg(hg)
269 .add_modifier(TextModifiers::REVERSED),
270 );
271 self.hg_str = self
273 .props
274 .get(Attribute::HighlightedStr)
275 .map(|x| x.unwrap_string());
276 if let Some(hg_str) = &self.hg_str {
277 list = list.highlight_symbol(hg_str);
278 }
279 let mut state: ListState = ListState::default();
280 state.select(Some(self.states.selected));
281 render.render_stateful_widget(list, chunks[1], &mut state);
282 }
283
284 fn render_closed_tab(&self, render: &mut Frame, area: Rect) {
288 let foreground = self
289 .props
290 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
291 .unwrap_color();
292 let background = self
293 .props
294 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
295 .unwrap_color();
296 let inactive_style = self
297 .props
298 .get(Attribute::FocusStyle)
299 .map(|x| x.unwrap_style());
300 let focus = self
301 .props
302 .get_or(Attribute::Focus, AttrValue::Flag(false))
303 .unwrap_flag();
304 let style = if focus {
305 Style::default().bg(background).fg(foreground)
306 } else {
307 inactive_style.unwrap_or_default()
308 };
309 let borders = self
310 .props
311 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
312 .unwrap_borders();
313 let borders_style = if focus {
314 borders.style()
315 } else {
316 inactive_style.unwrap_or_default()
317 };
318 let block: Block = Block::default()
319 .borders(BorderSides::ALL)
320 .border_style(borders_style)
321 .border_type(borders.modifiers)
322 .style(style);
323 let title = self.props.get(Attribute::Title).map(|x| x.unwrap_title());
324 let block = match title {
325 Some((text, alignment)) => block.title(text).title_alignment(alignment),
326 None => block,
327 };
328 let selected_text: String = match self.states.choices.get(self.states.selected) {
329 None => String::default(),
330 Some(s) => s.clone(),
331 };
332 let p: Paragraph = Paragraph::new(selected_text).style(style).block(block);
333 render.render_widget(p, area);
334 }
335
336 fn rewindable(&self) -> bool {
337 self.props
338 .get_or(Attribute::Rewind, AttrValue::Flag(false))
339 .unwrap_flag()
340 }
341}
342
343impl MockComponent for Select {
344 fn view(&mut self, render: &mut Frame, area: Rect) {
345 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
346 if self.states.is_tab_open() {
347 self.render_open_tab(render, area);
348 } else {
349 self.render_closed_tab(render, area);
350 }
351 }
352 }
353
354 fn query(&self, attr: Attribute) -> Option<AttrValue> {
355 self.props.get(attr)
356 }
357
358 fn attr(&mut self, attr: Attribute, value: AttrValue) {
359 match attr {
360 Attribute::Content => {
361 let choices: Vec<String> = value
363 .unwrap_payload()
364 .unwrap_vec()
365 .iter()
366 .map(|x| x.clone().unwrap_str())
367 .collect();
368 self.states.set_choices(choices);
369 }
370 Attribute::Value => {
371 self.states
372 .select(value.unwrap_payload().unwrap_one().unwrap_usize());
373 }
374 Attribute::Focus if self.states.is_tab_open() => {
375 if let AttrValue::Flag(false) = value {
376 self.states.cancel_tab();
377 }
378 self.props.set(attr, value);
379 }
380 attr => {
381 self.props.set(attr, value);
382 }
383 }
384 }
385
386 fn state(&self) -> State {
387 if self.states.is_tab_open() {
388 State::None
389 } else {
390 State::One(StateValue::Usize(self.states.selected))
391 }
392 }
393
394 fn perform(&mut self, cmd: Cmd) -> CmdResult {
395 match cmd {
396 Cmd::Move(Direction::Down) => {
397 self.states.next_choice(self.rewindable());
399 if self.states.is_tab_open() {
401 CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
402 } else {
403 CmdResult::None
404 }
405 }
406 Cmd::Move(Direction::Up) => {
407 self.states.prev_choice(self.rewindable());
409 if self.states.is_tab_open() {
411 CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
412 } else {
413 CmdResult::None
414 }
415 }
416 Cmd::Cancel => {
417 self.states.cancel_tab();
418 CmdResult::Changed(self.state())
419 }
420 Cmd::Submit => {
421 if self.states.is_tab_open() {
423 self.states.close_tab();
424 CmdResult::Submit(self.state())
425 } else {
426 self.states.open_tab();
427 CmdResult::None
428 }
429 }
430 _ => CmdResult::None,
431 }
432 }
433}
434
435#[cfg(test)]
436mod test {
437
438 use super::*;
439
440 use pretty_assertions::assert_eq;
441
442 use tuirealm::props::{PropPayload, PropValue};
443
444 #[test]
445 fn test_components_select_states() {
446 let mut states: SelectStates = SelectStates::default();
447 assert_eq!(states.selected, 0);
448 assert_eq!(states.choices.len(), 0);
449 assert_eq!(states.tab_open, false);
450 let choices: &[String] = &[
451 "lemon".to_string(),
452 "strawberry".to_string(),
453 "vanilla".to_string(),
454 "chocolate".to_string(),
455 ];
456 states.set_choices(choices);
457 assert_eq!(states.selected, 0);
458 assert_eq!(states.choices.len(), 4);
459 states.prev_choice(false);
461 assert_eq!(states.selected, 0);
462 states.next_choice(false);
463 assert_eq!(states.selected, 0);
465 states.open_tab();
466 assert_eq!(states.is_tab_open(), true);
467 states.next_choice(false);
469 assert_eq!(states.selected, 1);
470 states.next_choice(false);
471 assert_eq!(states.selected, 2);
472 states.next_choice(false);
474 states.next_choice(false);
475 assert_eq!(states.selected, 3);
476 states.prev_choice(false);
477 assert_eq!(states.selected, 2);
478 states.close_tab();
480 assert_eq!(states.is_tab_open(), false);
481 states.prev_choice(false);
482 assert_eq!(states.selected, 2);
483 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
485 states.set_choices(choices);
486 assert_eq!(states.selected, 1); assert_eq!(states.choices.len(), 2);
488 let choices = vec![];
489 states.set_choices(choices);
490 assert_eq!(states.selected, 0); assert_eq!(states.choices.len(), 0);
492 let choices: &[String] = &[
494 "lemon".to_string(),
495 "strawberry".to_string(),
496 "vanilla".to_string(),
497 "chocolate".to_string(),
498 ];
499 states.set_choices(choices);
500 states.open_tab();
501 assert_eq!(states.selected, 0);
502 states.prev_choice(true);
503 assert_eq!(states.selected, 3);
504 states.next_choice(true);
505 assert_eq!(states.selected, 0);
506 states.next_choice(true);
507 assert_eq!(states.selected, 1);
508 states.prev_choice(true);
509 assert_eq!(states.selected, 0);
510 states.close_tab();
512 states.select(2);
513 states.open_tab();
514 states.prev_choice(true);
515 states.prev_choice(true);
516 assert_eq!(states.selected, 0);
517 states.cancel_tab();
518 assert_eq!(states.selected, 2);
519 assert_eq!(states.is_tab_open(), false);
520 }
521
522 #[test]
523 fn test_components_select() {
524 let mut component = Select::default()
526 .foreground(Color::Red)
527 .background(Color::Black)
528 .borders(Borders::default())
529 .highlighted_color(Color::Red)
530 .highlighted_str(">>")
531 .title("C'est oui ou bien c'est non?", Alignment::Center)
532 .choices(["Oui!", "Non", "Peut-ĂȘtre"])
533 .value(1)
534 .rewind(false);
535 assert_eq!(component.states.is_tab_open(), false);
536 component.states.open_tab();
537 assert_eq!(component.states.is_tab_open(), true);
538 component.states.close_tab();
539 assert_eq!(component.states.is_tab_open(), false);
540 component.attr(
542 Attribute::Value,
543 AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
544 );
545 assert_eq!(component.state(), State::One(StateValue::Usize(2)));
547 component.states.open_tab();
549 assert_eq!(
552 component.perform(Cmd::Move(Direction::Up)),
553 CmdResult::Changed(State::One(StateValue::Usize(1))),
554 );
555 assert_eq!(
556 component.perform(Cmd::Move(Direction::Up)),
557 CmdResult::Changed(State::One(StateValue::Usize(0))),
558 );
559 assert_eq!(
561 component.perform(Cmd::Move(Direction::Up)),
562 CmdResult::Changed(State::One(StateValue::Usize(0))),
563 );
564 assert_eq!(
566 component.perform(Cmd::Move(Direction::Down)),
567 CmdResult::Changed(State::One(StateValue::Usize(1))),
568 );
569 assert_eq!(
570 component.perform(Cmd::Move(Direction::Down)),
571 CmdResult::Changed(State::One(StateValue::Usize(2))),
572 );
573 assert_eq!(
575 component.perform(Cmd::Move(Direction::Down)),
576 CmdResult::Changed(State::One(StateValue::Usize(2))),
577 );
578 assert_eq!(
580 component.perform(Cmd::Submit),
581 CmdResult::Submit(State::One(StateValue::Usize(2))),
582 );
583 assert_eq!(component.states.is_tab_open(), false);
585 assert_eq!(component.perform(Cmd::Submit), CmdResult::None);
587 assert_eq!(component.states.is_tab_open(), true);
588 assert_eq!(
590 component.perform(Cmd::Submit),
591 CmdResult::Submit(State::One(StateValue::Usize(2))),
592 );
593 assert_eq!(component.states.is_tab_open(), false);
594 assert_eq!(
595 component.perform(Cmd::Move(Direction::Down)),
596 CmdResult::None
597 );
598 assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
599 }
600
601 #[test]
602 fn various_set_choice_types() {
603 SelectStates::default().set_choices(&["hello".to_string()]);
605 SelectStates::default().set_choices(vec!["hello".to_string()]);
607 SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
609 }
610
611 #[test]
612 fn various_choice_types() {
613 let _ = Select::default().choices(["hello"]);
615 let _ = Select::default().choices(["hello".to_string()]);
617 let _ = Select::default().choices(vec!["hello"]);
619 let _ = Select::default().choices(vec!["hello".to_string()]);
621 let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
623 let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
625 }
626}