use std::marker::PhantomData;
use ratatui::widgets::{List, ListItem};
use super::{Component, EventContext, RenderContext};
use crate::input::{Event, Key};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RadioGroupMessage {
Up,
Down,
Confirm,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RadioGroupOutput<T: Clone> {
Selected(T),
Confirmed(T),
SelectionChanged(usize),
}
#[derive(Clone, Debug)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct RadioGroupState<T: Clone> {
options: Vec<T>,
selected: Option<usize>,
}
impl<T: Clone + PartialEq> PartialEq for RadioGroupState<T> {
fn eq(&self, other: &Self) -> bool {
self.options == other.options && self.selected == other.selected
}
}
impl<T: Clone> Default for RadioGroupState<T> {
fn default() -> Self {
Self {
options: Vec::new(),
selected: None,
}
}
}
impl<T: Clone> RadioGroupState<T> {
pub fn new(options: Vec<T>) -> Self {
let selected = if options.is_empty() { None } else { Some(0) };
Self { options, selected }
}
pub fn with_selected(options: Vec<T>, selected: usize) -> Self {
let selected = if options.is_empty() {
None
} else {
Some(selected.min(options.len() - 1))
};
Self { options, selected }
}
pub fn options(&self) -> &[T] {
&self.options
}
pub fn set_options(&mut self, options: Vec<T>) {
self.options = options;
if self.options.is_empty() {
self.selected = None;
} else if let Some(idx) = self.selected {
if idx >= self.options.len() {
self.selected = Some(self.options.len() - 1);
}
}
}
pub fn selected_index(&self) -> Option<usize> {
self.selected
}
pub fn selected(&self) -> Option<usize> {
self.selected_index()
}
pub fn selected_item(&self) -> Option<&T> {
self.options.get(self.selected?)
}
pub fn set_selected(&mut self, index: Option<usize>) {
match index {
Some(i) if i < self.options.len() => self.selected = Some(i),
Some(_) => {} None => self.selected = None,
}
}
pub fn is_empty(&self) -> bool {
self.options.is_empty()
}
pub fn len(&self) -> usize {
self.options.len()
}
}
impl<T: Clone + std::fmt::Display + 'static> RadioGroupState<T> {
pub fn update(&mut self, msg: RadioGroupMessage) -> Option<RadioGroupOutput<T>> {
RadioGroup::<T>::update(self, msg)
}
}
pub struct RadioGroup<T: Clone>(PhantomData<T>);
impl<T: Clone + std::fmt::Display + 'static> Component for RadioGroup<T> {
type State = RadioGroupState<T>;
type Message = RadioGroupMessage;
type Output = RadioGroupOutput<T>;
fn init() -> Self::State {
RadioGroupState::default()
}
fn handle_event(
_state: &Self::State,
event: &Event,
ctx: &EventContext,
) -> Option<Self::Message> {
if !ctx.focused || ctx.disabled {
return None;
}
if let Some(key) = event.as_key() {
match key.code {
Key::Up | Key::Char('k') => Some(RadioGroupMessage::Up),
Key::Down | Key::Char('j') => Some(RadioGroupMessage::Down),
Key::Enter => Some(RadioGroupMessage::Confirm),
_ => None,
}
} else {
None
}
}
fn update(state: &mut Self::State, msg: Self::Message) -> Option<Self::Output> {
if state.options.is_empty() {
return None;
}
let selected = state.selected?;
match msg {
RadioGroupMessage::Up => {
if selected > 0 {
let new_index = selected - 1;
state.selected = Some(new_index);
Some(RadioGroupOutput::SelectionChanged(new_index))
} else {
None
}
}
RadioGroupMessage::Down => {
if selected < state.options.len() - 1 {
let new_index = selected + 1;
state.selected = Some(new_index);
Some(RadioGroupOutput::SelectionChanged(new_index))
} else {
None
}
}
RadioGroupMessage::Confirm => state
.options
.get(selected)
.cloned()
.map(RadioGroupOutput::Confirmed),
}
}
fn view(state: &Self::State, ctx: &mut RenderContext<'_, '_>) {
let items: Vec<ListItem> = state
.options
.iter()
.enumerate()
.map(|(i, option)| {
let is_selected = state.selected == Some(i);
let indicator = if is_selected { "(•)" } else { "( )" };
let text = format!("{} {}", indicator, option);
let style = if ctx.disabled {
ctx.theme.disabled_style()
} else if is_selected && ctx.focused {
ctx.theme.focused_style()
} else {
ctx.theme.normal_style()
};
ListItem::new(text).style(style)
})
.collect();
let list = List::new(items);
let mut ann = crate::annotation::Annotation::new(crate::annotation::WidgetType::RadioGroup)
.with_id("radio_group")
.with_focus(ctx.focused)
.with_disabled(ctx.disabled);
if let Some(idx) = state.selected {
ann = ann.with_selected(true).with_value(idx.to_string());
}
let annotated = crate::annotation::Annotate::new(list, ann);
ctx.frame.render_widget(annotated, ctx.area);
}
}
#[cfg(test)]
mod tests;