tui_realm_stdlib/components/
radio.rs

1//! ## Radio
2//!
3//! `Radio` component renders a radio group
4
5/**
6 * MIT License
7 *
8 * termscp - Copyright (c) 2021 Christian Visintin
9 *
10 * Permission is hereby granted, free of charge, to any person obtaining a copy
11 * of this software and associated documentation files (the "Software"), to deal
12 * in the Software without restriction, including without limitation the rights
13 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14 * copies of the Software, and to permit persons to whom the Software is
15 * furnished to do so, subject to the following conditions:
16 *
17 * The above copyright notice and this permission notice shall be included in all
18 * copies or substantial portions of the Software.
19 *
20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26 * SOFTWARE.
27 */
28use tuirealm::command::{Cmd, CmdResult, Direction};
29use tuirealm::props::{
30    Alignment, AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, Style,
31    TextModifiers,
32};
33use tuirealm::ratatui::text::Line as Spans;
34use tuirealm::ratatui::{layout::Rect, widgets::Tabs};
35use tuirealm::{Frame, MockComponent, State, StateValue};
36
37// -- states
38
39/// ## RadioStates
40///
41/// RadioStates contains states for this component
42#[derive(Default)]
43pub struct RadioStates {
44    pub choice: usize,        // Selected option
45    pub choices: Vec<String>, // Available choices
46}
47
48impl RadioStates {
49    /// ### next_choice
50    ///
51    /// Move choice index to next choice
52    pub fn next_choice(&mut self, rewind: bool) {
53        if rewind && self.choice + 1 >= self.choices.len() {
54            self.choice = 0;
55        } else if self.choice + 1 < self.choices.len() {
56            self.choice += 1;
57        }
58    }
59
60    /// ### prev_choice
61    ///
62    /// Move choice index to previous choice
63    pub fn prev_choice(&mut self, rewind: bool) {
64        if rewind && self.choice == 0 && !self.choices.is_empty() {
65            self.choice = self.choices.len() - 1;
66        } else if self.choice > 0 {
67            self.choice -= 1;
68        }
69    }
70
71    /// ### set_choices
72    ///
73    /// Set RadioStates choices from a vector of text spans
74    /// In addition resets current selection and keep index if possible or set it to the first value
75    /// available
76    pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
77        self.choices = choices.into();
78        // Keep index if possible
79        if self.choice >= self.choices.len() {
80            self.choice = match self.choices.len() {
81                0 => 0,
82                l => l - 1,
83            };
84        }
85    }
86
87    pub fn select(&mut self, i: usize) {
88        if i < self.choices.len() {
89            self.choice = i;
90        }
91    }
92}
93
94// -- component
95
96/// ## Radio
97///
98/// Radio component represents a group of tabs to select from
99#[derive(Default)]
100#[must_use]
101pub struct Radio {
102    props: Props,
103    pub states: RadioStates,
104}
105
106impl Radio {
107    pub fn foreground(mut self, fg: Color) -> Self {
108        self.attr(Attribute::Foreground, AttrValue::Color(fg));
109        self
110    }
111
112    pub fn background(mut self, bg: Color) -> Self {
113        self.attr(Attribute::Background, AttrValue::Color(bg));
114        self
115    }
116
117    pub fn borders(mut self, b: Borders) -> Self {
118        self.attr(Attribute::Borders, AttrValue::Borders(b));
119        self
120    }
121
122    pub fn title<S: Into<String>>(mut self, t: S, a: Alignment) -> Self {
123        self.attr(Attribute::Title, AttrValue::Title((t.into(), a)));
124        self
125    }
126
127    pub fn inactive(mut self, s: Style) -> Self {
128        self.attr(Attribute::FocusStyle, AttrValue::Style(s));
129        self
130    }
131
132    pub fn rewind(mut self, r: bool) -> Self {
133        self.attr(Attribute::Rewind, AttrValue::Flag(r));
134        self
135    }
136
137    pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
138        self.attr(
139            Attribute::Content,
140            AttrValue::Payload(PropPayload::Vec(
141                choices
142                    .into_iter()
143                    .map(|v| PropValue::Str(v.into()))
144                    .collect(),
145            )),
146        );
147        self
148    }
149
150    pub fn value(mut self, i: usize) -> Self {
151        // Set state
152        self.attr(
153            Attribute::Value,
154            AttrValue::Payload(PropPayload::One(PropValue::Usize(i))),
155        );
156        self
157    }
158
159    fn is_rewind(&self) -> bool {
160        self.props
161            .get_or(Attribute::Rewind, AttrValue::Flag(false))
162            .unwrap_flag()
163    }
164}
165
166impl MockComponent for Radio {
167    fn view(&mut self, render: &mut Frame, area: Rect) {
168        if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
169            // Make choices
170            let choices: Vec<Spans> = self
171                .states
172                .choices
173                .iter()
174                .map(|x| Spans::from(x.as_str()))
175                .collect();
176            let foreground = self
177                .props
178                .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
179                .unwrap_color();
180            let background = self
181                .props
182                .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
183                .unwrap_color();
184            let borders = self
185                .props
186                .get_or(Attribute::Borders, AttrValue::Borders(Borders::default()))
187                .unwrap_borders();
188            let title = self
189                .props
190                .get_ref(Attribute::Title)
191                .and_then(|x| x.as_title());
192            let focus = self
193                .props
194                .get_or(Attribute::Focus, AttrValue::Flag(false))
195                .unwrap_flag();
196            let inactive_style = self
197                .props
198                .get(Attribute::FocusStyle)
199                .map(|x| x.unwrap_style());
200
201            let normal_style = Style::default().fg(foreground).bg(background);
202
203            let div = crate::utils::get_block(borders, title, focus, inactive_style);
204            let radio: Tabs = Tabs::new(choices)
205                .block(div)
206                .select(self.states.choice)
207                .style(normal_style)
208                .highlight_style(Style::default().fg(foreground).add_modifier(if focus {
209                    TextModifiers::REVERSED
210                } else {
211                    TextModifiers::empty()
212                }));
213            render.render_widget(radio, area);
214        }
215    }
216
217    fn query(&self, attr: Attribute) -> Option<AttrValue> {
218        self.props.get(attr)
219    }
220
221    fn attr(&mut self, attr: Attribute, value: AttrValue) {
222        match attr {
223            Attribute::Content => {
224                // Reset choices
225                let choices: Vec<String> = value
226                    .unwrap_payload()
227                    .unwrap_vec()
228                    .iter()
229                    .map(|x| x.clone().unwrap_str())
230                    .collect();
231                self.states.set_choices(choices);
232            }
233            Attribute::Value => {
234                self.states
235                    .select(value.unwrap_payload().unwrap_one().unwrap_usize());
236            }
237            attr => {
238                self.props.set(attr, value);
239            }
240        }
241    }
242
243    fn state(&self) -> State {
244        State::One(StateValue::Usize(self.states.choice))
245    }
246
247    fn perform(&mut self, cmd: Cmd) -> CmdResult {
248        match cmd {
249            Cmd::Move(Direction::Right) => {
250                // Increment choice
251                self.states.next_choice(self.is_rewind());
252                // Return CmdResult On Change
253                CmdResult::Changed(self.state())
254            }
255            Cmd::Move(Direction::Left) => {
256                // Decrement choice
257                self.states.prev_choice(self.is_rewind());
258                // Return CmdResult On Change
259                CmdResult::Changed(self.state())
260            }
261            Cmd::Submit => {
262                // Return Submit
263                CmdResult::Submit(self.state())
264            }
265            _ => CmdResult::None,
266        }
267    }
268}
269
270#[cfg(test)]
271mod test {
272
273    use super::*;
274
275    use pretty_assertions::assert_eq;
276    use tuirealm::props::{PropPayload, PropValue};
277
278    #[test]
279    fn test_components_radio_states() {
280        let mut states: RadioStates = RadioStates::default();
281        assert_eq!(states.choice, 0);
282        assert_eq!(states.choices.len(), 0);
283        let choices: &[String] = &[
284            "lemon".to_string(),
285            "strawberry".to_string(),
286            "vanilla".to_string(),
287            "chocolate".to_string(),
288        ];
289        states.set_choices(choices);
290        assert_eq!(states.choice, 0);
291        assert_eq!(states.choices.len(), 4);
292        // Move
293        states.prev_choice(false);
294        assert_eq!(states.choice, 0);
295        states.next_choice(false);
296        assert_eq!(states.choice, 1);
297        states.next_choice(false);
298        assert_eq!(states.choice, 2);
299        // Forward overflow
300        states.next_choice(false);
301        states.next_choice(false);
302        assert_eq!(states.choice, 3);
303        states.prev_choice(false);
304        assert_eq!(states.choice, 2);
305        // Update
306        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
307        states.set_choices(choices);
308        assert_eq!(states.choice, 1); // Move to first index available
309        assert_eq!(states.choices.len(), 2);
310        let choices: &[String] = &[];
311        states.set_choices(choices);
312        assert_eq!(states.choice, 0); // Move to first index available
313        assert_eq!(states.choices.len(), 0);
314        // Rewind
315        let choices: &[String] = &[
316            "lemon".to_string(),
317            "strawberry".to_string(),
318            "vanilla".to_string(),
319            "chocolate".to_string(),
320        ];
321        states.set_choices(choices);
322        assert_eq!(states.choice, 0);
323        states.prev_choice(true);
324        assert_eq!(states.choice, 3);
325        states.next_choice(true);
326        assert_eq!(states.choice, 0);
327        states.next_choice(true);
328        assert_eq!(states.choice, 1);
329        states.prev_choice(true);
330        assert_eq!(states.choice, 0);
331    }
332
333    #[test]
334    fn test_components_radio() {
335        // Make component
336        let mut component = Radio::default()
337            .background(Color::Blue)
338            .foreground(Color::Red)
339            .borders(Borders::default())
340            .title("C'est oui ou bien c'est non?", Alignment::Center)
341            .choices(["Oui!", "Non", "Peut-ĂȘtre"])
342            .value(1)
343            .rewind(false);
344        // Verify states
345        assert_eq!(component.states.choice, 1);
346        assert_eq!(component.states.choices.len(), 3);
347        component.attr(
348            Attribute::Value,
349            AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
350        );
351        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
352        // Get value
353        component.states.choice = 1;
354        assert_eq!(component.state(), State::One(StateValue::Usize(1)));
355        // Handle events
356        assert_eq!(
357            component.perform(Cmd::Move(Direction::Left)),
358            CmdResult::Changed(State::One(StateValue::Usize(0))),
359        );
360        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
361        // Left again
362        assert_eq!(
363            component.perform(Cmd::Move(Direction::Left)),
364            CmdResult::Changed(State::One(StateValue::Usize(0))),
365        );
366        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
367        // Right
368        assert_eq!(
369            component.perform(Cmd::Move(Direction::Right)),
370            CmdResult::Changed(State::One(StateValue::Usize(1))),
371        );
372        assert_eq!(component.state(), State::One(StateValue::Usize(1)));
373        // Right again
374        assert_eq!(
375            component.perform(Cmd::Move(Direction::Right)),
376            CmdResult::Changed(State::One(StateValue::Usize(2))),
377        );
378        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
379        // Right again
380        assert_eq!(
381            component.perform(Cmd::Move(Direction::Right)),
382            CmdResult::Changed(State::One(StateValue::Usize(2))),
383        );
384        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
385        // Submit
386        assert_eq!(
387            component.perform(Cmd::Submit),
388            CmdResult::Submit(State::One(StateValue::Usize(2))),
389        );
390    }
391
392    #[test]
393    fn various_set_choice_types() {
394        // static array of strings
395        RadioStates::default().set_choices(&["hello".to_string()]);
396        // vector of strings
397        RadioStates::default().set_choices(vec!["hello".to_string()]);
398        // boxed array of strings
399        RadioStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
400    }
401
402    #[test]
403    fn various_choice_types() {
404        // static array of static strings
405        let _ = Radio::default().choices(["hello"]);
406        // static array of strings
407        let _ = Radio::default().choices(["hello".to_string()]);
408        // vec of static strings
409        let _ = Radio::default().choices(vec!["hello"]);
410        // vec of strings
411        let _ = Radio::default().choices(vec!["hello".to_string()]);
412        // boxed array of static strings
413        let _ = Radio::default().choices(vec!["hello"].into_boxed_slice());
414        // boxed array of strings
415        let _ = Radio::default().choices(vec!["hello".to_string()].into_boxed_slice());
416    }
417}