lv_tui/widgets/
checkbox.rs1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::{Color, Style};
7use crate::text::Text;
8
9pub struct Checkbox {
14 label: Text,
15 checked: bool,
16 focused: bool,
17 rect: Rect,
18 style: Style,
19 checked_style: Style,
20}
21
22impl Checkbox {
23 pub fn new(label: impl Into<Text>) -> Self {
25 Self {
26 label: label.into(),
27 checked: false,
28 focused: false,
29 rect: Rect::default(),
30 style: Style::default(),
31 checked_style: Style::default().fg(crate::style::Color::Green),
32 }
33 }
34
35 pub fn checked(mut self) -> Self {
37 self.checked = true;
38 self
39 }
40
41 pub fn style(mut self, style: Style) -> Self {
43 self.style = style;
44 self
45 }
46
47 pub fn checked_style(mut self, style: Style) -> Self {
49 self.checked_style = style;
50 self
51 }
52
53 pub fn is_checked(&self) -> bool {
55 self.checked
56 }
57
58 pub fn set_checked(&mut self, checked: bool, cx: &mut EventCx) {
60 if self.checked != checked {
61 self.checked = checked;
62 cx.invalidate_paint();
63 }
64 }
65
66 pub fn toggle(&mut self, cx: &mut EventCx) {
68 self.checked = !self.checked;
69 cx.invalidate_paint();
70 }
71}
72
73impl Component for Checkbox {
74 fn render(&self, cx: &mut RenderCx) {
75 let mark = if self.checked { "✓" } else { " " };
76 let text = format!("[{}] {}", mark, self.label.first_text());
77 if self.focused {
78 cx.set_style(self.checked_style.clone());
79 } else if self.checked {
80 cx.set_style(self.checked_style.clone());
81 } else {
82 cx.set_style(self.style.clone());
83 }
84 cx.line(&text);
85 }
86
87 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
88 Size { width: 5 + self.label.max_width(), height: 1 }
89 }
90
91 fn event(&mut self, event: &Event, cx: &mut EventCx) {
92 match event {
93 Event::Focus => { self.focused = true; cx.invalidate_paint(); return; }
94 Event::Blur => { self.focused = false; cx.invalidate_paint(); return; }
95 _ => {}
96 }
97
98 if cx.phase() != crate::event::EventPhase::Target { return; }
100
101 if let Event::Key(key_event) = event {
102 match &key_event.key {
103 crate::event::Key::Char(' ') | crate::event::Key::Enter => {
104 self.toggle(cx);
105 }
106 _ => {}
107 }
108 }
109 }
110
111 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) { self.rect = rect; }
112 fn focusable(&self) -> bool { true }
113 fn style(&self) -> Style { self.style.clone() }
114}
115
116pub struct RadioGroup {
124 options: Vec<Text>,
125 selected: usize,
126 focused: bool,
127 rect: Rect,
128 style: Style,
129 selected_style: Style,
130}
131
132impl RadioGroup {
133 pub fn new(options: Vec<impl Into<Text>>) -> Self {
135 let options = options.into_iter().map(|o| o.into()).collect();
136 Self {
137 options,
138 selected: 0,
139 focused: false,
140 rect: Rect::default(),
141 style: Style::default(),
142 selected_style: Style::default().fg(crate::style::Color::Green),
143 }
144 }
145
146 pub fn style(mut self, style: Style) -> Self {
148 self.style = style;
149 self
150 }
151
152 pub fn selected_style(mut self, style: Style) -> Self {
154 self.selected_style = style;
155 self
156 }
157
158 pub fn selected(&self) -> usize {
160 self.selected
161 }
162
163 pub fn selected_text(&self) -> &str {
165 self.options.get(self.selected).map(|t| t.first_text()).unwrap_or("")
166 }
167
168 pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
170 if index < self.options.len() && index != self.selected {
171 self.selected = index;
172 cx.invalidate_paint();
173 }
174 }
175}
176
177impl Component for RadioGroup {
178 fn render(&self, cx: &mut RenderCx) {
179 if self.focused {
181 for y in cx.rect.y..cx.rect.y + cx.rect.height {
182 for x in cx.rect.x..cx.rect.x + cx.rect.width {
183 if let Some(cell) = cx.buffer.get_mut(x, y) {
184 cell.style.bg = Some(Color::White);
185 }
186 }
187 }
188 }
189 for (i, opt) in self.options.iter().enumerate() {
190 let (mark, style) = if i == self.selected {
191 if self.focused {
192 ("•", Style::default().bg(Color::White).fg(Color::Black))
193 } else {
194 ("•", Style::default().fg(Color::Green))
195 }
196 } else {
197 if self.focused {
198 (" ", Style::default().bg(Color::White).fg(Color::Black))
199 } else {
200 (" ", self.style.clone())
201 }
202 };
203 cx.set_style(style);
204 cx.line(&format!("({}) {}", mark, opt.first_text()));
205 }
206 }
207
208 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
209 let max_w = self.options.iter().map(|o| 4 + o.max_width()).max().unwrap_or(0);
210 Size { width: max_w, height: self.options.len() as u16 }
211 }
212
213 fn event(&mut self, event: &Event, cx: &mut EventCx) {
214 match event {
215 Event::Focus => { self.focused = true; cx.invalidate_paint(); return; }
216 Event::Blur => { self.focused = false; cx.invalidate_paint(); return; }
217 _ => {}
218 }
219
220 if cx.phase() != crate::event::EventPhase::Target { return; }
222 if self.options.is_empty() { return; }
223
224 if let Event::Key(key_event) = event {
225 match &key_event.key {
226 crate::event::Key::Up => {
227 self.selected = if self.selected > 0 { self.selected - 1 } else { self.options.len() - 1 };
228 cx.invalidate_paint();
229 }
230 crate::event::Key::Down => {
231 self.selected = if self.selected + 1 < self.options.len() { self.selected + 1 } else { 0 };
232 cx.invalidate_paint();
233 }
234 _ => {}
235 }
236 }
237 }
238
239 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) { self.rect = rect; }
240 fn focusable(&self) -> bool { true }
241 fn style(&self) -> Style { self.style.clone() }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::testbuffer::TestBuffer;
248
249 #[test]
250 fn test_radio_group_renders() {
251 let mut tb = TestBuffer::new(20, 2);
252 tb.render(&RadioGroup::new(vec![Text::from("A"), Text::from("B")]));
253 assert!(tb.buffer.cells[0].symbol.contains('('));
255 }
256
257 #[test]
258 fn test_radio_group_selection_marker() {
259 let mut tb = TestBuffer::new(20, 1);
260 tb.render(&RadioGroup::new(vec![Text::from("Option")]));
261 assert_eq!(&tb.buffer.cells[1].symbol, "•");
263 }
264}
265
266 #[test]
267 fn test_checkbox_toggle() {
268 let cb = Checkbox::new("opt").checked();
270 assert!(cb.is_checked());
271 }