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