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::{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}
121
122impl Select {
123 pub fn foreground(mut self, fg: Color) -> Self {
124 self.attr(Attribute::Foreground, AttrValue::Color(fg));
125 self
126 }
127
128 pub fn background(mut self, bg: Color) -> Self {
129 self.attr(Attribute::Background, AttrValue::Color(bg));
130 self
131 }
132
133 pub fn borders(mut self, b: Borders) -> Self {
134 self.attr(Attribute::Borders, AttrValue::Borders(b));
135 self
136 }
137
138 pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
139 self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
140 self
141 }
142
143 pub fn highlighted_str<S: Into<String>>(mut self, s: S) -> Self {
144 self.attr(Attribute::HighlightedStr, AttrValue::String(s.into()));
145 self
146 }
147
148 pub fn highlighted_color(mut self, c: Color) -> Self {
149 self.attr(Attribute::HighlightedColor, AttrValue::Color(c));
150 self
151 }
152
153 pub fn inactive(mut self, s: Style) -> Self {
154 self.attr(Attribute::FocusStyle, AttrValue::Style(s));
155 self
156 }
157
158 pub fn rewind(mut self, r: bool) -> Self {
159 self.attr(Attribute::Rewind, AttrValue::Flag(r));
160 self
161 }
162
163 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
164 self.attr(
165 Attribute::Content,
166 AttrValue::Payload(PropPayload::Vec(
167 choices
168 .into_iter()
169 .map(|v| PropValue::Str(v.into()))
170 .collect(),
171 )),
172 );
173 self
174 }
175
176 pub fn value(mut self, i: usize) -> Self {
177 self.attr(
179 Attribute::Value,
180 AttrValue::Payload(PropPayload::One(PropValue::Usize(i))),
181 );
182 self
183 }
184
185 fn render_open_tab(&mut self, render: &mut Frame, area: Rect) {
189 let choices: Vec<ListItem> = self
191 .states
192 .choices
193 .iter()
194 .map(|x| ListItem::new(Spans::from(x.as_str())))
195 .collect();
196 let foreground = self
197 .props
198 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
199 .unwrap_color();
200 let background = self
201 .props
202 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
203 .unwrap_color();
204 let hg: Color = self
205 .props
206 .get_or(Attribute::HighlightedColor, AttrValue::Color(foreground))
207 .unwrap_color();
208 let chunks = Layout::default()
210 .direction(LayoutDirection::Vertical)
211 .margin(0)
212 .constraints([Constraint::Length(2), Constraint::Min(1)])
213 .split(area);
214 let selected_text: String = match self.states.choices.get(self.states.selected) {
216 None => String::default(),
217 Some(s) => s.clone(),
218 };
219 let focus = self
220 .props
221 .get_or(Attribute::Focus, AttrValue::Flag(false))
222 .unwrap_flag();
223 let inactive_style = self
224 .props
225 .get(Attribute::FocusStyle)
226 .map(|x| x.unwrap_style());
227
228 let normal_style = Style::default().bg(background).fg(foreground);
229
230 let borders = self
231 .props
232 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
233 .unwrap_borders();
234 let title = self
235 .props
236 .get_ref(Attribute::Title)
237 .and_then(|x| x.as_title());
238 let block_a = crate::utils::get_block(borders, title, focus, inactive_style)
239 .borders(BorderSides::LEFT | BorderSides::TOP | BorderSides::RIGHT);
240 let block_b = crate::utils::get_block::<&str>(borders, None, focus, inactive_style)
241 .borders(BorderSides::LEFT | BorderSides::BOTTOM | BorderSides::RIGHT);
242
243 let p: Paragraph = Paragraph::new(selected_text)
244 .style(normal_style)
245 .block(block_a);
246 render.render_widget(p, chunks[0]);
247
248 let mut list = List::new(choices)
251 .block(block_b)
252 .direction(tuirealm::ratatui::widgets::ListDirection::TopToBottom)
253 .style(normal_style)
254 .highlight_style(
255 Style::default()
256 .fg(hg)
257 .add_modifier(TextModifiers::REVERSED),
258 );
259 let hg_str = self
261 .props
262 .get_ref(Attribute::HighlightedStr)
263 .and_then(|x| x.as_string());
264 if let Some(hg_str) = hg_str {
265 list = list.highlight_symbol(hg_str);
266 }
267 let mut state: ListState = ListState::default();
268 state.select(Some(self.states.selected));
269 render.render_stateful_widget(list, chunks[1], &mut state);
270 }
271
272 fn render_closed_tab(&self, render: &mut Frame, area: Rect) {
276 let foreground = self
277 .props
278 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
279 .unwrap_color();
280 let background = self
281 .props
282 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
283 .unwrap_color();
284 let inactive_style = self
285 .props
286 .get(Attribute::FocusStyle)
287 .map(|x| x.unwrap_style());
288 let focus = self
289 .props
290 .get_or(Attribute::Focus, AttrValue::Flag(false))
291 .unwrap_flag();
292
293 let normal_style = Style::default().bg(background).fg(foreground);
294
295 let borders = self
296 .props
297 .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
298 .unwrap_borders();
299 let title = self
300 .props
301 .get_ref(Attribute::Title)
302 .and_then(|x| x.as_title());
303 let block = crate::utils::get_block(borders, title, focus, inactive_style);
304
305 let selected_text: String = match self.states.choices.get(self.states.selected) {
306 None => String::default(),
307 Some(s) => s.clone(),
308 };
309 let p: Paragraph = Paragraph::new(selected_text)
310 .style(normal_style)
311 .block(block);
312 render.render_widget(p, area);
313 }
314
315 fn rewindable(&self) -> bool {
316 self.props
317 .get_or(Attribute::Rewind, AttrValue::Flag(false))
318 .unwrap_flag()
319 }
320}
321
322impl MockComponent for Select {
323 fn view(&mut self, render: &mut Frame, area: Rect) {
324 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
325 if self.states.is_tab_open() {
326 self.render_open_tab(render, area);
327 } else {
328 self.render_closed_tab(render, area);
329 }
330 }
331 }
332
333 fn query(&self, attr: Attribute) -> Option<AttrValue> {
334 self.props.get(attr)
335 }
336
337 fn attr(&mut self, attr: Attribute, value: AttrValue) {
338 match attr {
339 Attribute::Content => {
340 let choices: Vec<String> = value
342 .unwrap_payload()
343 .unwrap_vec()
344 .iter()
345 .map(|x| x.clone().unwrap_str())
346 .collect();
347 self.states.set_choices(choices);
348 }
349 Attribute::Value => {
350 self.states
351 .select(value.unwrap_payload().unwrap_one().unwrap_usize());
352 }
353 Attribute::Focus if self.states.is_tab_open() => {
354 if let AttrValue::Flag(false) = value {
355 self.states.cancel_tab();
356 }
357 self.props.set(attr, value);
358 }
359 attr => {
360 self.props.set(attr, value);
361 }
362 }
363 }
364
365 fn state(&self) -> State {
366 if self.states.is_tab_open() {
367 State::None
368 } else {
369 State::One(StateValue::Usize(self.states.selected))
370 }
371 }
372
373 fn perform(&mut self, cmd: Cmd) -> CmdResult {
374 match cmd {
375 Cmd::Move(Direction::Down) => {
376 self.states.next_choice(self.rewindable());
378 if self.states.is_tab_open() {
380 CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
381 } else {
382 CmdResult::None
383 }
384 }
385 Cmd::Move(Direction::Up) => {
386 self.states.prev_choice(self.rewindable());
388 if self.states.is_tab_open() {
390 CmdResult::Changed(State::One(StateValue::Usize(self.states.selected)))
391 } else {
392 CmdResult::None
393 }
394 }
395 Cmd::Cancel => {
396 self.states.cancel_tab();
397 CmdResult::Changed(self.state())
398 }
399 Cmd::Submit => {
400 if self.states.is_tab_open() {
402 self.states.close_tab();
403 CmdResult::Submit(self.state())
404 } else {
405 self.states.open_tab();
406 CmdResult::None
407 }
408 }
409 _ => CmdResult::None,
410 }
411 }
412}
413
414#[cfg(test)]
415mod test {
416
417 use super::*;
418
419 use pretty_assertions::assert_eq;
420
421 use tuirealm::props::{PropPayload, PropValue};
422
423 #[test]
424 fn test_components_select_states() {
425 let mut states: SelectStates = SelectStates::default();
426 assert_eq!(states.selected, 0);
427 assert_eq!(states.choices.len(), 0);
428 assert_eq!(states.tab_open, false);
429 let choices: &[String] = &[
430 "lemon".to_string(),
431 "strawberry".to_string(),
432 "vanilla".to_string(),
433 "chocolate".to_string(),
434 ];
435 states.set_choices(choices);
436 assert_eq!(states.selected, 0);
437 assert_eq!(states.choices.len(), 4);
438 states.prev_choice(false);
440 assert_eq!(states.selected, 0);
441 states.next_choice(false);
442 assert_eq!(states.selected, 0);
444 states.open_tab();
445 assert_eq!(states.is_tab_open(), true);
446 states.next_choice(false);
448 assert_eq!(states.selected, 1);
449 states.next_choice(false);
450 assert_eq!(states.selected, 2);
451 states.next_choice(false);
453 states.next_choice(false);
454 assert_eq!(states.selected, 3);
455 states.prev_choice(false);
456 assert_eq!(states.selected, 2);
457 states.close_tab();
459 assert_eq!(states.is_tab_open(), false);
460 states.prev_choice(false);
461 assert_eq!(states.selected, 2);
462 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
464 states.set_choices(choices);
465 assert_eq!(states.selected, 1); assert_eq!(states.choices.len(), 2);
467 let choices = vec![];
468 states.set_choices(choices);
469 assert_eq!(states.selected, 0); assert_eq!(states.choices.len(), 0);
471 let choices: &[String] = &[
473 "lemon".to_string(),
474 "strawberry".to_string(),
475 "vanilla".to_string(),
476 "chocolate".to_string(),
477 ];
478 states.set_choices(choices);
479 states.open_tab();
480 assert_eq!(states.selected, 0);
481 states.prev_choice(true);
482 assert_eq!(states.selected, 3);
483 states.next_choice(true);
484 assert_eq!(states.selected, 0);
485 states.next_choice(true);
486 assert_eq!(states.selected, 1);
487 states.prev_choice(true);
488 assert_eq!(states.selected, 0);
489 states.close_tab();
491 states.select(2);
492 states.open_tab();
493 states.prev_choice(true);
494 states.prev_choice(true);
495 assert_eq!(states.selected, 0);
496 states.cancel_tab();
497 assert_eq!(states.selected, 2);
498 assert_eq!(states.is_tab_open(), false);
499 }
500
501 #[test]
502 fn test_components_select() {
503 let mut component = Select::default()
505 .foreground(Color::Red)
506 .background(Color::Black)
507 .borders(Borders::default())
508 .highlighted_color(Color::Red)
509 .highlighted_str(">>")
510 .title("C'est oui ou bien c'est non?", Alignment::Center)
511 .choices(["Oui!", "Non", "Peut-ĂȘtre"])
512 .value(1)
513 .rewind(false);
514 assert_eq!(component.states.is_tab_open(), false);
515 component.states.open_tab();
516 assert_eq!(component.states.is_tab_open(), true);
517 component.states.close_tab();
518 assert_eq!(component.states.is_tab_open(), false);
519 component.attr(
521 Attribute::Value,
522 AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
523 );
524 assert_eq!(component.state(), State::One(StateValue::Usize(2)));
526 component.states.open_tab();
528 assert_eq!(
531 component.perform(Cmd::Move(Direction::Up)),
532 CmdResult::Changed(State::One(StateValue::Usize(1))),
533 );
534 assert_eq!(
535 component.perform(Cmd::Move(Direction::Up)),
536 CmdResult::Changed(State::One(StateValue::Usize(0))),
537 );
538 assert_eq!(
540 component.perform(Cmd::Move(Direction::Up)),
541 CmdResult::Changed(State::One(StateValue::Usize(0))),
542 );
543 assert_eq!(
545 component.perform(Cmd::Move(Direction::Down)),
546 CmdResult::Changed(State::One(StateValue::Usize(1))),
547 );
548 assert_eq!(
549 component.perform(Cmd::Move(Direction::Down)),
550 CmdResult::Changed(State::One(StateValue::Usize(2))),
551 );
552 assert_eq!(
554 component.perform(Cmd::Move(Direction::Down)),
555 CmdResult::Changed(State::One(StateValue::Usize(2))),
556 );
557 assert_eq!(
559 component.perform(Cmd::Submit),
560 CmdResult::Submit(State::One(StateValue::Usize(2))),
561 );
562 assert_eq!(component.states.is_tab_open(), false);
564 assert_eq!(component.perform(Cmd::Submit), CmdResult::None);
566 assert_eq!(component.states.is_tab_open(), true);
567 assert_eq!(
569 component.perform(Cmd::Submit),
570 CmdResult::Submit(State::One(StateValue::Usize(2))),
571 );
572 assert_eq!(component.states.is_tab_open(), false);
573 assert_eq!(
574 component.perform(Cmd::Move(Direction::Down)),
575 CmdResult::None
576 );
577 assert_eq!(component.perform(Cmd::Move(Direction::Up)), CmdResult::None);
578 }
579
580 #[test]
581 fn various_set_choice_types() {
582 SelectStates::default().set_choices(&["hello".to_string()]);
584 SelectStates::default().set_choices(vec!["hello".to_string()]);
586 SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
588 }
589
590 #[test]
591 fn various_choice_types() {
592 let _ = Select::default().choices(["hello"]);
594 let _ = Select::default().choices(["hello".to_string()]);
596 let _ = Select::default().choices(vec!["hello"]);
598 let _ = Select::default().choices(vec!["hello".to_string()]);
600 let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
602 let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
604 }
605}