Skip to main content

datui_lib/widgets/
radio_block.rs

1//! Reusable radio-button block: a bordered block with a grid of options (● selected, ○ unselected).
2//! Used for chart type, chart export format, and pivot aggregation.
3
4use ratatui::{
5    buffer::Buffer,
6    layout::{Constraint, Direction, Layout, Rect},
7    style::{Modifier, Style},
8    text::{Line, Span},
9    widgets::{Block, BorderType, Borders, Paragraph, Widget},
10};
11
12/// Renders a block of radio options. Options are laid out in a grid with `columns` per row.
13/// Selected item is drawn with ● and highlighted when focused; others with ○.
14pub struct RadioBlock<'a> {
15    pub title: &'a str,
16    pub options: &'a [&'a str],
17    pub selected: usize,
18    pub focused: bool,
19    pub columns: usize,
20    pub border_color: ratatui::style::Color,
21    pub active_color: ratatui::style::Color,
22}
23
24impl<'a> RadioBlock<'a> {
25    pub fn new(
26        title: &'a str,
27        options: &'a [&'a str],
28        selected: usize,
29        focused: bool,
30        columns: usize,
31        border_color: ratatui::style::Color,
32        active_color: ratatui::style::Color,
33    ) -> Self {
34        Self {
35            title,
36            options,
37            selected,
38            focused,
39            columns: columns.max(1),
40            border_color,
41            active_color,
42        }
43    }
44
45    fn render_inner(&self, area: Rect, buf: &mut Buffer) {
46        if self.options.is_empty() {
47            return;
48        }
49        let n = self.options.len();
50        let cols = self.columns.min(n);
51        let rows = n.div_ceil(cols);
52
53        let row_constraints: Vec<Constraint> = (0..rows).map(|_| Constraint::Length(1)).collect();
54        let row_chunks = Layout::default()
55            .direction(Direction::Vertical)
56            .constraints(row_constraints)
57            .split(area);
58
59        let col_width = area.width / cols as u16;
60        let col_constraints: Vec<Constraint> =
61            (0..cols).map(|_| Constraint::Length(col_width)).collect();
62
63        for (idx, label) in self.options.iter().enumerate() {
64            let row = idx / cols;
65            let col = idx % cols;
66            if row >= row_chunks.len() {
67                break;
68            }
69            let row_rect = row_chunks[row];
70            let col_chunks = Layout::default()
71                .direction(Direction::Horizontal)
72                .constraints(col_constraints.as_slice())
73                .split(row_rect);
74            let cell = col_chunks[col];
75
76            let is_selected = idx == self.selected;
77            let marker = if is_selected { "●" } else { "○" };
78            let text = format!("{} {}", marker, *label);
79            let style = if is_selected {
80                Style::default().fg(self.active_color)
81            } else {
82                Style::default().fg(self.border_color)
83            };
84            let style = if self.focused && is_selected {
85                style.add_modifier(Modifier::REVERSED)
86            } else {
87                style
88            };
89            Paragraph::new(Line::from(Span::styled(text, style))).render(cell, buf);
90        }
91    }
92}
93
94impl Widget for RadioBlock<'_> {
95    fn render(self, area: Rect, buf: &mut Buffer) {
96        let block_style = if self.focused {
97            Style::default().fg(self.active_color)
98        } else {
99            Style::default().fg(self.border_color)
100        };
101        let block = Block::default()
102            .borders(Borders::ALL)
103            .border_type(BorderType::Rounded)
104            .title(self.title)
105            .border_style(block_style);
106        let inner = block.inner(area);
107        block.render(area, buf);
108        self.render_inner(inner, buf);
109    }
110}