1use crate::_private::NonExhaustive;
24use crate::checkbox::event::CheckOutcome;
25use crate::util::{block_size, revert_style};
26use rat_event::util::MouseFlags;
27use rat_event::{ct_event, HandleEvent, MouseOnly, Regular};
28use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
29use rat_reloc::{relocate_area, RelocatableState};
30use ratatui::buffer::Buffer;
31use ratatui::layout::Rect;
32use ratatui::prelude::BlockExt;
33use ratatui::style::Style;
34use ratatui::text::Span;
35use ratatui::text::Text;
36use ratatui::widgets::Block;
37use ratatui::widgets::{StatefulWidget, Widget};
38use std::cmp::max;
39use unicode_segmentation::UnicodeSegmentation;
40
41#[derive(Debug, Clone)]
43pub struct Checkbox<'a> {
44 text: Text<'a>,
45
46 checked: Option<bool>,
48 default: Option<bool>,
49
50 true_str: Span<'a>,
51 false_str: Span<'a>,
52
53 style: Style,
54 focus_style: Option<Style>,
55 block: Option<Block<'a>>,
56}
57
58#[derive(Debug, Clone)]
60pub struct CheckboxStyle {
61 pub style: Style,
63 pub focus: Option<Style>,
65 pub block: Option<Block<'static>>,
67
68 pub true_str: Option<Span<'static>>,
70 pub false_str: Option<Span<'static>>,
72
73 pub non_exhaustive: NonExhaustive,
74}
75
76#[derive(Debug)]
78pub struct CheckboxState {
79 pub area: Rect,
82 pub inner: Rect,
85 pub check_area: Rect,
88 pub text_area: Rect,
91
92 pub checked: bool,
95
96 pub default: bool,
99
100 pub focus: FocusFlag,
103
104 pub mouse: MouseFlags,
107
108 pub non_exhaustive: NonExhaustive,
109}
110
111pub(crate) mod event {
112 use rat_event::{ConsumedEvent, Outcome};
113
114 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
116 pub enum CheckOutcome {
117 Continue,
119 Unchanged,
121 Changed,
123 Value,
125 }
126
127 impl ConsumedEvent for CheckOutcome {
128 fn is_consumed(&self) -> bool {
129 *self != CheckOutcome::Continue
130 }
131 }
132
133 impl From<CheckOutcome> for Outcome {
134 fn from(value: CheckOutcome) -> Self {
135 match value {
136 CheckOutcome::Continue => Outcome::Continue,
137 CheckOutcome::Unchanged => Outcome::Unchanged,
138 CheckOutcome::Changed => Outcome::Changed,
139 CheckOutcome::Value => Outcome::Changed,
140 }
141 }
142 }
143}
144
145impl Default for CheckboxStyle {
146 fn default() -> Self {
147 Self {
148 style: Default::default(),
149 focus: None,
150 block: Default::default(),
151 true_str: None,
152 false_str: None,
153 non_exhaustive: NonExhaustive,
154 }
155 }
156}
157
158impl Default for Checkbox<'_> {
159 fn default() -> Self {
160 Self {
161 text: Default::default(),
162 checked: None,
163 default: None,
164 true_str: Span::from("[\u{2713}]"),
165 false_str: Span::from("[ ]"),
166 style: Default::default(),
167 focus_style: None,
168 block: None,
169 }
170 }
171}
172
173impl<'a> Checkbox<'a> {
174 pub fn new() -> Self {
176 Self::default()
177 }
178
179 pub fn styles(mut self, styles: CheckboxStyle) -> Self {
181 self.style = styles.style;
182 if styles.focus.is_some() {
183 self.focus_style = styles.focus;
184 }
185 if let Some(block) = styles.block {
186 self.block = Some(block);
187 }
188 if let Some(true_str) = styles.true_str {
189 self.true_str = true_str;
190 }
191 if let Some(false_str) = styles.false_str {
192 self.false_str = false_str;
193 }
194 self.block = self.block.map(|v| v.style(self.style));
195 self
196 }
197
198 #[inline]
200 pub fn style(mut self, style: impl Into<Style>) -> Self {
201 self.style = style.into();
202 self
203 }
204
205 #[inline]
207 pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
208 self.focus_style = Some(style.into());
209 self
210 }
211
212 #[inline]
214 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
215 self.text = text.into();
216 self
217 }
218
219 pub fn checked(mut self, checked: bool) -> Self {
221 self.checked = Some(checked);
222 self
223 }
224
225 pub fn default_(mut self, default: bool) -> Self {
227 self.default = Some(default);
228 self
229 }
230
231 #[inline]
233 pub fn block(mut self, block: Block<'a>) -> Self {
234 self.block = Some(block);
235 self.block = self.block.map(|v| v.style(self.style));
236 self
237 }
238
239 pub fn true_str(mut self, str: Span<'a>) -> Self {
241 self.true_str = str;
242 self
243 }
244
245 pub fn false_str(mut self, str: Span<'a>) -> Self {
247 self.false_str = str;
248 self
249 }
250
251 fn check_len(&self) -> u16 {
253 max(
254 self.true_str.content.graphemes(true).count(),
255 self.false_str.content.graphemes(true).count(),
256 ) as u16
257 }
258
259 pub fn width(&self) -> u16 {
261 let chk_len = self.check_len();
262 let txt_len = self.text.width() as u16;
263
264 chk_len + 1 + txt_len + block_size(&self.block).width
265 }
266
267 pub fn height(&self) -> u16 {
269 self.text.height() as u16 + block_size(&self.block).height
270 }
271}
272
273impl<'a> StatefulWidget for &Checkbox<'a> {
274 type State = CheckboxState;
275
276 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
277 render_ref(self, area, buf, state);
278 }
279}
280
281impl StatefulWidget for Checkbox<'_> {
282 type State = CheckboxState;
283
284 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
285 render_ref(&self, area, buf, state);
286 }
287}
288
289fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
290 state.area = area;
291 state.inner = widget.block.inner_if_some(area);
292
293 let chk_len = widget.check_len();
294 state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
295 state.text_area = Rect::new(
296 state.inner.x + chk_len + 1,
297 state.inner.y,
298 state.inner.width.saturating_sub(chk_len + 1),
299 state.inner.height,
300 );
301
302 if let Some(checked) = widget.checked {
303 state.checked = checked;
304 }
305 if let Some(default) = widget.default {
306 state.default = default;
307 }
308
309 let style = widget.style;
310 let focus_style = if let Some(focus_style) = widget.focus_style {
311 style.patch(focus_style)
312 } else {
313 revert_style(style)
314 };
315
316 if let Some(block) = &widget.block {
317 block.render(area, buf);
318 if state.focus.get() {
319 buf.set_style(state.inner, focus_style);
320 }
321 } else {
322 if state.focus.get() {
323 buf.set_style(state.inner, focus_style);
324 } else {
325 buf.set_style(state.inner, widget.style);
326 }
327 }
328
329 let cc = if state.checked {
330 &widget.true_str
331 } else {
332 &widget.false_str
333 };
334 cc.render(state.check_area, buf);
335 (&widget.text).render(state.text_area, buf);
336}
337
338impl Clone for CheckboxState {
339 fn clone(&self) -> Self {
340 Self {
341 area: self.area,
342 inner: self.inner,
343 check_area: self.check_area,
344 text_area: self.text_area,
345 checked: self.checked,
346 default: self.default,
347 focus: FocusFlag::named(self.focus.name()),
348 mouse: Default::default(),
349 non_exhaustive: NonExhaustive,
350 }
351 }
352}
353
354impl Default for CheckboxState {
355 fn default() -> Self {
356 Self {
357 area: Default::default(),
358 inner: Default::default(),
359 check_area: Default::default(),
360 text_area: Default::default(),
361 checked: false,
362 default: false,
363 focus: Default::default(),
364 mouse: Default::default(),
365 non_exhaustive: NonExhaustive,
366 }
367 }
368}
369
370impl HasFocus for CheckboxState {
371 fn build(&self, builder: &mut FocusBuilder) {
372 builder.leaf_widget(self);
373 }
374
375 fn focus(&self) -> FocusFlag {
376 self.focus.clone()
377 }
378
379 fn area(&self) -> Rect {
380 self.area
381 }
382}
383
384impl RelocatableState for CheckboxState {
385 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
386 self.area = relocate_area(self.area, shift, clip);
387 self.inner = relocate_area(self.inner, shift, clip);
388 }
389}
390
391impl CheckboxState {
392 pub fn new() -> Self {
393 Self::default()
394 }
395
396 pub fn named(name: &str) -> Self {
397 Self {
398 focus: FocusFlag::named(name),
399 ..Default::default()
400 }
401 }
402
403 pub fn checked(&self) -> bool {
405 self.checked
406 }
407
408 pub fn set_checked(&mut self, checked: bool) -> bool {
410 let old_value = self.checked;
411 self.checked = checked;
412 old_value != self.checked
413 }
414
415 pub fn default_(&self) -> bool {
417 self.default
418 }
419
420 pub fn set_default(&mut self, default: bool) -> bool {
422 let old_value = self.default;
423 self.default = default;
424 old_value != self.default
425 }
426
427 pub fn value(&self) -> bool {
429 self.checked
430 }
431
432 pub fn set_value(&mut self, checked: bool) -> bool {
434 let old_value = self.checked;
435 self.checked = checked;
436 old_value != self.checked
437 }
438
439 pub fn flip_checked(&mut self) {
443 self.checked = !self.checked;
444 }
445}
446
447impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
448 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
449 let r = if self.is_focused() {
450 match event {
451 ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
452 self.flip_checked();
453 CheckOutcome::Value
454 }
455 ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
456 self.set_value(self.default);
457 CheckOutcome::Value
458 }
459 _ => CheckOutcome::Continue,
460 }
461 } else {
462 CheckOutcome::Continue
463 };
464
465 if r == CheckOutcome::Continue {
466 HandleEvent::handle(self, event, MouseOnly)
467 } else {
468 r
469 }
470 }
471}
472
473impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
474 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
475 match event {
476 ct_event!(mouse any for m) if self.mouse.doubleclick(self.area, m) => {
477 self.flip_checked();
478 CheckOutcome::Value
479 }
480 _ => CheckOutcome::Continue,
481 }
482 }
483}
484
485pub fn handle_events(
489 state: &mut CheckboxState,
490 focus: bool,
491 event: &crossterm::event::Event,
492) -> CheckOutcome {
493 state.focus.set(focus);
494 HandleEvent::handle(state, event, Regular)
495}
496
497pub fn handle_mouse_events(
499 state: &mut CheckboxState,
500 event: &crossterm::event::Event,
501) -> CheckOutcome {
502 HandleEvent::handle(state, event, MouseOnly)
503}