1use crate::_private::NonExhaustive;
24use crate::checkbox::event::CheckOutcome;
25use crate::text::HasScreenCursor;
26use crate::util::{block_size, revert_style};
27use rat_event::util::MouseFlags;
28use rat_event::{HandleEvent, MouseOnly, Regular, ct_event};
29use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
30use rat_reloc::RelocatableState;
31use ratatui::buffer::Buffer;
32use ratatui::layout::Rect;
33use ratatui::prelude::BlockExt;
34use ratatui::style::Style;
35use ratatui::text::Span;
36use ratatui::text::Text;
37use ratatui::widgets::Block;
38use ratatui::widgets::{StatefulWidget, Widget};
39use std::cmp::max;
40use unicode_segmentation::UnicodeSegmentation;
41
42#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
44pub enum CheckboxCheck {
45 SingleClick,
46 #[default]
47 DoubleClick,
48}
49
50#[derive(Debug, Clone)]
52pub struct Checkbox<'a> {
53 text: Text<'a>,
54
55 checked: Option<bool>,
57 default: Option<bool>,
58
59 true_str: Span<'a>,
60 false_str: Span<'a>,
61
62 behave_check: CheckboxCheck,
63
64 style: Style,
65 block: Option<Block<'a>>,
66 focus_style: Option<Style>,
67}
68
69#[derive(Debug, Clone)]
71pub struct CheckboxStyle {
72 pub style: Style,
74 pub focus: Option<Style>,
76 pub block: Option<Block<'static>>,
78 pub border_style: Option<Style>,
79 pub title_style: Option<Style>,
80
81 pub true_str: Option<Span<'static>>,
83 pub false_str: Option<Span<'static>>,
85
86 pub behave_check: Option<CheckboxCheck>,
87
88 pub non_exhaustive: NonExhaustive,
89}
90
91#[derive(Debug)]
93pub struct CheckboxState {
94 pub area: Rect,
97 pub inner: Rect,
100 pub check_area: Rect,
103 pub text_area: Rect,
106 pub behave_check: CheckboxCheck,
109
110 pub checked: bool,
113
114 pub default: bool,
117
118 pub focus: FocusFlag,
121
122 pub mouse: MouseFlags,
125
126 pub non_exhaustive: NonExhaustive,
127}
128
129pub(crate) mod event {
130 use rat_event::{ConsumedEvent, Outcome};
131
132 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
134 pub enum CheckOutcome {
135 Continue,
137 Unchanged,
139 Changed,
141 Value,
143 }
144
145 impl ConsumedEvent for CheckOutcome {
146 fn is_consumed(&self) -> bool {
147 *self != CheckOutcome::Continue
148 }
149 }
150
151 impl From<CheckOutcome> for Outcome {
152 fn from(value: CheckOutcome) -> Self {
153 match value {
154 CheckOutcome::Continue => Outcome::Continue,
155 CheckOutcome::Unchanged => Outcome::Unchanged,
156 CheckOutcome::Changed => Outcome::Changed,
157 CheckOutcome::Value => Outcome::Changed,
158 }
159 }
160 }
161}
162
163impl Default for CheckboxStyle {
164 fn default() -> Self {
165 Self {
166 style: Default::default(),
167 focus: Default::default(),
168 block: Default::default(),
169 border_style: Default::default(),
170 title_style: Default::default(),
171 true_str: Default::default(),
172 false_str: Default::default(),
173 behave_check: Default::default(),
174 non_exhaustive: NonExhaustive,
175 }
176 }
177}
178
179impl Default for Checkbox<'_> {
180 fn default() -> Self {
181 Self {
182 text: Default::default(),
183 checked: Default::default(),
184 default: Default::default(),
185 true_str: Span::from("[\u{2713}]"),
186 false_str: Span::from("[ ]"),
187 behave_check: Default::default(),
188 style: Default::default(),
189 focus_style: Default::default(),
190 block: Default::default(),
191 }
192 }
193}
194
195impl<'a> Checkbox<'a> {
196 pub fn new() -> Self {
198 Self::default()
199 }
200
201 pub fn styles(mut self, styles: CheckboxStyle) -> Self {
203 self.style = styles.style;
204 if styles.block.is_some() {
205 self.block = styles.block;
206 }
207 if let Some(border_style) = styles.border_style {
208 self.block = self.block.map(|v| v.border_style(border_style));
209 }
210 if let Some(title_style) = styles.title_style {
211 self.block = self.block.map(|v| v.title_style(title_style));
212 }
213 self.block = self.block.map(|v| v.style(self.style));
214 if styles.focus.is_some() {
215 self.focus_style = styles.focus;
216 }
217 if let Some(true_str) = styles.true_str {
218 self.true_str = true_str;
219 }
220 if let Some(false_str) = styles.false_str {
221 self.false_str = false_str;
222 }
223 if let Some(check) = styles.behave_check {
224 self.behave_check = check;
225 }
226 self
227 }
228
229 #[inline]
231 pub fn style(mut self, style: impl Into<Style>) -> Self {
232 let style = style.into();
233 self.style = style.clone();
234 self.block = self.block.map(|v| v.style(style));
235 self
236 }
237
238 #[inline]
240 pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
241 self.focus_style = Some(style.into());
242 self
243 }
244
245 #[inline]
247 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
248 self.text = text.into();
249 self
250 }
251
252 pub fn checked(mut self, checked: bool) -> Self {
254 self.checked = Some(checked);
255 self
256 }
257
258 pub fn default_(mut self, default: bool) -> Self {
260 self.default = Some(default);
261 self
262 }
263
264 #[inline]
266 pub fn block(mut self, block: Block<'a>) -> Self {
267 self.block = Some(block);
268 self.block = self.block.map(|v| v.style(self.style));
269 self
270 }
271
272 pub fn true_str(mut self, str: Span<'a>) -> Self {
274 self.true_str = str;
275 self
276 }
277
278 pub fn false_str(mut self, str: Span<'a>) -> Self {
280 self.false_str = str;
281 self
282 }
283
284 pub fn behave_check(mut self, check: CheckboxCheck) -> Self {
286 self.behave_check = check;
287 self
288 }
289
290 fn check_len(&self) -> u16 {
292 max(
293 self.true_str.content.graphemes(true).count(),
294 self.false_str.content.graphemes(true).count(),
295 ) as u16
296 }
297
298 pub fn width(&self) -> u16 {
300 let chk_len = self.check_len();
301 let txt_len = self.text.width() as u16;
302
303 chk_len + 1 + txt_len + block_size(&self.block).width
304 }
305
306 pub fn height(&self) -> u16 {
308 self.text.height() as u16 + block_size(&self.block).height
309 }
310}
311
312impl<'a> StatefulWidget for &Checkbox<'a> {
313 type State = CheckboxState;
314
315 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
316 render_ref(self, area, buf, state);
317 }
318}
319
320impl StatefulWidget for Checkbox<'_> {
321 type State = CheckboxState;
322
323 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
324 render_ref(&self, area, buf, state);
325 }
326}
327
328fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
329 state.area = area;
330 state.inner = widget.block.inner_if_some(area);
331 state.behave_check = widget.behave_check;
332
333 let chk_len = widget.check_len();
334 state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
335 state.text_area = Rect::new(
336 state.inner.x + chk_len + 1,
337 state.inner.y,
338 state.inner.width.saturating_sub(chk_len + 1),
339 state.inner.height,
340 );
341
342 if let Some(checked) = widget.checked {
343 state.checked = checked;
344 }
345 if let Some(default) = widget.default {
346 state.default = default;
347 }
348
349 let style = widget.style;
350 let focus_style = if let Some(focus_style) = widget.focus_style {
351 style.patch(focus_style)
352 } else {
353 revert_style(style)
354 };
355
356 if let Some(block) = &widget.block {
357 block.render(area, buf);
358 if state.focus.get() {
359 buf.set_style(state.inner, focus_style);
360 }
361 } else {
362 if state.focus.get() {
363 buf.set_style(state.inner, focus_style);
364 } else {
365 buf.set_style(state.inner, widget.style);
366 }
367 }
368
369 let cc = if state.checked {
370 &widget.true_str
371 } else {
372 &widget.false_str
373 };
374 cc.render(state.check_area, buf);
375 (&widget.text).render(state.text_area, buf);
376}
377
378impl Clone for CheckboxState {
379 fn clone(&self) -> Self {
380 Self {
381 area: self.area,
382 inner: self.inner,
383 check_area: self.check_area,
384 text_area: self.text_area,
385 behave_check: self.behave_check,
386 checked: self.checked,
387 default: self.default,
388 focus: self.focus.new_instance(),
389 mouse: Default::default(),
390 non_exhaustive: NonExhaustive,
391 }
392 }
393}
394
395impl Default for CheckboxState {
396 fn default() -> Self {
397 Self {
398 area: Default::default(),
399 inner: Default::default(),
400 check_area: Default::default(),
401 text_area: Default::default(),
402 behave_check: Default::default(),
403 checked: false,
404 default: false,
405 focus: Default::default(),
406 mouse: Default::default(),
407 non_exhaustive: NonExhaustive,
408 }
409 }
410}
411
412impl HasFocus for CheckboxState {
413 fn build(&self, builder: &mut FocusBuilder) {
414 builder.leaf_widget(self);
415 }
416
417 fn focus(&self) -> FocusFlag {
418 self.focus.clone()
419 }
420
421 fn area(&self) -> Rect {
422 self.area
423 }
424}
425
426impl HasScreenCursor for CheckboxState {
427 fn screen_cursor(&self) -> Option<(u16, u16)> {
428 None
429 }
430}
431
432impl RelocatableState for CheckboxState {
433 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
434 self.area.relocate(shift, clip);
435 self.inner.relocate(shift, clip);
436 self.check_area.relocate(shift, clip);
437 }
438}
439
440impl CheckboxState {
441 pub fn new() -> Self {
442 Self::default()
443 }
444
445 pub fn named(name: &str) -> Self {
446 let mut z = Self::default();
447 z.focus = z.focus.with_name(name);
448 z
449 }
450
451 pub fn checked(&self) -> bool {
453 self.checked
454 }
455
456 pub fn set_checked(&mut self, checked: bool) -> bool {
458 let old_value = self.checked;
459 self.checked = checked;
460 old_value != self.checked
461 }
462
463 pub fn default_(&self) -> bool {
465 self.default
466 }
467
468 pub fn set_default(&mut self, default: bool) -> bool {
470 let old_value = self.default;
471 self.default = default;
472 old_value != self.default
473 }
474
475 pub fn value(&self) -> bool {
477 self.checked
478 }
479
480 pub fn set_value(&mut self, checked: bool) -> bool {
482 let old_value = self.checked;
483 self.checked = checked;
484 old_value != self.checked
485 }
486
487 pub fn clear(&mut self) {
489 self.checked = self.default;
490 }
491
492 pub fn flip_checked(&mut self) {
496 self.checked = !self.checked;
497 }
498}
499
500impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
501 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
502 let r = if self.is_focused() {
503 match event {
504 ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
505 self.flip_checked();
506 CheckOutcome::Value
507 }
508 ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
509 self.set_value(self.default);
510 CheckOutcome::Value
511 }
512 _ => CheckOutcome::Continue,
513 }
514 } else {
515 CheckOutcome::Continue
516 };
517
518 if r == CheckOutcome::Continue {
519 HandleEvent::handle(self, event, MouseOnly)
520 } else {
521 r
522 }
523 }
524}
525
526impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
527 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
528 match event {
529 ct_event!(mouse any for m)
530 if self.behave_check == CheckboxCheck::DoubleClick
531 && self.mouse.doubleclick(self.area, m) =>
532 {
533 self.flip_checked();
534 CheckOutcome::Value
535 }
536 ct_event!(mouse down Left for x,y)
537 if self.behave_check == CheckboxCheck::SingleClick
538 && self.area.contains((*x, *y).into()) =>
539 {
540 self.flip_checked();
541 CheckOutcome::Value
542 }
543 _ => CheckOutcome::Continue,
544 }
545 }
546}
547
548pub fn handle_events(
552 state: &mut CheckboxState,
553 focus: bool,
554 event: &crossterm::event::Event,
555) -> CheckOutcome {
556 state.focus.set(focus);
557 HandleEvent::handle(state, event, Regular)
558}
559
560pub fn handle_mouse_events(
562 state: &mut CheckboxState,
563 event: &crossterm::event::Event,
564) -> CheckOutcome {
565 HandleEvent::handle(state, event, MouseOnly)
566}