tui/components/
radio_select.rs1use crossterm::event::KeyCode;
2
3use super::select_option::SelectOption;
4use crate::components::{Component, Event, ViewContext};
5use crate::line::Line;
6use crate::rendering::frame::Frame;
7use crate::style::Style;
8
9pub struct RadioSelect {
11 pub options: Vec<SelectOption>,
12 pub selected: usize,
13}
14
15impl RadioSelect {
16 pub fn new(options: Vec<SelectOption>, selected: usize) -> Self {
17 Self { options, selected }
18 }
19
20 pub fn to_json(&self) -> serde_json::Value {
21 self.options
22 .get(self.selected)
23 .map_or(serde_json::Value::Null, |o| {
24 serde_json::Value::String(o.value.clone())
25 })
26 }
27
28 fn render_inline(&self, context: &ViewContext) -> Line {
29 if let Some(opt) = self.options.get(self.selected) {
30 Line::styled(&opt.title, context.theme.info())
31 } else {
32 Line::default()
33 }
34 }
35
36 fn render_options(&self, context: &ViewContext) -> Vec<Line> {
37 self.options
38 .iter()
39 .enumerate()
40 .map(|(j, opt)| {
41 let marker = if j == self.selected { "● " } else { "○ " };
42 let style = if j == self.selected {
43 Style::fg(context.theme.primary())
44 } else {
45 Style::default()
46 };
47 Line::with_style(format!("{marker}{}", opt.title), style)
48 })
49 .collect()
50 }
51}
52
53impl Component for RadioSelect {
54 type Message = ();
55
56 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
57 let Event::Key(key) = event else {
58 return None;
59 };
60 if self.options.is_empty() {
61 return None;
62 }
63
64 match key.code {
65 KeyCode::Up => {
66 self.selected = (self.selected + self.options.len() - 1) % self.options.len();
67 Some(vec![])
68 }
69 KeyCode::Down => {
70 self.selected = (self.selected + 1) % self.options.len();
71 Some(vec![])
72 }
73 _ => None,
74 }
75 }
76
77 fn render(&mut self, context: &ViewContext) -> Frame {
78 Frame::new(self.render_field(context, true))
79 }
80}
81
82impl RadioSelect {
83 pub fn render_field(&self, context: &ViewContext, focused: bool) -> Vec<Line> {
84 if focused {
85 self.render_options(context)
86 } else {
87 vec![self.render_inline(context)]
88 }
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use crossterm::event::{KeyEvent, KeyModifiers};
96
97 fn key(code: KeyCode) -> KeyEvent {
98 KeyEvent::new(code, KeyModifiers::NONE)
99 }
100
101 fn sample_options() -> Vec<SelectOption> {
102 vec![
103 SelectOption {
104 value: "a".into(),
105 title: "Alpha".into(),
106 description: None,
107 },
108 SelectOption {
109 value: "b".into(),
110 title: "Beta".into(),
111 description: None,
112 },
113 SelectOption {
114 value: "c".into(),
115 title: "Gamma".into(),
116 description: None,
117 },
118 ]
119 }
120
121 #[tokio::test]
122 async fn down_cycles_forward() {
123 let mut rs = RadioSelect::new(sample_options(), 0);
124 rs.on_event(&Event::Key(key(KeyCode::Down))).await;
125 assert_eq!(rs.selected, 1);
126 rs.on_event(&Event::Key(key(KeyCode::Down))).await;
127 assert_eq!(rs.selected, 2);
128 rs.on_event(&Event::Key(key(KeyCode::Down))).await;
129 assert_eq!(rs.selected, 0); }
131
132 #[tokio::test]
133 async fn up_cycles_backward() {
134 let mut rs = RadioSelect::new(sample_options(), 0);
135 rs.on_event(&Event::Key(key(KeyCode::Up))).await;
136 assert_eq!(rs.selected, 2); }
138
139 #[tokio::test]
140 async fn left_right_ignored() {
141 let mut rs = RadioSelect::new(sample_options(), 0);
142 let outcome = rs.on_event(&Event::Key(key(KeyCode::Right))).await;
143 assert!(outcome.is_none());
144 assert_eq!(rs.selected, 0);
145 let outcome = rs.on_event(&Event::Key(key(KeyCode::Left))).await;
146 assert!(outcome.is_none());
147 assert_eq!(rs.selected, 0);
148 }
149
150 #[test]
151 fn to_json_returns_selected_value() {
152 let rs = RadioSelect::new(sample_options(), 1);
153 assert_eq!(rs.to_json(), serde_json::json!("b"));
154 }
155
156 #[test]
157 fn to_json_empty_options_returns_null() {
158 let rs = RadioSelect::new(vec![], 0);
159 assert_eq!(rs.to_json(), serde_json::Value::Null);
160 }
161
162 #[tokio::test]
163 async fn empty_options_ignores_keys() {
164 let mut rs = RadioSelect::new(vec![], 0);
165 let outcome = rs.on_event(&Event::Key(key(KeyCode::Right))).await;
166 assert!(outcome.is_none());
167 }
168}