tui_realm_stdlib/components/
radio.rs1use 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#[derive(Default)]
43pub struct RadioStates {
44 pub choice: usize, pub choices: Vec<String>, }
47
48impl RadioStates {
49 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 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 pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
77 self.choices = choices.into();
78 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#[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 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 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 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 self.states.next_choice(self.is_rewind());
252 CmdResult::Changed(self.state())
254 }
255 Cmd::Move(Direction::Left) => {
256 self.states.prev_choice(self.is_rewind());
258 CmdResult::Changed(self.state())
260 }
261 Cmd::Submit => {
262 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 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 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 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
307 states.set_choices(choices);
308 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
310 let choices: &[String] = &[];
311 states.set_choices(choices);
312 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
314 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 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 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 component.states.choice = 1;
354 assert_eq!(component.state(), State::One(StateValue::Usize(1)));
355 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 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 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 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 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 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 RadioStates::default().set_choices(&["hello".to_string()]);
396 RadioStates::default().set_choices(vec!["hello".to_string()]);
398 RadioStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
400 }
401
402 #[test]
403 fn various_choice_types() {
404 let _ = Radio::default().choices(["hello"]);
406 let _ = Radio::default().choices(["hello".to_string()]);
408 let _ = Radio::default().choices(vec!["hello"]);
410 let _ = Radio::default().choices(vec!["hello".to_string()]);
412 let _ = Radio::default().choices(vec!["hello"].into_boxed_slice());
414 let _ = Radio::default().choices(vec!["hello".to_string()].into_boxed_slice());
416 }
417}