1use std::hash::{Hash, Hasher};
4use std::sync::Arc;
5
6use rustc_hash::FxHasher;
7
8use crate::callback::{Callback, KeyHandler};
9use crate::core::element::{Element, IntoElement};
10use crate::core::event::{KeyCode, KeyEvent, MouseEvent};
11use crate::style::{Length, Padding, Style, StyleSlot};
12use crate::widgets::{Checkbox, CheckboxEvent, CheckboxVariant, HStack, VStack};
13
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
16pub enum RadioLayout {
17 #[default]
19 Vertical,
20 Horizontal,
22}
23
24#[derive(Clone)]
32pub struct Radio {
33 options: Vec<Arc<str>>,
34 selected: Option<usize>,
35 on_change: Option<Callback<usize>>,
36 disabled: bool,
37 gap: u16,
38 layout: RadioLayout,
39 variant: CheckboxVariant,
40 style: Style,
41 hover_style: StyleSlot,
42 focus_style: StyleSlot,
43 checked_style: Style,
44 unchecked_style: Style,
45 label_style: Style,
46 padding: Padding,
47 width: Length,
48 height: Length,
49 disabled_style: Style,
50 focusable: bool,
51 tab_stop: bool,
52 focus_key: Option<Arc<str>>,
53 on_focus: Option<Callback<usize>>,
54 on_blur: Option<Callback<usize>>,
55 on_key: Option<KeyHandler>,
56}
57
58fn derived_focus_key(options: &[Arc<str>]) -> Arc<str> {
59 let mut hasher = FxHasher::default();
60 options.len().hash(&mut hasher);
61 for option in options {
62 option.hash(&mut hasher);
63 }
64 Arc::from(format!("__tui_lipan_radio:{:016x}", hasher.finish()))
65}
66
67impl Radio {
68 pub fn new(options: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Self {
70 Self {
71 options: options.into_iter().map(Into::into).collect(),
72 selected: None,
73 on_change: None,
74 disabled: false,
75 gap: 0,
76 layout: RadioLayout::Vertical,
77 variant: CheckboxVariant::Circle,
78 style: Style::default(),
79 hover_style: StyleSlot::Inherit,
80 focus_style: StyleSlot::Inherit,
81 checked_style: Style::default(),
82 unchecked_style: Style::default(),
83 label_style: Style::default(),
84 padding: Padding::default(),
85 width: Length::Auto,
86 height: Length::Auto,
87 disabled_style: Style::default(),
88 focusable: true,
89 tab_stop: true,
90 focus_key: None,
91 on_focus: None,
92 on_blur: None,
93 on_key: None,
94 }
95 }
96
97 pub fn selected(mut self, selected: Option<usize>) -> Self {
99 self.selected = selected;
100 self
101 }
102
103 pub fn on_change(mut self, cb: Callback<usize>) -> Self {
105 self.on_change = Some(cb);
106 self
107 }
108
109 pub fn disabled(mut self, disabled: bool) -> Self {
111 self.disabled = disabled;
112 self
113 }
114
115 pub fn gap(mut self, gap: u16) -> Self {
117 self.gap = gap;
118 self
119 }
120
121 pub fn layout(mut self, layout: RadioLayout) -> Self {
123 self.layout = layout;
124 self
125 }
126
127 pub fn variant(mut self, variant: CheckboxVariant) -> Self {
129 self.variant = variant;
130 self
131 }
132
133 pub fn style(mut self, style: Style) -> Self {
135 self.style = style;
136 self
137 }
138
139 pub fn hover_style(mut self, style: Style) -> Self {
141 self.hover_style = StyleSlot::Replace(style);
142 self
143 }
144
145 pub fn extend_hover_style(mut self, style: Style) -> Self {
147 self.hover_style = StyleSlot::Extend(style);
148 self
149 }
150
151 pub fn inherit_hover_style(mut self) -> Self {
153 self.hover_style = StyleSlot::Inherit;
154 self
155 }
156
157 pub fn hover_style_slot(mut self, slot: StyleSlot) -> Self {
159 self.hover_style = slot;
160 self
161 }
162
163 pub fn focus_style(mut self, style: Style) -> Self {
165 self.focus_style = StyleSlot::Replace(style);
166 self
167 }
168
169 pub fn extend_focus_style(mut self, style: Style) -> Self {
171 self.focus_style = StyleSlot::Extend(style);
172 self
173 }
174
175 pub fn inherit_focus_style(mut self) -> Self {
177 self.focus_style = StyleSlot::Inherit;
178 self
179 }
180
181 pub fn focus_style_slot(mut self, slot: StyleSlot) -> Self {
183 self.focus_style = slot;
184 self
185 }
186
187 pub fn checked_style(mut self, style: Style) -> Self {
189 self.checked_style = style;
190 self
191 }
192
193 pub fn unchecked_style(mut self, style: Style) -> Self {
195 self.unchecked_style = style;
196 self
197 }
198
199 pub fn label_style(mut self, style: Style) -> Self {
201 self.label_style = style;
202 self
203 }
204
205 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
207 self.padding = padding.into();
208 self
209 }
210
211 pub fn width(mut self, width: Length) -> Self {
213 self.width = width;
214 self
215 }
216
217 pub fn height(mut self, height: Length) -> Self {
219 self.height = height;
220 self
221 }
222
223 pub fn disabled_style(mut self, style: Style) -> Self {
225 self.disabled_style = style;
226 self
227 }
228
229 pub fn focusable(mut self, focusable: bool) -> Self {
231 self.focusable = focusable;
232 self
233 }
234
235 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
239 self.tab_stop = tab_stop;
240 self
241 }
242
243 pub fn focus_key(mut self, key: impl Into<Arc<str>>) -> Self {
249 self.focus_key = Some(key.into());
250 self
251 }
252
253 pub fn on_focus(mut self, cb: Callback<usize>) -> Self {
255 self.on_focus = Some(cb);
256 self
257 }
258
259 pub fn on_blur(mut self, cb: Callback<usize>) -> Self {
261 self.on_blur = Some(cb);
262 self
263 }
264
265 pub fn on_key(mut self, handler: KeyHandler) -> Self {
267 self.on_key = Some(handler);
268 self
269 }
270}
271
272impl From<Radio> for Element {
273 fn from(radio: Radio) -> Self {
274 let len = radio.options.len();
275 let active = radio
276 .selected
277 .filter(|&i| i < len)
278 .or_else(|| (len > 0).then_some(0));
279 let focus_key = radio
280 .focus_key
281 .clone()
282 .unwrap_or_else(|| derived_focus_key(&radio.options));
283
284 let items: Vec<Element> = radio
285 .options
286 .into_iter()
287 .enumerate()
288 .map(|(i, option)| {
289 let is_selected = radio.selected == Some(i);
290 let is_active = active == Some(i);
291 let on_change = radio.on_change.clone();
292
293 let mut checkbox = Checkbox::new(is_selected)
294 .label(option)
295 .variant(radio.variant)
296 .gap(1)
297 .style(radio.style)
298 .hover_style_slot(radio.hover_style)
299 .focus_style_slot(radio.focus_style)
300 .checked_style(radio.checked_style)
301 .unchecked_style(radio.unchecked_style)
302 .label_style(radio.label_style)
303 .padding(radio.padding)
304 .width(radio.width)
305 .height(radio.height)
306 .disabled(radio.disabled)
307 .disabled_style(radio.disabled_style)
308 .focusable(radio.focusable && is_active && !radio.disabled)
309 .tab_stop(radio.tab_stop && is_active && !radio.disabled);
310
311 if is_active {
312 if let Some(cb) = radio.on_focus.clone() {
313 checkbox = checkbox.on_focus(Callback::new(move |_| cb.emit(i)));
314 }
315 if let Some(cb) = radio.on_blur.clone() {
316 checkbox = checkbox.on_blur(Callback::new(move |_| cb.emit(i)));
317 }
318 }
319
320 if is_active && !radio.disabled {
321 let change_cb = radio.on_change.clone();
322 let caller_on_key = radio.on_key.clone();
323 let layout = radio.layout;
324 checkbox = checkbox.on_key(KeyHandler::new(move |key: KeyEvent| {
325 if caller_on_key
326 .as_ref()
327 .is_some_and(|handler| handler.handle(key))
328 {
329 return true;
330 }
331 handle_radio_key(key, i, len, layout, &change_cb)
332 }));
333 }
334
335 if let Some(cb) = on_change
336 && !radio.disabled
337 {
338 let cb_toggle = cb.clone();
339 checkbox = checkbox.on_toggle(Callback::new(move |ev: CheckboxEvent| {
340 if ev.state.is_checked() {
341 cb_toggle.emit(i);
342 }
343 }));
344
345 checkbox = checkbox.on_click(Callback::new(move |_: MouseEvent| {
346 cb.emit(i);
347 }));
348 }
349
350 if is_active {
351 checkbox.key(focus_key.clone())
352 } else {
353 checkbox.into()
354 }
355 })
356 .collect();
357
358 match radio.layout {
359 RadioLayout::Vertical => {
360 let mut stack = VStack::new().gap(radio.gap);
361 for item in items {
362 stack = stack.child(item);
363 }
364 stack.into()
365 }
366 RadioLayout::Horizontal => {
367 let mut stack = HStack::new().gap(radio.gap);
368 for item in items {
369 stack = stack.child(item);
370 }
371 stack.into()
372 }
373 }
374 }
375}
376
377fn handle_radio_key(
378 key: KeyEvent,
379 current: usize,
380 len: usize,
381 layout: RadioLayout,
382 on_change: &Option<Callback<usize>>,
383) -> bool {
384 if len == 0 || key.mods.ctrl || key.mods.alt || key.mods.super_key {
385 return false;
386 }
387
388 let next = match (layout, key.code) {
389 (RadioLayout::Vertical, KeyCode::Up | KeyCode::Left)
390 | (RadioLayout::Horizontal, KeyCode::Left | KeyCode::Up) => {
391 Some(current.checked_sub(1).unwrap_or(len - 1))
392 }
393 (RadioLayout::Vertical, KeyCode::Down | KeyCode::Right)
394 | (RadioLayout::Horizontal, KeyCode::Right | KeyCode::Down) => Some((current + 1) % len),
395 (_, KeyCode::Home) => Some(0),
396 (_, KeyCode::End) => Some(len - 1),
397 _ => None,
398 };
399
400 let Some(next) = next else {
401 return false;
402 };
403
404 if next != current
405 && let Some(cb) = on_change
406 {
407 cb.emit(next);
408 }
409 true
410}