datui_lib/widgets/
radio_block.rs1use 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
12pub 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}