tui_realm_stdlib/components/
radio.rs1use tuirealm::command::{Cmd, CmdResult, Direction};
27use tuirealm::component::Component;
28use tuirealm::props::{
29 AttrValue, Attribute, Borders, Color, PropPayload, PropValue, Props, QueryResult, Style,
30 TextModifiers, Title,
31};
32use tuirealm::ratatui::Frame;
33use tuirealm::ratatui::layout::Rect;
34use tuirealm::ratatui::text::Line;
35use tuirealm::ratatui::widgets::Tabs;
36use tuirealm::state::{State, StateValue};
37
38use crate::prop_ext::{CommonHighlight, CommonProps};
39
40#[derive(Default)]
44pub struct RadioStates {
45 pub choice: usize,
47 pub choices: Vec<String>,
49}
50
51impl RadioStates {
52 pub fn next_choice(&mut self, rewind: bool) {
54 if rewind && self.choice + 1 >= self.choices.len() {
55 self.choice = 0;
56 } else if self.choice + 1 < self.choices.len() {
57 self.choice += 1;
58 }
59 }
60
61 pub fn prev_choice(&mut self, rewind: bool) {
63 if rewind && self.choice == 0 && !self.choices.is_empty() {
64 self.choice = self.choices.len() - 1;
65 } else if self.choice > 0 {
66 self.choice -= 1;
67 }
68 }
69
70 pub fn set_choices(&mut self, choices: impl Into<Vec<String>>) {
75 self.choices = choices.into();
76 if self.choice >= self.choices.len() {
78 self.choice = match self.choices.len() {
79 0 => 0,
80 l => l - 1,
81 };
82 }
83 }
84
85 pub fn select(&mut self, i: usize) {
87 if i < self.choices.len() {
88 self.choice = i;
89 }
90 }
91}
92
93#[derive(Default)]
99#[must_use]
100pub struct Radio {
101 common: CommonProps,
102 common_hg: CommonHighlight,
103 props: Props,
104 pub states: RadioStates,
105}
106
107impl Radio {
108 pub fn foreground(mut self, fg: Color) -> Self {
110 self.attr(Attribute::Foreground, AttrValue::Color(fg));
111 self
112 }
113
114 pub fn background(mut self, bg: Color) -> Self {
116 self.attr(Attribute::Background, AttrValue::Color(bg));
117 self
118 }
119
120 pub fn modifiers(mut self, m: TextModifiers) -> Self {
122 self.attr(Attribute::TextProps, AttrValue::TextModifiers(m));
123 self
124 }
125
126 pub fn style(mut self, style: Style) -> Self {
130 self.attr(Attribute::Style, AttrValue::Style(style));
131 self
132 }
133
134 pub fn inactive(mut self, s: Style) -> Self {
136 self.attr(Attribute::UnfocusedBorderStyle, AttrValue::Style(s));
137 self
138 }
139
140 pub fn borders(mut self, b: Borders) -> Self {
142 self.attr(Attribute::Borders, AttrValue::Borders(b));
143 self
144 }
145
146 pub fn title<T: Into<Title>>(mut self, title: T) -> Self {
148 self.attr(Attribute::Title, AttrValue::Title(title.into()));
149 self
150 }
151
152 pub fn highlight_style(mut self, s: Style) -> Self {
156 self.attr(Attribute::HighlightStyle, AttrValue::Style(s));
157 self
158 }
159
160 pub fn highlight_style_inactive(mut self, s: Style) -> Self {
162 self.attr(Attribute::HighlightStyleUnfocused, AttrValue::Style(s));
163 self
164 }
165
166 pub fn rewind(mut self, r: bool) -> Self {
168 self.attr(Attribute::Rewind, AttrValue::Flag(r));
169 self
170 }
171
172 pub fn choices<S: Into<String>>(mut self, choices: impl IntoIterator<Item = S>) -> Self {
174 self.attr(
176 Attribute::Content,
177 AttrValue::Payload(PropPayload::Vec(
178 choices
179 .into_iter()
180 .map(|v| PropValue::Str(v.into()))
181 .collect(),
182 )),
183 );
184 self
185 }
186
187 pub fn value(mut self, i: usize) -> Self {
189 self.attr(
191 Attribute::Value,
192 AttrValue::Payload(PropPayload::Single(PropValue::Usize(i))),
193 );
194 self
195 }
196
197 pub fn always_active(mut self) -> Self {
199 self.attr(Attribute::AlwaysActive, AttrValue::Flag(true));
200 self
201 }
202
203 fn is_rewind(&self) -> bool {
204 self.props
205 .get(Attribute::Rewind)
206 .and_then(AttrValue::as_flag)
207 .unwrap_or_default()
208 }
209}
210
211impl Component for Radio {
212 fn view(&mut self, render: &mut Frame, area: Rect) {
213 if !self.common.display {
214 return;
215 }
216
217 let choices: Vec<Line> = self
219 .states
220 .choices
221 .iter()
222 .map(|x| Line::from(x.as_str()))
223 .collect();
224
225 let mut widget = Tabs::new(choices)
226 .select(self.states.choice)
227 .style(self.common.style)
228 .highlight_style(
229 self.common_hg
230 .get_style_focus(self.common.style, self.common.is_active()),
231 );
232
233 if let Some(block) = self.common.get_block() {
234 widget = widget.block(block);
235 }
236
237 render.render_widget(widget, area);
238 }
239
240 fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
241 if let Some(value) = self
242 .common
243 .get_for_query(attr)
244 .or_else(|| self.common_hg.get_for_query(attr))
245 {
246 return Some(value);
247 }
248
249 self.props.get_for_query(attr)
250 }
251
252 fn attr(&mut self, attr: Attribute, value: AttrValue) {
253 if let Some(value) = self
254 .common
255 .set(attr, value)
256 .and_then(|value| self.common_hg.set(attr, value))
257 {
258 match attr {
259 Attribute::Content => {
260 let choices: Vec<String> = value
262 .unwrap_payload()
263 .unwrap_vec()
264 .iter()
265 .map(|x| x.clone().unwrap_str())
266 .collect();
267 self.states.set_choices(choices);
268 }
269 Attribute::Value => {
270 self.states
271 .select(value.unwrap_payload().unwrap_single().unwrap_usize());
272 }
273 attr => {
274 self.props.set(attr, value);
275 }
276 }
277 }
278 }
279
280 fn state(&self) -> State {
281 State::Single(StateValue::Usize(self.states.choice))
282 }
283
284 fn perform(&mut self, cmd: Cmd) -> CmdResult {
285 match cmd {
286 Cmd::Move(Direction::Right) => {
287 self.states.next_choice(self.is_rewind());
289 CmdResult::Changed(self.state())
291 }
292 Cmd::Move(Direction::Left) => {
293 self.states.prev_choice(self.is_rewind());
295 CmdResult::Changed(self.state())
297 }
298 Cmd::Submit => {
299 CmdResult::Submit(self.state())
301 }
302 _ => CmdResult::Invalid(cmd),
303 }
304 }
305}
306
307#[cfg(test)]
308mod test {
309
310 use pretty_assertions::assert_eq;
311 use tuirealm::props::{HorizontalAlignment, PropPayload, PropValue};
312
313 use super::*;
314
315 #[test]
316 fn test_components_radio_states() {
317 let mut states: RadioStates = RadioStates::default();
318 assert_eq!(states.choice, 0);
319 assert_eq!(states.choices.len(), 0);
320 let choices: &[String] = &[
321 "lemon".to_string(),
322 "strawberry".to_string(),
323 "vanilla".to_string(),
324 "chocolate".to_string(),
325 ];
326 states.set_choices(choices);
327 assert_eq!(states.choice, 0);
328 assert_eq!(states.choices.len(), 4);
329 states.prev_choice(false);
331 assert_eq!(states.choice, 0);
332 states.next_choice(false);
333 assert_eq!(states.choice, 1);
334 states.next_choice(false);
335 assert_eq!(states.choice, 2);
336 states.next_choice(false);
338 states.next_choice(false);
339 assert_eq!(states.choice, 3);
340 states.prev_choice(false);
341 assert_eq!(states.choice, 2);
342 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
344 states.set_choices(choices);
345 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
347 let choices: &[String] = &[];
348 states.set_choices(choices);
349 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
351 let choices: &[String] = &[
353 "lemon".to_string(),
354 "strawberry".to_string(),
355 "vanilla".to_string(),
356 "chocolate".to_string(),
357 ];
358 states.set_choices(choices);
359 assert_eq!(states.choice, 0);
360 states.prev_choice(true);
361 assert_eq!(states.choice, 3);
362 states.next_choice(true);
363 assert_eq!(states.choice, 0);
364 states.next_choice(true);
365 assert_eq!(states.choice, 1);
366 states.prev_choice(true);
367 assert_eq!(states.choice, 0);
368 }
369
370 #[test]
371 fn test_components_radio() {
372 let mut component = Radio::default()
374 .background(Color::Blue)
375 .foreground(Color::Red)
376 .borders(Borders::default())
377 .title(
378 Title::from("C'est oui ou bien c'est non?").alignment(HorizontalAlignment::Center),
379 )
380 .choices(["Oui!", "Non", "Peut-ĂȘtre"])
381 .value(1)
382 .rewind(false);
383 assert_eq!(component.states.choice, 1);
385 assert_eq!(component.states.choices.len(), 3);
386 component.attr(
387 Attribute::Value,
388 AttrValue::Payload(PropPayload::Single(PropValue::Usize(2))),
389 );
390 assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
391 component.states.choice = 1;
393 assert_eq!(component.state(), State::Single(StateValue::Usize(1)));
394 assert_eq!(
396 component.perform(Cmd::Move(Direction::Left)),
397 CmdResult::Changed(State::Single(StateValue::Usize(0))),
398 );
399 assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
400 assert_eq!(
402 component.perform(Cmd::Move(Direction::Left)),
403 CmdResult::Changed(State::Single(StateValue::Usize(0))),
404 );
405 assert_eq!(component.state(), State::Single(StateValue::Usize(0)));
406 assert_eq!(
408 component.perform(Cmd::Move(Direction::Right)),
409 CmdResult::Changed(State::Single(StateValue::Usize(1))),
410 );
411 assert_eq!(component.state(), State::Single(StateValue::Usize(1)));
412 assert_eq!(
414 component.perform(Cmd::Move(Direction::Right)),
415 CmdResult::Changed(State::Single(StateValue::Usize(2))),
416 );
417 assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
418 assert_eq!(
420 component.perform(Cmd::Move(Direction::Right)),
421 CmdResult::Changed(State::Single(StateValue::Usize(2))),
422 );
423 assert_eq!(component.state(), State::Single(StateValue::Usize(2)));
424 assert_eq!(
426 component.perform(Cmd::Submit),
427 CmdResult::Submit(State::Single(StateValue::Usize(2))),
428 );
429 }
430
431 #[test]
432 fn various_set_choice_types() {
433 RadioStates::default().set_choices(&["hello".to_string()]);
435 RadioStates::default().set_choices(vec!["hello".to_string()]);
437 RadioStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
439 }
440
441 #[test]
442 fn various_choice_types() {
443 let _ = Radio::default().choices(["hello"]);
445 let _ = Radio::default().choices(["hello".to_string()]);
447 let _ = Radio::default().choices(vec!["hello"]);
449 let _ = Radio::default().choices(vec!["hello".to_string()]);
451 let _ = Radio::default().choices(vec!["hello"].into_boxed_slice());
453 let _ = Radio::default().choices(vec!["hello".to_string()].into_boxed_slice());
455 }
456}