rat_text/
date_input.rs

1//!
2//! Date-input widget using [chrono](https://docs.rs/chrono/latest/chrono/)
3//!
4
5use crate::_private::NonExhaustive;
6use crate::clipboard::Clipboard;
7use crate::event::{ReadOnly, TextOutcome};
8use crate::text_input_mask::{MaskedInput, MaskedInputState};
9use crate::undo_buffer::{UndoBuffer, UndoEntry};
10use crate::{HasScreenCursor, TextError, TextFocusGained, TextFocusLost, TextStyle, upos_type};
11use chrono::NaiveDate;
12use chrono::format::{Fixed, Item, Numeric, Pad, StrftimeItems};
13use rat_event::{HandleEvent, MouseOnly, Regular};
14use rat_focus::{FocusBuilder, FocusFlag, HasFocus, Navigation};
15use rat_reloc::RelocatableState;
16use ratatui::buffer::Buffer;
17use ratatui::layout::Rect;
18use ratatui::style::Style;
19use ratatui::widgets::Block;
20use ratatui::widgets::StatefulWidget;
21use std::fmt;
22use std::ops::Range;
23use unicode_segmentation::UnicodeSegmentation;
24
25/// Widget for dates.
26///
27/// # Stateful
28/// This widget implements [`StatefulWidget`], you can use it with
29/// [`DateInputState`] to handle common actions.
30#[derive(Debug, Default, Clone)]
31pub struct DateInput<'a> {
32    widget: MaskedInput<'a>,
33}
34
35/// State & event-handling.
36/// Use [DateInputState::with_pattern] to set the date pattern.
37#[derive(Debug, Clone)]
38pub struct DateInputState {
39    /// Uses MaskedInputState for the actual functionality.
40    pub widget: MaskedInputState,
41    /// The chrono format pattern.
42    pattern: String,
43    /// Locale
44    locale: chrono::Locale,
45
46    pub non_exhaustive: NonExhaustive,
47}
48
49impl<'a> DateInput<'a> {
50    pub fn new() -> Self {
51        Self::default()
52    }
53
54    /// Show the compact form, if the focus is not with this widget.
55    #[inline]
56    pub fn compact(mut self, compact: bool) -> Self {
57        self.widget = self.widget.compact(compact);
58        self
59    }
60
61    /// Set the combined style.
62    #[inline]
63    pub fn styles(mut self, style: TextStyle) -> Self {
64        self.widget = self.widget.styles(style);
65        self
66    }
67
68    /// Base text style.
69    #[inline]
70    pub fn style(mut self, style: impl Into<Style>) -> Self {
71        self.widget = self.widget.style(style);
72        self
73    }
74
75    /// Style when focused.
76    #[inline]
77    pub fn focus_style(mut self, style: impl Into<Style>) -> Self {
78        self.widget = self.widget.focus_style(style);
79        self
80    }
81
82    /// Style for selection
83    #[inline]
84    pub fn select_style(mut self, style: impl Into<Style>) -> Self {
85        self.widget = self.widget.select_style(style);
86        self
87    }
88
89    /// Style for the invalid indicator.
90    #[inline]
91    pub fn invalid_style(mut self, style: impl Into<Style>) -> Self {
92        self.widget = self.widget.invalid_style(style);
93        self
94    }
95
96    /// Block
97    #[inline]
98    pub fn block(mut self, block: Block<'a>) -> Self {
99        self.widget = self.widget.block(block);
100        self
101    }
102
103    /// Focus behaviour
104    #[inline]
105    pub fn on_focus_gained(mut self, of: TextFocusGained) -> Self {
106        self.widget = self.widget.on_focus_gained(of);
107        self
108    }
109
110    /// Focus behaviour
111    #[inline]
112    pub fn on_focus_lost(mut self, of: TextFocusLost) -> Self {
113        self.widget = self.widget.on_focus_lost(of);
114        self
115    }
116}
117
118impl<'a> StatefulWidget for &DateInput<'a> {
119    type State = DateInputState;
120
121    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
122        (&self.widget).render(area, buf, &mut state.widget);
123    }
124}
125
126impl StatefulWidget for DateInput<'_> {
127    type State = DateInputState;
128
129    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
130        self.widget.render(area, buf, &mut state.widget);
131    }
132}
133
134impl Default for DateInputState {
135    fn default() -> Self {
136        Self {
137            widget: Default::default(),
138            pattern: Default::default(),
139            locale: Default::default(),
140            non_exhaustive: NonExhaustive,
141        }
142    }
143}
144
145impl HasFocus for DateInputState {
146    fn build(&self, builder: &mut FocusBuilder) {
147        builder.leaf_widget(self);
148    }
149
150    #[inline]
151    fn focus(&self) -> FocusFlag {
152        self.widget.focus.clone()
153    }
154
155    #[inline]
156    fn area(&self) -> Rect {
157        self.widget.area
158    }
159
160    #[inline]
161    fn navigable(&self) -> Navigation {
162        self.widget.navigable()
163    }
164}
165
166impl DateInputState {
167    /// New state.
168    pub fn new() -> Self {
169        Self::default()
170    }
171
172    pub fn named(name: &str) -> Self {
173        Self {
174            widget: MaskedInputState::named(name),
175            ..Default::default()
176        }
177    }
178
179    /// New state with a chrono date pattern.
180    pub fn with_pattern<S: AsRef<str>>(mut self, pattern: S) -> Result<Self, fmt::Error> {
181        self.set_format(pattern)?;
182        Ok(self)
183    }
184
185    /// New state with a localized chrono date pattern.
186    #[inline]
187    pub fn with_loc_pattern<S: AsRef<str>>(
188        mut self,
189        pattern: S,
190        locale: chrono::Locale,
191    ) -> Result<Self, fmt::Error> {
192        self.set_format_loc(pattern, locale)?;
193        Ok(self)
194    }
195
196    /// chrono format string.
197    #[inline]
198    pub fn format(&self) -> &str {
199        self.pattern.as_str()
200    }
201
202    /// chrono locale.
203    #[inline]
204    pub fn locale(&self) -> chrono::Locale {
205        self.locale
206    }
207
208    /// chrono format string.
209    ///
210    /// generates a mask according to the format and overwrites whatever
211    /// set_mask() did.
212    #[inline]
213    pub fn set_format<S: AsRef<str>>(&mut self, pattern: S) -> Result<(), fmt::Error> {
214        self.set_format_loc(pattern, chrono::Locale::default())
215    }
216
217    /// chrono format string.
218    ///
219    /// generates a mask according to the format and overwrites whatever
220    /// set_mask() did.
221    #[inline]
222    pub fn set_format_loc<S: AsRef<str>>(
223        &mut self,
224        pattern: S,
225        locale: chrono::Locale,
226    ) -> Result<(), fmt::Error> {
227        let mut mask = String::new();
228        let items = StrftimeItems::new_with_locale(pattern.as_ref(), locale)
229            .parse()
230            .map_err(|_| fmt::Error)?;
231        for t in &items {
232            match t {
233                Item::Literal(s) => {
234                    for c in s.graphemes(true) {
235                        mask.push('\\');
236                        mask.push_str(c);
237                    }
238                }
239                Item::OwnedLiteral(s) => {
240                    for c in s.graphemes(true) {
241                        mask.push('\\');
242                        mask.push_str(c);
243                    }
244                }
245                Item::Space(s) => {
246                    for c in s.graphemes(true) {
247                        mask.push_str(c);
248                    }
249                }
250                Item::OwnedSpace(s) => {
251                    for c in s.graphemes(true) {
252                        mask.push_str(c);
253                    }
254                }
255                Item::Numeric(v, Pad::None | Pad::Space) => match v {
256                    Numeric::Year | Numeric::IsoYear => mask.push_str("9999"),
257                    Numeric::YearDiv100
258                    | Numeric::YearMod100
259                    | Numeric::IsoYearDiv100
260                    | Numeric::IsoYearMod100
261                    | Numeric::Month
262                    | Numeric::Day
263                    | Numeric::WeekFromSun
264                    | Numeric::WeekFromMon
265                    | Numeric::IsoWeek
266                    | Numeric::Hour
267                    | Numeric::Hour12
268                    | Numeric::Minute
269                    | Numeric::Second => mask.push_str("99"),
270                    Numeric::NumDaysFromSun | Numeric::WeekdayFromMon => mask.push('9'),
271                    Numeric::Ordinal => mask.push_str("999"),
272                    Numeric::Nanosecond => mask.push_str("999999999"),
273                    Numeric::Timestamp => mask.push_str("###########"),
274                    _ => return Err(fmt::Error),
275                },
276                Item::Numeric(v, Pad::Zero) => match v {
277                    Numeric::Year | Numeric::IsoYear => mask.push_str("0000"),
278                    Numeric::YearDiv100
279                    | Numeric::YearMod100
280                    | Numeric::IsoYearDiv100
281                    | Numeric::IsoYearMod100
282                    | Numeric::Month
283                    | Numeric::Day
284                    | Numeric::WeekFromSun
285                    | Numeric::WeekFromMon
286                    | Numeric::IsoWeek
287                    | Numeric::Hour
288                    | Numeric::Hour12
289                    | Numeric::Minute
290                    | Numeric::Second => mask.push_str("00"),
291                    Numeric::NumDaysFromSun | Numeric::WeekdayFromMon => mask.push('0'),
292                    Numeric::Ordinal => mask.push_str("000"),
293                    Numeric::Nanosecond => mask.push_str("000000000"),
294                    Numeric::Timestamp => mask.push_str("#0000000000"),
295                    _ => return Err(fmt::Error),
296                },
297                Item::Fixed(v) => match v {
298                    Fixed::ShortMonthName => mask.push_str("___"),
299                    Fixed::LongMonthName => mask.push_str("_________"),
300                    Fixed::ShortWeekdayName => mask.push_str("___"),
301                    Fixed::LongWeekdayName => mask.push_str("________"),
302                    Fixed::LowerAmPm => mask.push_str("__"),
303                    Fixed::UpperAmPm => mask.push_str("__"),
304                    Fixed::Nanosecond => mask.push_str(".#########"),
305                    Fixed::Nanosecond3 => mask.push_str(".###"),
306                    Fixed::Nanosecond6 => mask.push_str(".######"),
307                    Fixed::Nanosecond9 => mask.push_str(".#########"),
308                    Fixed::TimezoneName => mask.push_str("__________"),
309                    Fixed::TimezoneOffsetColon | Fixed::TimezoneOffset => mask.push_str("+##:##"),
310                    Fixed::TimezoneOffsetDoubleColon => mask.push_str("+##:##:##"),
311                    Fixed::TimezoneOffsetTripleColon => mask.push_str("+##"),
312                    Fixed::TimezoneOffsetColonZ | Fixed::TimezoneOffsetZ => return Err(fmt::Error),
313                    Fixed::RFC2822 => {
314                        // 01 Jun 2016 14:31:46 -0700
315                        return Err(fmt::Error);
316                    }
317                    Fixed::RFC3339 => {
318                        // not supported, for now
319                        return Err(fmt::Error);
320                    }
321                    _ => return Err(fmt::Error),
322                },
323                Item::Error => return Err(fmt::Error),
324            }
325        }
326
327        self.locale = locale;
328        self.pattern = pattern.as_ref().to_string();
329        self.widget.set_mask(mask)?;
330        Ok(())
331    }
332
333    /// Renders the widget in invalid style.
334    #[inline]
335    pub fn set_invalid(&mut self, invalid: bool) {
336        self.widget.invalid = invalid;
337    }
338
339    /// Renders the widget in invalid style.
340    #[inline]
341    pub fn get_invalid(&self) -> bool {
342        self.widget.invalid
343    }
344
345    /// The next edit operation will overwrite the current content
346    /// instead of adding text. Any move operations will cancel
347    /// this overwrite.
348    #[inline]
349    pub fn set_overwrite(&mut self, overwrite: bool) {
350        self.widget.set_overwrite(overwrite);
351    }
352
353    /// Will the next edit operation overwrite the content?
354    #[inline]
355    pub fn overwrite(&self) -> bool {
356        self.widget.overwrite()
357    }
358}
359
360impl DateInputState {
361    /// Clipboard used.
362    /// Default is to use the [global_clipboard](crate::clipboard::global_clipboard).
363    #[inline]
364    pub fn set_clipboard(&mut self, clip: Option<impl Clipboard + 'static>) {
365        self.widget.set_clipboard(clip);
366    }
367
368    /// Clipboard used.
369    /// Default is to use the [global_clipboard](crate::clipboard::global_clipboard).
370    #[inline]
371    pub fn clipboard(&self) -> Option<&dyn Clipboard> {
372        self.widget.clipboard()
373    }
374
375    /// Copy to clipboard
376    #[inline]
377    pub fn copy_to_clip(&mut self) -> bool {
378        self.widget.copy_to_clip()
379    }
380
381    /// Cut to clipboard
382    #[inline]
383    pub fn cut_to_clip(&mut self) -> bool {
384        self.widget.cut_to_clip()
385    }
386
387    /// Paste from clipboard.
388    #[inline]
389    pub fn paste_from_clip(&mut self) -> bool {
390        self.widget.paste_from_clip()
391    }
392}
393
394impl DateInputState {
395    /// Set undo buffer.
396    #[inline]
397    pub fn set_undo_buffer(&mut self, undo: Option<impl UndoBuffer + 'static>) {
398        self.widget.set_undo_buffer(undo);
399    }
400
401    /// Undo
402    #[inline]
403    pub fn undo_buffer(&self) -> Option<&dyn UndoBuffer> {
404        self.widget.undo_buffer()
405    }
406
407    /// Undo
408    #[inline]
409    pub fn undo_buffer_mut(&mut self) -> Option<&mut dyn UndoBuffer> {
410        self.widget.undo_buffer_mut()
411    }
412
413    /// Get all recent replay recordings.
414    #[inline]
415    pub fn recent_replay_log(&mut self) -> Vec<UndoEntry> {
416        self.widget.recent_replay_log()
417    }
418
419    /// Apply the replay recording.
420    #[inline]
421    pub fn replay_log(&mut self, replay: &[UndoEntry]) {
422        self.widget.replay_log(replay)
423    }
424
425    /// Undo operation
426    #[inline]
427    pub fn undo(&mut self) -> bool {
428        self.widget.undo()
429    }
430
431    /// Redo operation
432    #[inline]
433    pub fn redo(&mut self) -> bool {
434        self.widget.redo()
435    }
436}
437
438impl DateInputState {
439    /// Set and replace all styles.
440    #[inline]
441    pub fn set_styles(&mut self, styles: Vec<(Range<usize>, usize)>) {
442        self.widget.set_styles(styles);
443    }
444
445    /// Add a style for a byte-range.
446    #[inline]
447    pub fn add_style(&mut self, range: Range<usize>, style: usize) {
448        self.widget.add_style(range, style);
449    }
450
451    /// Add a style for a `Range<upos_type>` .
452    /// The style-nr refers to one of the styles set with the widget.
453    #[inline]
454    pub fn add_range_style(
455        &mut self,
456        range: Range<upos_type>,
457        style: usize,
458    ) -> Result<(), TextError> {
459        self.widget.add_range_style(range, style)
460    }
461
462    /// Remove the exact TextRange and style.
463    #[inline]
464    pub fn remove_style(&mut self, range: Range<usize>, style: usize) {
465        self.widget.remove_style(range, style);
466    }
467
468    /// Remove the exact `Range<upos_type>` and style.
469    #[inline]
470    pub fn remove_range_style(
471        &mut self,
472        range: Range<upos_type>,
473        style: usize,
474    ) -> Result<(), TextError> {
475        self.widget.remove_range_style(range, style)
476    }
477
478    /// Find all styles that touch the given range.
479    pub fn styles_in(&self, range: Range<usize>, buf: &mut Vec<(Range<usize>, usize)>) {
480        self.widget.styles_in(range, buf)
481    }
482
483    /// All styles active at the given position.
484    #[inline]
485    pub fn styles_at(&self, byte_pos: usize, buf: &mut Vec<(Range<usize>, usize)>) {
486        self.widget.styles_at(byte_pos, buf)
487    }
488
489    /// Check if the given style applies at the position and
490    /// return the complete range for the style.
491    #[inline]
492    pub fn style_match(&self, byte_pos: usize, style: usize) -> Option<Range<usize>> {
493        self.widget.styles_at_match(byte_pos, style)
494    }
495
496    /// List of all styles.
497    #[inline]
498    pub fn styles(&self) -> Option<impl Iterator<Item = (Range<usize>, usize)> + '_> {
499        self.widget.styles()
500    }
501}
502
503impl DateInputState {
504    /// Offset shown.
505    #[inline]
506    pub fn offset(&self) -> upos_type {
507        self.widget.offset()
508    }
509
510    /// Offset shown. This is corrected if the cursor wouldn't be visible.
511    #[inline]
512    pub fn set_offset(&mut self, offset: upos_type) {
513        self.widget.set_offset(offset)
514    }
515
516    /// Cursor position
517    #[inline]
518    pub fn cursor(&self) -> upos_type {
519        self.widget.cursor()
520    }
521
522    /// Set the cursor position, reset selection.
523    #[inline]
524    pub fn set_cursor(&mut self, cursor: upos_type, extend_selection: bool) -> bool {
525        self.widget.set_cursor(cursor, extend_selection)
526    }
527
528    /// Place cursor at some sensible position according to the mask.
529    #[inline]
530    pub fn set_default_cursor(&mut self) {
531        self.widget.set_default_cursor()
532    }
533
534    /// Selection anchor.
535    #[inline]
536    pub fn anchor(&self) -> upos_type {
537        self.widget.anchor()
538    }
539
540    /// Selection
541    #[inline]
542    pub fn has_selection(&self) -> bool {
543        self.widget.has_selection()
544    }
545
546    /// Selection
547    #[inline]
548    pub fn selection(&self) -> Range<upos_type> {
549        self.widget.selection()
550    }
551
552    /// Selection
553    #[inline]
554    pub fn set_selection(&mut self, anchor: upos_type, cursor: upos_type) -> bool {
555        self.widget.set_selection(anchor, cursor)
556    }
557
558    /// Select all text.
559    #[inline]
560    pub fn select_all(&mut self) {
561        self.widget.select_all();
562    }
563
564    /// Selection
565    #[inline]
566    pub fn selected_text(&self) -> &str {
567        self.widget.selected_text()
568    }
569}
570
571impl DateInputState {
572    /// Empty
573    #[inline]
574    pub fn is_empty(&self) -> bool {
575        self.widget.is_empty()
576    }
577
578    /// Parses the text according to the given pattern.
579    #[inline]
580    pub fn value(&self) -> Result<NaiveDate, chrono::ParseError> {
581        NaiveDate::parse_from_str(self.widget.text(), self.pattern.as_str())
582    }
583
584    /// Length in grapheme count.
585    #[inline]
586    pub fn len(&self) -> upos_type {
587        self.widget.len()
588    }
589
590    /// Length as grapheme count.
591    #[inline]
592    pub fn line_width(&self) -> upos_type {
593        self.widget.line_width()
594    }
595}
596
597impl DateInputState {
598    /// Reset to empty.
599    #[inline]
600    pub fn clear(&mut self) {
601        self.widget.clear();
602    }
603
604    /// Set the date value.
605    #[inline]
606    pub fn set_value(&mut self, date: NaiveDate) {
607        let v = date.format(self.pattern.as_str()).to_string();
608        self.widget.set_text(v);
609    }
610
611    /// Insert a char at the current position.
612    #[inline]
613    pub fn insert_char(&mut self, c: char) -> bool {
614        self.widget.insert_char(c)
615    }
616
617    /// Remove the selected range. The text will be replaced with the default value
618    /// as defined by the mask.
619    #[inline]
620    pub fn delete_range(&mut self, range: Range<upos_type>) -> bool {
621        self.widget.delete_range(range)
622    }
623
624    /// Remove the selected range. The text will be replaced with the default value
625    /// as defined by the mask.
626    #[inline]
627    pub fn try_delete_range(&mut self, range: Range<upos_type>) -> Result<bool, TextError> {
628        self.widget.try_delete_range(range)
629    }
630}
631
632impl DateInputState {
633    /// Delete the char after the cursor.
634    #[inline]
635    pub fn delete_next_char(&mut self) -> bool {
636        self.widget.delete_next_char()
637    }
638
639    /// Delete the char before the cursor.
640    #[inline]
641    pub fn delete_prev_char(&mut self) -> bool {
642        self.widget.delete_prev_char()
643    }
644
645    /// Move to the next char.
646    #[inline]
647    pub fn move_right(&mut self, extend_selection: bool) -> bool {
648        self.widget.move_right(extend_selection)
649    }
650
651    /// Move to the previous char.
652    #[inline]
653    pub fn move_left(&mut self, extend_selection: bool) -> bool {
654        self.widget.move_left(extend_selection)
655    }
656
657    /// Start of line
658    #[inline]
659    pub fn move_to_line_start(&mut self, extend_selection: bool) -> bool {
660        self.widget.move_to_line_start(extend_selection)
661    }
662
663    /// End of line
664    #[inline]
665    pub fn move_to_line_end(&mut self, extend_selection: bool) -> bool {
666        self.widget.move_to_line_end(extend_selection)
667    }
668}
669
670impl HasScreenCursor for DateInputState {
671    /// The current text cursor as an absolute screen position.
672    #[inline]
673    fn screen_cursor(&self) -> Option<(u16, u16)> {
674        self.widget.screen_cursor()
675    }
676}
677
678impl RelocatableState for DateInputState {
679    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
680        self.widget.relocate(shift, clip);
681    }
682}
683
684impl DateInputState {
685    /// Converts a grapheme based position to a screen position
686    /// relative to the widget area.
687    #[inline]
688    pub fn col_to_screen(&self, pos: upos_type) -> Option<u16> {
689        self.widget.col_to_screen(pos)
690    }
691
692    /// Converts from a widget relative screen coordinate to a grapheme index.
693    /// x is the relative screen position.
694    #[inline]
695    pub fn screen_to_col(&self, scx: i16) -> upos_type {
696        self.widget.screen_to_col(scx)
697    }
698
699    /// Set the cursor position from a screen position relative to the origin
700    /// of the widget. This value can be negative, which selects a currently
701    /// not visible position and scrolls to it.
702    #[inline]
703    pub fn set_screen_cursor(&mut self, cursor: i16, extend_selection: bool) -> bool {
704        self.widget.set_screen_cursor(cursor, extend_selection)
705    }
706}
707
708impl HandleEvent<crossterm::event::Event, Regular, TextOutcome> for DateInputState {
709    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> TextOutcome {
710        self.widget.handle(event, Regular)
711    }
712}
713
714impl HandleEvent<crossterm::event::Event, ReadOnly, TextOutcome> for DateInputState {
715    fn handle(&mut self, event: &crossterm::event::Event, _keymap: ReadOnly) -> TextOutcome {
716        self.widget.handle(event, ReadOnly)
717    }
718}
719
720impl HandleEvent<crossterm::event::Event, MouseOnly, TextOutcome> for DateInputState {
721    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> TextOutcome {
722        self.widget.handle(event, MouseOnly)
723    }
724}
725
726/// Handle all events.
727/// Text events are only processed if focus is true.
728/// Mouse events are processed if they are in range.
729pub fn handle_events(
730    state: &mut DateInputState,
731    focus: bool,
732    event: &crossterm::event::Event,
733) -> TextOutcome {
734    state.widget.focus.set(focus);
735    HandleEvent::handle(state, event, Regular)
736}
737
738/// Handle only navigation events.
739/// Text events are only processed if focus is true.
740/// Mouse events are processed if they are in range.
741pub fn handle_readonly_events(
742    state: &mut DateInputState,
743    focus: bool,
744    event: &crossterm::event::Event,
745) -> TextOutcome {
746    state.widget.focus.set(focus);
747    state.handle(event, ReadOnly)
748}
749
750/// Handle only mouse-events.
751pub fn handle_mouse_events(
752    state: &mut DateInputState,
753    event: &crossterm::event::Event,
754) -> TextOutcome {
755    HandleEvent::handle(state, event, MouseOnly)
756}