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 choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
182 self.attr(
184 Attribute::Content,
185 AttrValue::Payload(PropPayload::Vec(
186 choices
187 .into_iter()
188 .map(|v| PropValue::Str(v.into()))
189 .collect(),
190 )),
191 );
192 self
193 }
194
195 pub fn value(mut self, i: usize) -> Self {
197 self.attr(
199 Attribute::Value,
200 AttrValue::Payload(PropPayload::Single(PropValue::Usize(i))),
201 );
202 self
203 }
204
205 fn render_selected_text(&self, render: &mut Frame, area: Rect) {
207 let selected_text = self
208 .states
209 .choices
210 .get(self.states.selected)
211 .map(String::as_str)
212 .unwrap_or_default();
213 let widget = Paragraph::new(selected_text).style(self.common.style);
214
215 render.render_widget(widget, area);
216 }
217
218 fn render_choices(&self, render: &mut Frame, area: Rect) {
220 let choices: Vec<ListItem> = self
222 .states
223 .choices
224 .iter()
225 .map(|x| ListItem::new(x.as_str()))
226 .collect();
227
228 let mut widget = List::new(choices)
231 .direction(ListDirection::TopToBottom)
232 .style(self.common.style);
233
234 if self.common.is_active() {
235 widget = widget.highlight_style(self.common_hg.get_style(self.common.style));
236 }
237
238 if let Some(symbol) = self.common_hg.get_symbol() {
239 widget = widget.highlight_symbol(symbol);
240 }
241
242 let mut state = ListState::default();
243 state.select(Some(self.states.selected));
244
245 render.render_stateful_widget(widget, area, &mut state);
246 }
247
248 #[inline]
250 fn rewindable(&self) -> bool {
251 self.props
252 .get(Attribute::Rewind)
253 .and_then(AttrValue::as_flag)
254 .unwrap_or_default()
255 }
256}
257
258impl Component for Select {
259 fn view(&mut self, render: &mut Frame, mut area: Rect) {
260 if !self.common.display {
261 return;
262 }
263
264 render.buffer_mut().set_style(area, self.common.style);
266
267 if let Some(block) = self.common.get_block() {
269 let inner = block.inner(area);
270 render.render_widget(block, area);
271 area = inner;
272 }
273
274 let [para_area, list_area] = if self.states.is_tab_open() {
276 Layout::default()
277 .direction(LayoutDirection::Vertical)
278 .margin(0)
279 .constraints([Constraint::Length(2), Constraint::Min(1)])
280 .areas(area)
281 } else {
282 [area, Rect::ZERO]
283 };
284
285 self.render_selected_text(render, para_area);
286
287 if !list_area.is_empty() {
288 self.render_choices(render, list_area);
289 }
290 }
291
292 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
293 if let Some(value) = self
294 .common
295 .get_for_query(attr)
296 .or_else(|| self.common_hg.get_for_query(attr))
297 {
298 return Some(value);
299 }
300
301 self.props.get_for_query(attr)
302 }
303
304 fn attr(&mut self, attr: Attribute, value: AttrValue) {
305 if let Some(value) = self
306 .common
307 .set(attr, value)
308 .and_then(|value| self.common_hg.set(attr, value))
309 {
310 match attr {
311 Attribute::Content => {
312 let choices: Vec<String> = value
314 .unwrap_payload()
315 .unwrap_vec()
316 .iter()
317 .map(|x| x.clone().unwrap_str())
318 .collect();
319 self.states.set_choices(choices);
320 }
321 Attribute::Value => {
322 self.states
323 .select(value.unwrap_payload().unwrap_single().unwrap_usize());
324 }
325 Attribute::Focus if self.states.is_tab_open() => {
326 if let AttrValue::Flag(false) = value {
327 self.states.cancel_tab();
328 }
329 self.props.set(attr, value);
330 }
331 attr => {
332 self.props.set(attr, value);
333 }
334 }
335 }
336 }
337
338 fn state(&self) -> State {
339 if self.states.is_tab_open() {
340 State::None
341 } else {
342 State::Single(StateValue::Usize(self.states.selected))
343 }
344 }
345
346 fn perform(&mut self, cmd: Cmd) -> CmdResult {
347 match cmd {
348 Cmd::Move(Direction::Down) => {
349 self.states.next_choice(self.rewindable());
351 if self.states.is_tab_open() {
353 CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
354 } else {
355 CmdResult::NoChange
356 }
357 }
358 Cmd::Move(Direction::Up) => {
359 self.states.prev_choice(self.rewindable());
361 if self.states.is_tab_open() {
363 CmdResult::Changed(State::Single(StateValue::Usize(self.states.selected)))
364 } else {
365 CmdResult::NoChange
366 }
367 }
368 Cmd::Cancel => {
369 self.states.cancel_tab();
370 CmdResult::Changed(self.state())
371 }
372 Cmd::Submit => {
373 if self.states.is_tab_open() {
375 self.states.close_tab();
376 CmdResult::Submit(self.state())
377 } else {
378 self.states.open_tab();
379 CmdResult::Visual
380 }
381 }
382 _ => CmdResult::Invalid(cmd),
383 }
384 }
385}
386
387#[cfg(test)]
388mod test {
389
390 use pretty_assertions::assert_eq;
391 use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
392
393 use super::*;
394
395 #[test]
396 fn test_components_select_states() {
397 let mut states: SelectStates = SelectStates::default();
398 assert_eq!(states.selected, 0);
399 assert_eq!(states.choices.len(), 0);
400 assert_eq!(states.tab_open, false);
401 let choices: &[String] = &[
402 "lemon".to_string(),
403 "strawberry".to_string(),
404 "vanilla".to_string(),
405 "chocolate".to_string(),
406 ];
407 states.set_choices(choices);
408 assert_eq!(states.selected, 0);
409 assert_eq!(states.choices.len(), 4);
410 states.prev_choice(false);
412 assert_eq!(states.selected, 0);
413 states.next_choice(false);
414 assert_eq!(states.selected, 0);
416 states.open_tab();
417 assert_eq!(states.is_tab_open(), true);
418 states.next_choice(false);
420 assert_eq!(states.selected, 1);
421 states.next_choice(false);
422 assert_eq!(states.selected, 2);
423 states.next_choice(false);
425 states.next_choice(false);
426 assert_eq!(states.selected, 3);
427 states.prev_choice(false);
428 assert_eq!(states.selected, 2);
429 states.close_tab();
431 assert_eq!(states.is_tab_open(), false);
432 states.prev_choice(false);
433 assert_eq!(states.selected, 2);
434 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
436 states.set_choices(choices);
437 assert_eq!(states.selected, 1); assert_eq!(states.choices.len(), 2);
439 let choices = vec![];
440 states.set_choices(choices);
441 assert_eq!(states.selected, 0); assert_eq!(states.choices.len(), 0);
443 let choices: &[String] = &[
445 "lemon".to_string(),
446 "strawberry".to_string(),
447 "vanilla".to_string(),
448 "chocolate".to_string(),
449 ];
450 states.set_choices(choices);
451 states.open_tab();
452 assert_eq!(states.selected, 0);
453 states.prev_choice(true);
454 assert_eq!(states.selected, 3);
455 states.next_choice(true);
456 assert_eq!(states.selected, 0);
457 states.next_choice(true);
458 assert_eq!(states.selected, 1);
459 states.prev_choice(true);
460 assert_eq!(states.selected, 0);
461 states.close_tab();
463 states.select(2);
464 states.open_tab();
465 states.prev_choice(true);
466 states.prev_choice(true);
467 assert_eq!(states.selected, 0);
468 states.cancel_tab();
469 assert_eq!(states.selected, 2);
470 assert_eq!(states.is_tab_open(), false);
471 }
472
473 #[test]
474 fn test_components_select() {
475 let mut component = Select::default()
477 .foreground(Color::Red)
478 .background(Color::Black)
479 .borders(Borders::default())
480 .highlight_style(
481 Style::new()
482 .fg(Color::Red)
483 .add_modifier(TextModifiers::REVERSED),
484 )
485 .highlight_str(">>")
486 .title(
487 Title::from("C'est oui ou bien c'est non?").alignment(HorizontalAlignment::Center),
488 )
489 .choices(["Oui!", "Non", "Peut-ĂȘtre"])
490 .value(1)
491 .rewind(false);
492 assert_eq!(component.states.is_tab_open(), false);
493 component.states.open_tab();
494 assert_eq!(component.states.is_tab_open(), true);
495 component.states.close_tab();
496 assert_eq!(component.states.is_tab_open(), false);
497 component.attr(
499 Attribute::Value,
500 AttrValue::Payload(PropPayload::Single(PropValue::Usize(2))),
501 );
502 assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
504 component.states.open_tab();
506 assert_eq!(
509 component.perform(Cmd::Move(Direction::Up)),
510 CmdResult::Changed(State::Single(StateValue::Usize(1))),
511 );
512 assert_eq!(
513 component.perform(Cmd::Move(Direction::Up)),
514 CmdResult::Changed(State::Single(StateValue::Usize(0))),
515 );
516 assert_eq!(
518 component.perform(Cmd::Move(Direction::Up)),
519 CmdResult::Changed(State::Single(StateValue::Usize(0))),
520 );
521 assert_eq!(
523 component.perform(Cmd::Move(Direction::Down)),
524 CmdResult::Changed(State::Single(StateValue::Usize(1))),
525 );
526 assert_eq!(
527 component.perform(Cmd::Move(Direction::Down)),
528 CmdResult::Changed(State::Single(StateValue::Usize(2))),
529 );
530 assert_eq!(
532 component.perform(Cmd::Move(Direction::Down)),
533 CmdResult::Changed(State::Single(StateValue::Usize(2))),
534 );
535 assert_eq!(
537 component.perform(Cmd::Submit),
538 CmdResult::Submit(State::Single(StateValue::Usize(2))),
539 );
540 assert_eq!(component.states.is_tab_open(), false);
542 assert_eq!(component.perform(Cmd::Submit), CmdResult::Visual);
544 assert_eq!(component.states.is_tab_open(), true);
545 assert_eq!(
547 component.perform(Cmd::Submit),
548 CmdResult::Submit(State::Single(StateValue::Usize(2))),
549 );
550 assert_eq!(component.states.is_tab_open(), false);
551 assert_eq!(
552 component.perform(Cmd::Move(Direction::Down)),
553 CmdResult::NoChange
554 );
555 assert_eq!(
556 component.perform(Cmd::Move(Direction::Up)),
557 CmdResult::NoChange
558 );
559 }
560
561 #[test]
562 fn various_set_choice_types() {
563 SelectStates::default().set_choices(&["hello".to_string()]);
565 SelectStates::default().set_choices(vec!["hello".to_string()]);
567 SelectStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
569 }
570
571 #[test]
572 fn various_choice_types() {
573 let _ = Select::default().choices(["hello"]);
575 let _ = Select::default().choices(["hello".to_string()]);
577 let _ = Select::default().choices(vec!["hello"]);
579 let _ = Select::default().choices(vec!["hello".to_string()]);
581 let _ = Select::default().choices(vec!["hello"].into_boxed_slice());
583 let _ = Select::default().choices(vec!["hello".to_string()].into_boxed_slice());
585 }
586}