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, StatefulWidget, Text, Widget};
33use ratatui::style::Style;
34use ratatui::text::Span;
35use ratatui::widgets::Block;
36#[cfg(feature = "unstable-widget-ref")]
37use ratatui::widgets::StatefulWidgetRef;
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
273#[cfg(feature = "unstable-widget-ref")]
274impl<'a> StatefulWidgetRef for Checkbox<'a> {
275 type State = CheckboxState;
276
277 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
278 render_ref(self, area, buf, state);
279 }
280}
281
282impl StatefulWidget for Checkbox<'_> {
283 type State = CheckboxState;
284
285 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
286 render_ref(&self, area, buf, state);
287 }
288}
289
290fn render_ref(widget: &Checkbox<'_>, area: Rect, buf: &mut Buffer, state: &mut CheckboxState) {
291 state.area = area;
292 state.inner = widget.block.inner_if_some(area);
293
294 let chk_len = widget.check_len();
295 state.check_area = Rect::new(state.inner.x, state.inner.y, chk_len, 1);
296 state.text_area = Rect::new(
297 state.inner.x + chk_len + 1,
298 state.inner.y,
299 state.inner.width.saturating_sub(chk_len + 1),
300 state.inner.height,
301 );
302
303 if let Some(checked) = widget.checked {
304 state.checked = checked;
305 }
306 if let Some(default) = widget.default {
307 state.default = default;
308 }
309
310 let style = widget.style;
311 let focus_style = if let Some(focus_style) = widget.focus_style {
312 style.patch(focus_style)
313 } else {
314 revert_style(style)
315 };
316
317 if widget.block.is_some() {
318 widget.block.render(area, buf);
319 if state.focus.get() {
320 buf.set_style(state.inner, focus_style);
321 }
322 } else {
323 if state.focus.get() {
324 buf.set_style(state.inner, focus_style);
325 } else {
326 buf.set_style(state.inner, widget.style);
327 }
328 }
329
330 let cc = if state.checked {
331 &widget.true_str
332 } else {
333 &widget.false_str
334 };
335 cc.render(state.check_area, buf);
336 (&widget.text).render(state.text_area, buf);
337}
338
339impl Clone for CheckboxState {
340 fn clone(&self) -> Self {
341 Self {
342 area: self.area,
343 inner: self.inner,
344 check_area: self.check_area,
345 text_area: self.text_area,
346 checked: self.checked,
347 default: self.default,
348 focus: FocusFlag::named(self.focus.name()),
349 mouse: Default::default(),
350 non_exhaustive: NonExhaustive,
351 }
352 }
353}
354
355impl Default for CheckboxState {
356 fn default() -> Self {
357 Self {
358 area: Default::default(),
359 inner: Default::default(),
360 check_area: Default::default(),
361 text_area: Default::default(),
362 checked: false,
363 default: false,
364 focus: Default::default(),
365 mouse: Default::default(),
366 non_exhaustive: NonExhaustive,
367 }
368 }
369}
370
371impl HasFocus for CheckboxState {
372 fn build(&self, builder: &mut FocusBuilder) {
373 builder.leaf_widget(self);
374 }
375
376 fn focus(&self) -> FocusFlag {
377 self.focus.clone()
378 }
379
380 fn area(&self) -> Rect {
381 self.area
382 }
383}
384
385impl RelocatableState for CheckboxState {
386 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
387 self.area = relocate_area(self.area, shift, clip);
388 self.inner = relocate_area(self.inner, shift, clip);
389 }
390}
391
392impl CheckboxState {
393 pub fn new() -> Self {
394 Self::default()
395 }
396
397 pub fn named(name: &str) -> Self {
398 Self {
399 focus: FocusFlag::named(name),
400 ..Default::default()
401 }
402 }
403
404 pub fn checked(&self) -> bool {
406 self.checked
407 }
408
409 pub fn set_checked(&mut self, checked: bool) -> bool {
411 let old_value = self.checked;
412 self.checked = checked;
413 old_value != self.checked
414 }
415
416 pub fn default_(&self) -> bool {
418 self.default
419 }
420
421 pub fn set_default(&mut self, default: bool) -> bool {
423 let old_value = self.default;
424 self.default = default;
425 old_value != self.default
426 }
427
428 pub fn value(&self) -> bool {
430 self.checked
431 }
432
433 pub fn set_value(&mut self, checked: bool) -> bool {
435 let old_value = self.checked;
436 self.checked = checked;
437 old_value != self.checked
438 }
439
440 pub fn flip_checked(&mut self) {
444 self.checked = !self.checked;
445 }
446}
447
448impl HandleEvent<crossterm::event::Event, Regular, CheckOutcome> for CheckboxState {
449 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CheckOutcome {
450 let r = if self.is_focused() {
451 match event {
452 ct_event!(keycode press Enter) | ct_event!(key press ' ') => {
453 self.flip_checked();
454 CheckOutcome::Value
455 }
456 ct_event!(keycode press Backspace) | ct_event!(keycode press Delete) => {
457 self.set_value(self.default);
458 CheckOutcome::Value
459 }
460 _ => CheckOutcome::Continue,
461 }
462 } else {
463 CheckOutcome::Continue
464 };
465
466 if r == CheckOutcome::Continue {
467 HandleEvent::handle(self, event, MouseOnly)
468 } else {
469 r
470 }
471 }
472}
473
474impl HandleEvent<crossterm::event::Event, MouseOnly, CheckOutcome> for CheckboxState {
475 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> CheckOutcome {
476 match event {
477 ct_event!(mouse any for m) if self.mouse.doubleclick(self.area, m) => {
478 self.flip_checked();
479 CheckOutcome::Value
480 }
481 _ => CheckOutcome::Continue,
482 }
483 }
484}
485
486pub fn handle_events(
490 state: &mut CheckboxState,
491 focus: bool,
492 event: &crossterm::event::Event,
493) -> CheckOutcome {
494 state.focus.set(focus);
495 HandleEvent::handle(state, event, Regular)
496}
497
498pub fn handle_mouse_events(
500 state: &mut CheckboxState,
501 event: &crossterm::event::Event,
502) -> CheckOutcome {
503 HandleEvent::handle(state, event, MouseOnly)
504}