tui_realm_stdlib/components/
select.rs1use tuirealm::command::{Cmd, CmdResult, Direction};
5use tuirealm::component::Component;
6use tuirealm::props::{
7 AttrValue, Attribute, Borders, Color, LineStatic, PropPayload, PropValue, Props, QueryResult,
8 Style, TextModifiers, Title,
9};
10use tuirealm::ratatui::Frame;
11use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout, Rect};
12use tuirealm::ratatui::widgets::{List, ListDirection, ListItem, ListState, Paragraph};
13use tuirealm::state::{State, StateValue};
14
15use crate::prop_ext::{CommonHighlight, CommonProps};
16
17#[derive(Default)]
21pub struct SelectStates {
22 pub choices: Vec<String>,
24 pub selected: usize,
26 pub previously_selected: usize,
28 pub tab_open: bool,
29}
30
31impl SelectStates {
32 pub fn next_choice(&mut self, rewind: bool) {
34 if self.tab_open {
35 if rewind && self.selected + 1 >= self.choices.len() {
36 self.selected = 0;
37 } else if self.selected + 1 < self.choices.len() {
38 self.selected += 1;
39 }
40 }
41 }
42
43 pub fn prev_choice(&mut self, rewind: bool) {
45 if self.tab_open {
46 if rewind && self.selected == 0 && !self.choices.is_empty() {
47 self.selected = self.choices.len() - 1;
48 } else if self.selected > 0 {
49 self.selected -= 1;
50 }
51 }
52 }
53
54 pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
59 self.choices = choices.into();
60 if self.selected >= self.choices.len() {
62 self.selected = match self.choices.len() {
63 0 => 0,
64 l => l - 1,
65 };
66 }
67 }
68
69 pub fn select(&mut self, i: usize) {
70 if i < self.choices.len() {
71 self.selected = i;
72 }
73 }
74
75 pub fn close_tab(&mut self) {
77 self.tab_open = false;
78 }
79
80 pub fn open_tab(&mut self) {
82 self.previously_selected = self.selected;
83 self.tab_open = true;
84 }
85
86 pub fn cancel_tab(&mut self) {
88 self.close_tab();
89 self.selected = self.previously_selected;
90 }
91
92 #[must_use]
94 pub fn is_tab_open(&self) -> bool {
95 self.tab_open
96 }
97}
98
99#[derive(Default)]
107#[must_use]
108pub struct Select {
109 common: CommonProps,
110 common_hg: CommonHighlight,
111 props: Props,
112 pub states: SelectStates,
113}
114
115impl Select {
116 pub fn foreground(mut self, fg: Color) -> Self {
118 self.attr(Attribute::Foreground, AttrValue::Color(fg));
119 self
120 }
121
122 pub fn background(mut self, bg: Color) -> Self {
124 self.attr(Attribute::Background, AttrValue::Color(bg));
125 self
126 }
127
128 pub fn modifiers(mut self, m: TextModifiers) -> Self {
130 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
131 self
132 }
133
134 pub fn style(mut self, style: Style) -> Self {
138 self.attr(Attribute::Style, AttrValue::Style(style));
139 self
140 }
141
142 pub fn inactive(mut self, s: Style) -> Self {
144 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
145 self
146 }
147
148 pub fn borders(mut self, b: Borders) -> Self {
150 self.attr(Attribute::Borders, AttrValue::Borders(b));
151 self
152 }
153
154 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
156 self.attr(Attribute::Title, AttrValue::Title(title.into()));
157 self
158 }
159
160 pub fn rewind(mut self, r: bool) -> Self {
162 self.attr(Attribute::Rewind, AttrValue::Flag(r));
163 self
164 }
165
166 pub fn highlight_str<S: Into<LineStatic>>(mut self, s: S) -> Self {
168 self.attr(Attribute::HighlightedStr, AttrValue::TextLine(s.into()));
169 self
170 }
171
172 pub fn highlight_style(mut self, s: Style) -> Self {
176 self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
177 self
178 }
179
180 pub fn highlight_style_inactive(mut self, s: Style) -> Self {
182 self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
183 self
184 }
185
186 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
188 self.attr(
190 Attribute::Content,
191 AttrValue::Payload(PropPayload::Vec(
192 choices
193 .into_iter()
194 .map(|v| PropValue::Str(v.into()))
195 .collect(),
196 )),
197 );
198 self
199 }
200
201 pub fn value(mut self, i: usize) -> Self {
203 self.attr(
205 Attribute::Value,
206 AttrValue::Payload(PropPayload::Single(PropValue::Usize(i))),
207 );
208 self
209 }
210
211 fn render_selected_text(&self, render: &mut Frame, area: Rect) {
213 let selected_text = self
214 .states
215 .choices
216 .get(self.states.selected)
217 .map(String::as_str)
218 .unwrap_or_default();
219 let widget = Paragraph::new(selected_text).style(self.common.style);
220
221 render.render_widget(widget, area);
222 }
223
224 fn render_choices(&self, render: &mut Frame, area: Rect) {
226 let choices: Vec<ListItem> = self
228 .states
229 .choices
230 .iter()
231 .map(|x| ListItem::new(x.as_str()))
232 .collect();
233
234 let mut widget = List::new(choices)
237 .direction(ListDirection::TopToBottom)
238 .style(self.common.style)
239 .highlight_style(
240 self.common_hg
241 .get_style_focus(self.common.style, self.common.is_active()),
242 );
243
244 if let Some(symbol) = self.common_hg.get_symbol() {
245 widget = widget.highlight_symbol(symbol);
246 }
247
248 let mut state = ListState::default();
249 state.select(Some(self.states.selected));
250
251 render.render_stateful_widget(widget, area, &mut state);
252 }
253
254 #[inline]
256 fn rewindable(&self) -> bool {
257 self.props
258 .get(Attribute::Rewind)
259 .and_then(AttrValue::as_flag)
260 .unwrap_or_default()
261 }
262}
263
264impl Component for Select {
265 fn view(&mut self, render: &mut Frame, mut area: Rect) {
266 if !self.common.display {
267 return;
268 }
269
270 render.buffer_mut().set_style(area, self.common.style);
272
273 if let Some(block) = self.common.get_block() {
275 let inner = block.inner(area);
276 render.render_widget(block, area);
277 area = inner;
278 }
279
280 let [para_area, list_area] = if self.states.is_tab_open() {
282 Layout::default()
283 .direction(LayoutDirection::Vertical)
284 .margin(0)
285 .constraints([Constraint::Length(2), Constraint::Min(1)])
286 .areas(area)
287 } else {
288 [area, Rect::ZERO]
289 };
290
291 self.render_selected_text(render, para_area);
292
293 if !list_area.is_empty() {
294 self.render_choices(render, list_area);
295 }
296 }
297
298 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
299 if let Some(value) = self
300 .common
301 .get_for_query(attr)
302 .or_else(|| self.common_hg.get_for_query(attr))
303 {
304 return Some(value);
305 }
306
307 self.props.get_for_query(attr)
308 }
309
310 fn attr(&mut self, attr: Attribute, value: AttrValue) {
311 if let Some(value) = self
312 .common
313 .set(attr, value)
314 .and_then(|value| self.common_hg.set(attr, value))
315 {
316 match attr {
317 Attribute::Content => {
318 let choices: Vec<String> = value
320 .unwrap_payload()
321 .unwrap_vec()
322 .iter()
323 .map(|x| x.clone().unwrap_str())
324 .collect();
325 self.states.set_choices(choices);
326 }
327 Attribute::Value => {
328 self.states
329 .select(value.unwrap_payload().unwrap_single().unwrap_usize());
330 }
331 Attribute::Focus if self.states.is_tab_open() => {
332 if let AttrValue::Flag(false) = value {
333 self.states.cancel_tab();
334 }
335 self.props.set(attr, value);
336 }
337 attr => {
338 self.props.set(attr, value);
339 }
340 }
341 }
342 }
343
344 fn state(&self) -> State {
345 if self.states.is_tab_open() {
346 State::None
347 } else {
348 State::Single(StateValue::Usize(self.states.selected))
349 }
350 }
351
352 fn perform(&mut self, cmd: Cmd) -> CmdResult {
353 match cmd {
354 Cmd::Move(Direction::Down) => {
355 self.states.next_choice(self.rewindable());
357 if self.states.is_tab_open() {
359 CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
360 } else {
361 CmdResult::NoChange
362 }
363 }
364 Cmd::Move(Direction::Up) => {
365 self.states.prev_choice(self.rewindable());
367 if self.states.is_tab_open() {
369 CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
370 } else {
371 CmdResult::NoChange
372 }
373 }
374 Cmd::Cancel => {
375 self.states.cancel_tab();
376 CmdResult::Changed(self.state())
377 }
378 Cmd::Submit => {
379 if self.states.is_tab_open() {
381 self.states.close_tab();
382 CmdResult::Submit(self.state())
383 } else {
384 self.states.open_tab();
385 CmdResult::Visual
386 }
387 }
388 _ => CmdResult::Invalid(cmd),
389 }
390 }
391}
392
393#[cfg(test)]
394mod test {
395
396 use pretty_assertions::assert_eq;
397 use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
398
399 use super::*;
400
401 #[test]
402 fn test_components_select_states() {
403 let mut states: SelectStates = SelectStates::default();
404 assert_eq!(states.selected, 0);
405 assert_eq!(states.choices.len(), 0);
406 assert_eq!(states.tab_open, false);
407 let choices: &[String] = &[
408 "lemon".to_string(),
409 "strawberry".to_string(),
410 "vanilla".to_string(),
411 "chocolate".to_string(),
412 ];
413 states.set_choices(choices);
414 assert_eq!(states.selected, 0);
415 assert_eq!(states.choices.len(), 4);
416 states.prev_choice(false);
418 assert_eq!(states.selected, 0);
419 states.next_choice(false);
420 assert_eq!(states.selected, 0);
422 states.open_tab();
423 assert_eq!(states.is_tab_open(), true);
424 states.next_choice(false);
426 assert_eq!(states.selected, 1);
427 states.next_choice(false);
428 assert_eq!(states.selected, 2);
429 states.next_choice(false);
431 states.next_choice(false);
432 assert_eq!(states.selected, 3);
433 states.prev_choice(false);
434 assert_eq!(states.selected, 2);
435 states.close_tab();
437 assert_eq!(states.is_tab_open(), false);
438 states.prev_choice(false);
439 assert_eq!(states.selected, 2);
440 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
442 states.set_choices(choices);
443 assert_eq!(states.selected, 1); assert_eq!(states.choices.len(), 2);
445 let choices = vec![];
446 states.set_choices(choices);
447 assert_eq!(states.selected, 0); assert_eq!(states.choices.len(), 0);
449 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 states.open_tab();
458 assert_eq!(states.selected, 0);
459 states.prev_choice(true);
460 assert_eq!(states.selected, 3);
461 states.next_choice(true);
462 assert_eq!(states.selected, 0);
463 states.next_choice(true);
464 assert_eq!(states.selected, 1);
465 states.prev_choice(true);
466 assert_eq!(states.selected, 0);
467 states.close_tab();
469 states.select(2);
470 states.open_tab();
471 states.prev_choice(true);
472 states.prev_choice(true);
473 assert_eq!(states.selected, 0);
474 states.cancel_tab();
475 assert_eq!(states.selected, 2);
476 assert_eq!(states.is_tab_open(), false);
477 }
478
479 #[test]
480 fn test_components_select() {
481 let mut component = Select::default()
483 .foreground(Color::Red)
484 .background(Color::Black)
485 .borders(Borders::default())
486 .highlight_style(
487 Style::new()
488 .fg(Color::Red)
489 .add_modifier(TextModifiers::REVERSED),
490 )
491 .highlight_str(">>")
492 .title(
493 Title::from("C'est oui ou bien c'est non?").alignment(HorizontalAlignment::Center),
494 )
495 .choices(["Oui!", "Non", "Peut-ĂȘtre"])
496 .value(1)
497 .rewind(false);
498 assert_eq!(component.states.is_tab_open(), false);
499 component.states.open_tab();
500 assert_eq!(component.states.is_tab_open(), true);
501 component.states.close_tab();
502 assert_eq!(component.states.is_tab_open(), false);
503 component.attr(
505 Attribute::Value,
506 AttrValue::Payload(PropPayload::Single(PropValue::Usize(2))),
507 );
508 assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
510 component.states.open_tab();
512 assert_eq!(
515 component.perform(Cmd::Move(Direction::Up)),
516 CmdResult::Changed(State::Single(StateValue::Usize(1))),
517 );
518 assert_eq!(
519 component.perform(Cmd::Move(Direction::Up)),
520 CmdResult::Changed(State::Single(StateValue::Usize(0))),
521 );
522 assert_eq!(
524 component.perform(Cmd::Move(Direction::Up)),
525 CmdResult::Changed(State::Single(StateValue::Usize(0))),
526 );
527 assert_eq!(
529 component.perform(Cmd::Move(Direction::Down)),
530 CmdResult::Changed(State::Single(StateValue::Usize(1))),
531 );
532 assert_eq!(
533 component.perform(Cmd::Move(Direction::Down)),
534 CmdResult::Changed(State::Single(StateValue::Usize(2))),
535 );
536 assert_eq!(
538 component.perform(Cmd::Move(Direction::Down)),
539 CmdResult::Changed(State::Single(StateValue::Usize(2))),
540 );
541 assert_eq!(
543 component.perform(Cmd::Submit),
544 CmdResult::Submit(State::Single(StateValue::Usize(2))),
545 );
546 assert_eq!(component.states.is_tab_open(), false);
548 assert_eq!(component.perform(Cmd::Submit), CmdResult::Visual);
550 assert_eq!(component.states.is_tab_open(), true);
551 assert_eq!(
553 component.perform(Cmd::Submit),
554 CmdResult::Submit(State::Single(StateValue::Usize(2))),
555 );
556 assert_eq!(component.states.is_tab_open(), false);
557 assert_eq!(
558 component.perform(Cmd::Move(Direction::Down)),
559 CmdResult::NoChange
560 );
561 assert_eq!(
562 component.perform(Cmd::Move(Direction::Up)),
563 CmdResult::NoChange
564 );
565 }
566
567 #[test]
568 fn various_set_choice_types() {
569 SelectStates::default().set_choices(&["hello".to_string()]);
571 SelectStates::default().set_choices(vec!["hello".to_string()]);
573 SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
575 }
576
577 #[test]
578 fn various_choice_types() {
579 let _ = Select::default().choices(["hello"]);
581 let _ = Select::default().choices(["hello".to_string()]);
583 let _ = Select::default().choices(vec!["hello"]);
585 let _ = Select::default().choices(vec!["hello".to_string()]);
587 let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
589 let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
591 }
592}