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 let div = crate::utils::get_block(borders, title, focus, inactive_style);
201 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 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 self.states.next_choice(self.is_rewind());
256 CmdResult::Changed(self.state())
258 }
259 Cmd::Move(Direction::Left) => {
260 self.states.prev_choice(self.is_rewind());
262 CmdResult::Changed(self.state())
264 }
265 Cmd::Submit => {
266 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 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 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 let choices: &[String] = &["lemon".to_string(), "strawberry".to_string()];
311 states.set_choices(choices);
312 assert_eq!(states.choice, 1); assert_eq!(states.choices.len(), 2);
314 let choices: &[String] = &[];
315 states.set_choices(choices);
316 assert_eq!(states.choice, 0); assert_eq!(states.choices.len(), 0);
318 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 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 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 component.states.choice = 1;
358 assert_eq!(component.state(), State::One(StateValue::Usize(1)));
359 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 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 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 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 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 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 RadioStates::default().set_choices(&["hello".to_string()]);
400 RadioStates::default().set_choices(vec!["hello".to_string()]);
402 RadioStates::default().set_choices(vec!["hello".to_string()].into_boxed_slice());
404 }
405
406 #[test]
407 fn various_choice_types() {
408 let _ = Radio::default().choices(["hello"]);
410 let _ = Radio::default().choices(["hello".to_string()]);
412 let _ = Radio::default().choices(vec!["hello"]);
414 let _ = Radio::default().choices(vec!["hello".to_string()]);
416 let _ = Radio::default().choices(vec!["hello"].into_boxed_slice());
418 let _ = Radio::default().choices(vec!["hello".to_string()].into_boxed_slice());
420 }
421}