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::Style;
7
8pub struct Checkbox {
13 label: String,
14 checked: bool,
15 focused: bool,
16 rect: Rect,
17 style: Style,
18 checked_style: Style,
19}
20
21impl Checkbox {
22 pub fn new(label: impl Into<String>) -> Self {
24 Self {
25 label: label.into(),
26 checked: false,
27 focused: false,
28 rect: Rect::default(),
29 style: Style::default(),
30 checked_style: Style::default().fg(crate::style::Color::Green),
31 }
32 }
33
34 pub fn checked(mut self) -> Self {
36 self.checked = true;
37 self
38 }
39
40 pub fn style(mut self, style: Style) -> Self {
42 self.style = style;
43 self
44 }
45
46 pub fn checked_style(mut self, style: Style) -> Self {
48 self.checked_style = style;
49 self
50 }
51
52 pub fn is_checked(&self) -> bool {
54 self.checked
55 }
56
57 pub fn set_checked(&mut self, checked: bool, cx: &mut EventCx) {
59 if self.checked != checked {
60 self.checked = checked;
61 cx.invalidate_paint();
62 }
63 }
64
65 pub fn toggle(&mut self, cx: &mut EventCx) {
67 self.checked = !self.checked;
68 cx.invalidate_paint();
69 }
70}
71
72impl Component for Checkbox {
73 fn render(&self, cx: &mut RenderCx) {
74 let mark = if self.checked { "✓" } else { " " };
75 let text = format!("[{}] {}", mark, self.label);
76 if self.focused {
77 cx.set_style(self.checked_style.clone());
78 } else if self.checked {
79 cx.set_style(self.checked_style.clone());
80 } else {
81 cx.set_style(self.style.clone());
82 }
83 cx.line(&text);
84 }
85
86 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
87 let w: u16 = self.label.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum();
88 Size { width: 5 + w, 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<String>,
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<String>) -> Self {
135 Self {
136 options,
137 selected: 0,
138 focused: false,
139 rect: Rect::default(),
140 style: Style::default(),
141 selected_style: Style::default().fg(crate::style::Color::Green),
142 }
143 }
144
145 pub fn style(mut self, style: Style) -> Self {
147 self.style = style;
148 self
149 }
150
151 pub fn selected_style(mut self, style: Style) -> Self {
153 self.selected_style = style;
154 self
155 }
156
157 pub fn selected(&self) -> usize {
159 self.selected
160 }
161
162 pub fn selected_text(&self) -> &str {
164 self.options.get(self.selected).map(|s| s.as_str()).unwrap_or("")
165 }
166
167 pub fn set_selected(&mut self, index: usize, cx: &mut EventCx) {
169 if index < self.options.len() && index != self.selected {
170 self.selected = index;
171 cx.invalidate_paint();
172 }
173 }
174}
175
176impl Component for RadioGroup {
177 fn render(&self, cx: &mut RenderCx) {
178 for (i, opt) in self.options.iter().enumerate() {
179 let (mark, style) = if i == self.selected {
180 if self.focused {
181 ("•", self.selected_style.clone())
182 } else {
183 ("•", self.selected_style.clone())
184 }
185 } else {
186 (" ", self.style.clone())
187 };
188 cx.set_style(style);
189 cx.line(&format!("({}) {}", mark, opt));
190 }
191 }
192
193 fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
194 let max_w: u16 = self.options.iter()
195 .map(|o| 4 + o.chars().map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0) as u16).sum::<u16>())
196 .max()
197 .unwrap_or(0);
198 Size { width: max_w, height: self.options.len() as u16 }
199 }
200
201 fn event(&mut self, event: &Event, cx: &mut EventCx) {
202 match event {
203 Event::Focus => { self.focused = true; cx.invalidate_paint(); return; }
204 Event::Blur => { self.focused = false; cx.invalidate_paint(); return; }
205 _ => {}
206 }
207
208 if cx.phase() != crate::event::EventPhase::Target { return; }
210 if self.options.is_empty() { return; }
211
212 if let Event::Key(key_event) = event {
213 match &key_event.key {
214 crate::event::Key::Up => {
215 self.selected = if self.selected > 0 { self.selected - 1 } else { self.options.len() - 1 };
216 cx.invalidate_paint();
217 }
218 crate::event::Key::Down => {
219 self.selected = if self.selected + 1 < self.options.len() { self.selected + 1 } else { 0 };
220 cx.invalidate_paint();
221 }
222 _ => {}
223 }
224 }
225 }
226
227 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) { self.rect = rect; }
228 fn focusable(&self) -> bool { true }
229 fn style(&self) -> Style { self.style.clone() }
230}