use crate::event::Key;
use crate::render::Cell;
use crate::style::Color;
use crate::utils::Selection;
use crate::widget::theme::{DISABLED_FG, LIGHT_GRAY};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RadioStyle {
#[default]
Parentheses,
Unicode,
Brackets,
Diamond,
}
impl RadioStyle {
fn chars(&self) -> (char, char) {
match self {
RadioStyle::Parentheses => ('●', ' '),
RadioStyle::Unicode => ('◉', '○'),
RadioStyle::Brackets => ('*', ' '),
RadioStyle::Diamond => ('◆', '◇'),
}
}
fn brackets(&self) -> (char, char) {
match self {
RadioStyle::Parentheses => ('(', ')'),
RadioStyle::Brackets => ('[', ']'),
_ => (' ', ' '),
}
}
fn has_brackets(&self) -> bool {
matches!(self, RadioStyle::Parentheses | RadioStyle::Brackets)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RadioLayout {
#[default]
Vertical,
Horizontal,
}
#[derive(Clone)]
pub struct RadioGroup {
options: Vec<String>,
selection: Selection,
focused: bool,
disabled: bool,
style: RadioStyle,
layout: RadioLayout,
gap: u16,
fg: Option<Color>,
selected_fg: Option<Color>,
props: WidgetProps,
}
impl RadioGroup {
pub fn new<I, S>(options: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let opts: Vec<String> = options.into_iter().map(|s| s.into()).collect();
let len = opts.len();
Self {
options: opts,
selection: Selection::new(len),
focused: false,
disabled: false,
style: RadioStyle::default(),
layout: RadioLayout::default(),
gap: 0,
fg: None,
selected_fg: None,
props: WidgetProps::new(),
}
}
pub fn selected(mut self, index: usize) -> Self {
self.selection.set(index);
self
}
pub fn focused(mut self, focused: bool) -> Self {
self.focused = focused;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn style(mut self, style: RadioStyle) -> Self {
self.style = style;
self
}
pub fn layout(mut self, layout: RadioLayout) -> Self {
self.layout = layout;
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn fg(mut self, color: Color) -> Self {
self.fg = Some(color);
self
}
pub fn selected_fg(mut self, color: Color) -> Self {
self.selected_fg = Some(color);
self
}
pub fn selected_index(&self) -> usize {
self.selection.index
}
pub fn selected_value(&self) -> Option<&str> {
self.options.get(self.selection.index).map(|s| s.as_str())
}
pub fn is_focused(&self) -> bool {
self.focused
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn select_next(&mut self) {
if !self.disabled {
self.selection.next();
}
}
pub fn select_prev(&mut self) {
if !self.disabled {
self.selection.prev();
}
}
pub fn set_focused(&mut self, focused: bool) {
self.focused = focused;
}
pub fn set_selected(&mut self, index: usize) {
self.selection.set(index);
}
pub fn handle_key(&mut self, key: &Key) -> bool {
if self.disabled {
return false;
}
match key {
Key::Up | Key::Char('k') => {
self.select_prev();
true
}
Key::Down | Key::Char('j') => {
self.select_next();
true
}
Key::Left if self.layout == RadioLayout::Horizontal => {
self.select_prev();
true
}
Key::Right if self.layout == RadioLayout::Horizontal => {
self.select_next();
true
}
Key::Char(c) if c.is_ascii_digit() => {
let index = (*c as u8 - b'0') as usize;
if index > 0 && index <= self.options.len() {
self.selection.set(index - 1);
true
} else {
false
}
}
_ => false,
}
}
fn render_option(&self, ctx: &mut RenderContext, index: usize, x: u16, y: u16) -> u16 {
let area = ctx.area;
if x >= area.width || y >= area.height {
return 0;
}
let is_selected = self.selection.is_selected(index);
let (selected_char, unselected_char) = self.style.chars();
let (left_bracket, right_bracket) = self.style.brackets();
let has_brackets = self.style.has_brackets();
let label_fg = if self.disabled {
DISABLED_FG
} else {
self.fg.unwrap_or(Color::WHITE)
};
let indicator_fg = if self.disabled {
DISABLED_FG
} else if is_selected {
self.selected_fg.unwrap_or(Color::CYAN)
} else {
self.fg.unwrap_or(LIGHT_GRAY)
};
let mut current_x = x;
if has_brackets {
let mut left_cell = Cell::new(left_bracket);
left_cell.fg = Some(label_fg);
ctx.set(current_x, y, left_cell);
current_x += 1;
let indicator = if is_selected {
selected_char
} else {
unselected_char
};
let mut ind_cell = Cell::new(indicator);
ind_cell.fg = Some(indicator_fg);
ctx.set(current_x, y, ind_cell);
current_x += 1;
let mut right_cell = Cell::new(right_bracket);
right_cell.fg = Some(label_fg);
ctx.set(current_x, y, right_cell);
current_x += 1;
} else {
let indicator = if is_selected {
selected_char
} else {
unselected_char
};
let mut ind_cell = Cell::new(indicator);
ind_cell.fg = Some(indicator_fg);
ctx.set(current_x, y, ind_cell);
current_x += 1;
}
ctx.set(current_x, y, Cell::new(' '));
current_x += 1;
if let Some(option) = self.options.get(index) {
for ch in option.chars() {
if current_x >= area.width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(label_fg);
if is_selected && self.focused && !self.disabled {
cell.modifier = crate::render::Modifier::BOLD;
}
ctx.set(current_x, y, cell);
current_x += 1;
}
}
current_x - x
}
}
impl Default for RadioGroup {
fn default() -> Self {
Self::new(Vec::<String>::new())
}
}
impl View for RadioGroup {
crate::impl_view_meta!("RadioGroup");
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
if area.width == 0 || area.height == 0 || self.options.is_empty() {
return;
}
let start_x: u16 = if self.focused && !self.disabled {
let mut arrow = Cell::new('>');
arrow.fg = Some(Color::CYAN);
ctx.set(0, 0, arrow);
2
} else {
0
};
match self.layout {
RadioLayout::Vertical => {
let mut y: u16 = 0;
for (i, _) in self.options.iter().enumerate() {
if y >= area.height {
break;
}
self.render_option(ctx, i, start_x, y);
y += 1 + self.gap;
}
}
RadioLayout::Horizontal => {
let mut x = start_x;
for (i, _option) in self.options.iter().enumerate() {
if x >= area.width {
break;
}
let width = self.render_option(ctx, i, x, 0);
x += width + 2 + self.gap; }
}
}
}
}
impl_styled_view!(RadioGroup);
impl_props_builders!(RadioGroup);
pub fn radio_group<I, S>(options: I) -> RadioGroup
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
RadioGroup::new(options)
}