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 focus_style: Option<Style>,
66 block: Option<Block<'a>>,
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
79 pub true_str: Option<Span<'static>>,
81 pub false_str: Option<Span<'static>>,
83
84 pub behave_check: Option<CheckboxCheck>,
85
86 pub non_exhaustive: NonExhaustive,
87}
88
89#[derive(Debug)]
91pub struct CheckboxState {
92 pub area: Rect,
95 pub inner: Rect,
98 pub check_area: Rect,
101 pub text_area: Rect,
104 pub behave_check: CheckboxCheck,
107
108 pub checked: bool,
111
112 pub default: bool,
115
116 pub focus: FocusFlag,
119
120 pub mouse: MouseFlags,
123
124 pub non_exhaustive: NonExhaustive,
125}
126
127pub(crate) mod event {
128 use rat_event::{ConsumedEvent, Outcome};
129
130 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
132 pub enum CheckOutcome {
133 Continue,
135 Unchanged,
137 Changed,
139 Value,
141 }
142
143 impl ConsumedEvent for CheckOutcome {
144 fn is_consumed(&self) -> bool {
145 *self != CheckOutcome::Continue
146 }
147 }
148
149 impl From<CheckOutcome> for Outcome {
150 fn from(value: CheckOutcome) -> Self {
151 match value {
152 CheckOutcome::Continue => Outcome::Continue,
153 CheckOutcome::Unchanged => Outcome::Unchanged,
154 CheckOutcome::Changed => Outcome::Changed,
155 CheckOutcome::Value => Outcome::Changed,
156 }
157 }
158 }
159}
160
161impl Default for CheckboxStyle {
162 fn default() -> Self {
163 Self {
164 style: Default::default(),
165 focus: Default::default(),
166 block: Default::default(),
167 true_str: Default::default(),
168 false_str: Default::default(),
169 behave_check: Default::default(),
170 non_exhaustive: NonExhaustive,
171 }
172 }
173}
174
175impl Default for Checkbox<'_> {
176 fn default() -> Self {
177 Self {
178 text: Default::default(),
179 checked: Default::default(),
180 default: Default::default(),
181 true_str: Span::from("[\u{2713}]"),
182 false_str: Span::from("[ ]"),
183 behave_check: Default::default(),
184 style: Default::default(),
185 focus_style: Default::default(),
186 block: Default::default(),
187 }
188 }
189}
190
191impl<'a> Checkbox<'a> {
192 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn styles(mut self, styles: CheckboxStyle) -> Self {
199 self.style = styles.style;
200 if styles.focus.is_some() {
201 self.focus_style = styles.focus;
202 }
203 if let Some(block) = styles.block {
204 self.block = Some(block);
205 }
206 if let Some(true_str) = styles.true_str {
207 self.true_str = true_str;
208 }
209 if let Some(false_str) = styles.false_str {
210 self.false_str = false_str;
211 }
212 if let Some(check) = styles.behave_check {
213 self.behave_check = check;
214 }
215 self.block = self.block.map(|v| v.style(self.style));
216 self
217 }
218
219 #[inline]
221 pub fn style(mut self, style: impl Into<Style>) -> Self {
222 self.style = style.into();
223 self
224 }
225
226 #[inline]
228 pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
229 self.focus_style = Some(style.into());
230 self
231 }
232
233 #[inline]
235 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
236 self.text = text.into();
237 self
238 }
239
240 pub fn checked(mut self, checked: bool) -> Self {
242 self.checked = Some(checked);
243 self
244 }
245
246 pub fn default_(mut self, default: bool) -> Self {
248 self.default = Some(default);
249 self
250 }
251
252 #[inline]
254 pub fn block(mut self, block: Block<'a>) -> Self {
255 self.block = Some(block);
256 self.block = self.block.map(|v| v.style(self.style));
257 self
258 }
259
260 pub fn true_str(mut self, str: Span<'a>) -> Self {
262 self.true_str = str;
263 self
264 }
265
266 pub fn false_str(mut self, str: Span<'a>) -> Self {
268 self.false_str = str;
269 self
270 }
271
272 pub fn behave_check(mut self, check: CheckboxCheck) -> Self {
274 self.behave_check = check;
275 self
276 }
277
278 fn check_len(&self) -> u16 {
280 max(
281 self.true_str.content.graphemes(true).count(),
282 self.false_str.content.graphemes(true).count(),
283 ) as u16
284 }
285
286 pub fn width(&self) -> u16 {
288 let chk_len = self.check_len();
289 let txt_len = self.text.width() as u16;
290
291 chk_len + 1 + txt_len + block_size(&self.block).width
292 }
293
294 pub fn height(&self) -> u16 {
296 self.text.height() as u16 + block_size(&self.block).height
297 }
298}
299
300impl<'a> StatefulWidget for &Checkbox<'a> {
301 type State = CheckboxState;
302
303 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
304 render_ref(self, area, buf, state);
305 }
306}
307
308impl StatefulWidget for Checkbox<'_> {
309 type State = CheckboxState;
310
311 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
312 render_ref(&self, area, buf, state);
313 }
314}
315
316fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
317 state.area = area;
318 state.inner = widget.block.inner_if_some(area);
319 state.behave_check = widget.behave_check;
320
321 let chk_len = widget.check_len();
322 state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
323 state.text_area = Rect::new(
324 state.inner.x + chk_len + 1,
325 state.inner.y,
326 state.inner.width.saturating_sub(chk_len + 1),
327 state.inner.height,
328 );
329
330 if let Some(checked) = widget.checked {
331 state.checked = checked;
332 }
333 if let Some(default) = widget.default {
334 state.default = default;
335 }
336
337 let style = widget.style;
338 let focus_style = if let Some(focus_style) = widget.focus_style {
339 style.patch(focus_style)
340 } else {
341 revert_style(style)
342 };
343
344 if let Some(block) = &widget.block {
345 block.render(area, buf);
346 if state.focus.get() {
347 buf.set_style(state.inner, focus_style);
348 }
349 } else {
350 if state.focus.get() {
351 buf.set_style(state.inner, focus_style);
352 } else {
353 buf.set_style(state.inner, widget.style);
354 }
355 }
356
357 let cc = if state.checked {
358 &widget.true_str
359 } else {
360 &widget.false_str
361 };
362 cc.render(state.check_area, buf);
363 (&widget.text).render(state.text_area, buf);
364}
365
366impl Clone for CheckboxState {
367 fn clone(&self) -> Self {
368 Self {
369 area: self.area,
370 inner: self.inner,
371 check_area: self.check_area,
372 text_area: self.text_area,
373 behave_check: self.behave_check,
374 checked: self.checked,
375 default: self.default,
376 focus: self.focus.new_instance(),
377 mouse: Default::default(),
378 non_exhaustive: NonExhaustive,
379 }
380 }
381}
382
383impl Default for CheckboxState {
384 fn default() -> Self {
385 Self {
386 area: Default::default(),
387 inner: Default::default(),
388 check_area: Default::default(),
389 text_area: Default::default(),
390 behave_check: Default::default(),
391 checked: false,
392 default: false,
393 focus: Default::default(),
394 mouse: Default::default(),
395 non_exhaustive: NonExhaustive,
396 }
397 }
398}
399
400impl HasFocus for CheckboxState {
401 fn build(&self, builder: &mut FocusBuilder) {
402 builder.leaf_widget(self);
403 }
404
405 fn focus(&self) -> FocusFlag {
406 self.focus.clone()
407 }
408
409 fn area(&self) -> Rect {
410 self.area
411 }
412}
413
414impl HasScreenCursor for CheckboxState {
415 fn screen_cursor(&self) -> Option<(u16, u16)> {
416 None
417 }
418}
419
420impl RelocatableState for CheckboxState {
421 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
422 self.area.relocate(shift, clip);
423 self.inner.relocate(shift, clip);
424 self.check_area.relocate(shift, clip);
425 }
426}
427
428impl CheckboxState {
429 pub fn new() -> Self {
430 Self::default()
431 }
432
433 pub fn named(name: &str) -> Self {
434 let mut z = Self::default();
435 z.focus = z.focus.with_name(name);
436 z
437 }
438
439 pub fn checked(&self) -> bool {
441 self.checked
442 }
443
444 pub fn set_checked(&mut self, checked: bool) -> bool {
446 let old_value = self.checked;
447 self.checked = checked;
448 old_value != self.checked
449 }
450
451 pub fn default_(&self) -> bool {
453 self.default
454 }
455
456 pub fn set_default(&mut self, default: bool) -> bool {
458 let old_value = self.default;
459 self.default = default;
460 old_value != self.default
461 }
462
463 pub fn value(&self) -> bool {
465 self.checked
466 }
467
468 pub fn set_value(&mut self, checked: bool) -> bool {
470 let old_value = self.checked;
471 self.checked = checked;
472 old_value != self.checked
473 }
474
475 pub fn clear(&mut self) {
477 self.checked = self.default;
478 }
479
480 pub fn flip_checked(&mut self) {
484 self.checked = !self.checked;
485 }
486}
487
488impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
489 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
490 let r = if self.is_focused() {
491 match event {
492 ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
493 self.flip_checked();
494 CheckOutcome::Value
495 }
496 ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
497 self.set_value(self.default);
498 CheckOutcome::Value
499 }
500 _ => CheckOutcome::Continue,
501 }
502 } else {
503 CheckOutcome::Continue
504 };
505
506 if r == CheckOutcome::Continue {
507 HandleEvent::handle(self, event, MouseOnly)
508 } else {
509 r
510 }
511 }
512}
513
514impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
515 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
516 match event {
517 ct_event!(mouse any for m)
518 if self.behave_check == CheckboxCheck::DoubleClick
519 && self.mouse.doubleclick(self.area, m) =>
520 {
521 self.flip_checked();
522 CheckOutcome::Value
523 }
524 ct_event!(mouse down Left for x,y)
525 if self.behave_check == CheckboxCheck::SingleClick
526 && self.area.contains((*x, *y).into()) =>
527 {
528 self.flip_checked();
529 CheckOutcome::Value
530 }
531 _ => CheckOutcome::Continue,
532 }
533 }
534}
535
536pub fn handle_events(
540 state: &mut CheckboxState,
541 focus: bool,
542 event: &crossterm::event::Event,
543) -> CheckOutcome {
544 state.focus.set(focus);
545 HandleEvent::handle(state, event, Regular)
546}
547
548pub fn handle_mouse_events(
550 state: &mut CheckboxState,
551 event: &crossterm::event::Event,
552) -> CheckOutcome {
553 HandleEvent::handle(state, event, MouseOnly)
554}