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