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