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            let div = crate::utils::get_block(borders, title, focus, inactive_style);
201            // Make colors
202            let (fg, block_color): (Color, Color) = if focus {
203                (foreground, foreground)
204            } else {
205                (foreground, Color::Reset)
206            };
207            let modifiers = if focus {
208                TextModifiers::REVERSED
209            } else {
210                TextModifiers::empty()
211            };
212            let radio: Tabs = Tabs::new(choices)
213                .block(div)
214                .select(self.states.choice)
215                .style(Style::default().fg(block_color).bg(background))
216                .highlight_style(Style::default().fg(fg).add_modifier(modifiers));
217            render.render_widget(radio, area);
218        }
219    }
220
221    fn query(&self, attr: Attribute) -> Option<AttrValue> {
222        self.props.get(attr)
223    }
224
225    fn attr(&mut self, attr: Attribute, value: AttrValue) {
226        match attr {
227            Attribute::Content => {
228                // Reset choices
229                let choices: Vec<String> = value
230                    .unwrap_payload()
231                    .unwrap_vec()
232                    .iter()
233                    .map(|x| x.clone().unwrap_str())
234                    .collect();
235                self.states.set_choices(choices);
236            }
237            Attribute::Value => {
238                self.states
239                    .select(value.unwrap_payload().unwrap_one().unwrap_usize());
240            }
241            attr => {
242                self.props.set(attr, value);
243            }
244        }
245    }
246
247    fn state(&self) -> State {
248        State::One(StateValue::Usize(self.states.choice))
249    }
250
251    fn perform(&mut self, cmd: Cmd) -> CmdResult {
252        match cmd {
253            Cmd::Move(Direction::Right) => {
254                // Increment choice
255                self.states.next_choice(self.is_rewind());
256                // Return CmdResult On Change
257                CmdResult::Changed(self.state())
258            }
259            Cmd::Move(Direction::Left) => {
260                // Decrement choice
261                self.states.prev_choice(self.is_rewind());
262                // Return CmdResult On Change
263                CmdResult::Changed(self.state())
264            }
265            Cmd::Submit => {
266                // Return Submit
267                CmdResult::Submit(self.state())
268            }
269            _ => CmdResult::None,
270        }
271    }
272}
273
274#[cfg(test)]
275mod test {
276
277    use super::*;
278
279    use pretty_assertions::assert_eq;
280    use tuirealm::props::{PropPayload, PropValue};
281
282    #[test]
283    fn test_components_radio_states() {
284        let mut states: RadioStates = RadioStates::default();
285        assert_eq!(states.choice, 0);
286        assert_eq!(states.choices.len(), 0);
287        let choices: &[String] = &[
288            "lemon".to_string(),
289            "strawberry".to_string(),
290            "vanilla".to_string(),
291            "chocolate".to_string(),
292        ];
293        states.set_choices(choices);
294        assert_eq!(states.choice, 0);
295        assert_eq!(states.choices.len(), 4);
296        // Move
297        states.prev_choice(false);
298        assert_eq!(states.choice, 0);
299        states.next_choice(false);
300        assert_eq!(states.choice, 1);
301        states.next_choice(false);
302        assert_eq!(states.choice, 2);
303        // Forward overflow
304        states.next_choice(false);
305        states.next_choice(false);
306        assert_eq!(states.choice, 3);
307        states.prev_choice(false);
308        assert_eq!(states.choice, 2);
309        // Update
310        let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
311        states.set_choices(choices);
312        assert_eq!(states.choice, 1); // Move to first index available
313        assert_eq!(states.choices.len(), 2);
314        let choices: &[String] = &[];
315        states.set_choices(choices);
316        assert_eq!(states.choice, 0); // Move to first index available
317        assert_eq!(states.choices.len(), 0);
318        // Rewind
319        let choices: &[String] = &[
320            "lemon".to_string(),
321            "strawberry".to_string(),
322            "vanilla".to_string(),
323            "chocolate".to_string(),
324        ];
325        states.set_choices(choices);
326        assert_eq!(states.choice, 0);
327        states.prev_choice(true);
328        assert_eq!(states.choice, 3);
329        states.next_choice(true);
330        assert_eq!(states.choice, 0);
331        states.next_choice(true);
332        assert_eq!(states.choice, 1);
333        states.prev_choice(true);
334        assert_eq!(states.choice, 0);
335    }
336
337    #[test]
338    fn test_components_radio() {
339        // Make component
340        let mut component = Radio::default()
341            .background(Color::Blue)
342            .foreground(Color::Red)
343            .borders(Borders::default())
344            .title("C'est oui ou bien c'est non?", Alignment::Center)
345            .choices(["Oui!", "Non", "Peut-ĂȘtre"])
346            .value(1)
347            .rewind(false);
348        // Verify states
349        assert_eq!(component.states.choice, 1);
350        assert_eq!(component.states.choices.len(), 3);
351        component.attr(
352            Attribute::Value,
353            AttrValue::Payload(PropPayload::One(PropValue::Usize(2))),
354        );
355        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
356        // Get value
357        component.states.choice = 1;
358        assert_eq!(component.state(), State::One(StateValue::Usize(1)));
359        // Handle events
360        assert_eq!(
361            component.perform(Cmd::Move(Direction::Left)),
362            CmdResult::Changed(State::One(StateValue::Usize(0))),
363        );
364        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
365        // Left again
366        assert_eq!(
367            component.perform(Cmd::Move(Direction::Left)),
368            CmdResult::Changed(State::One(StateValue::Usize(0))),
369        );
370        assert_eq!(component.state(), State::One(StateValue::Usize(0)));
371        // Right
372        assert_eq!(
373            component.perform(Cmd::Move(Direction::Right)),
374            CmdResult::Changed(State::One(StateValue::Usize(1))),
375        );
376        assert_eq!(component.state(), State::One(StateValue::Usize(1)));
377        // Right again
378        assert_eq!(
379            component.perform(Cmd::Move(Direction::Right)),
380            CmdResult::Changed(State::One(StateValue::Usize(2))),
381        );
382        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
383        // Right again
384        assert_eq!(
385            component.perform(Cmd::Move(Direction::Right)),
386            CmdResult::Changed(State::One(StateValue::Usize(2))),
387        );
388        assert_eq!(component.state(), State::One(StateValue::Usize(2)));
389        // Submit
390        assert_eq!(
391            component.perform(Cmd::Submit),
392            CmdResult::Submit(State::One(StateValue::Usize(2))),
393        );
394    }
395
396    #[test]
397    fn various_set_choice_types() {
398        // static array of strings
399        RadioStates::default().set_choices(&["hello".to_string()]);
400        // vector of strings
401        RadioStates::default().set_choices(vec!["hello".to_string()]);
402        // boxed array of strings
403        RadioStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
404    }
405
406    #[test]
407    fn various_choice_types() {
408        // static array of static strings
409        let _ = Radio::default().choices(["hello"]);
410        // static array of strings
411        let _ = Radio::default().choices(["hello".to_string()]);
412        // vec of static strings
413        let _ = Radio::default().choices(vec!["hello"]);
414        // vec of strings
415        let _ = Radio::default().choices(vec!["hello".to_string()]);
416        // boxed array of static strings
417        let _ = Radio::default().choices(vec!["hello"].into_boxed_slice());
418        // boxed array of strings
419        let _ = Radio::default().choices(vec!["hello".to_string()].into_boxed_slice());
420    }
421}