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::event::{ReadOnly, TextOutcome};
7use crate::text_input_mask::{MaskedInput, MaskedInputState};
8use crate::{
9    TextFocusGained, TextFocusLost, TextStyle, TextTab, derive_text_widget,
10    derive_text_widget_state,
11};
12use chrono::NaiveDate;
13use chrono::format::{Fixed, Item, Numeric, Pad, StrftimeItems};
14use rat_event::{HandleEvent, MouseOnly, Regular};
15use ratatui::buffer::Buffer;
16use ratatui::layout::Rect;
17use ratatui::style::Style;
18use ratatui::widgets::Block;
19use ratatui::widgets::StatefulWidget;
20use std::fmt;
21use unicode_segmentation::UnicodeSegmentation;
22
23/// Widget for dates.
24///
25/// # Stateful
26/// This widget implements [`StatefulWidget`], you can use it with
27/// [`DateInputState`] to handle common actions.
28#[derive(Debug, Default, Clone)]
29pub struct DateInput<'a> {
30    widget: MaskedInput<'a>,
31}
32
33/// State & event-handling.
34/// Use [DateInputState::with_pattern] to set the date pattern.
35#[derive(Debug, Clone)]
36pub struct DateInputState {
37    /// Area of the widget
38    /// __read only__ renewed with each render.
39    pub area: Rect,
40    /// Area inside the block.
41    /// __read only__ renewed with each render.
42    pub inner: Rect,
43    /// Uses MaskedInputState for the actual functionality.
44    pub widget: MaskedInputState,
45    /// The chrono format pattern.
46    pattern: String,
47    /// Locale
48    locale: chrono::Locale,
49
50    pub non_exhaustive: NonExhaustive,
51}
52
53derive_text_widget!(DateInput<'a>);
54
55impl<'a> DateInput<'a> {
56    pub fn new() -> Self {
57        Self::default()
58    }
59
60    /// Show the compact form, if the focus is not with this widget.
61    #[inline]
62    pub fn compact(mut self, compact: bool) -> Self {
63        self.widget = self.widget.compact(compact);
64        self
65    }
66
67    /// Preferred width.
68    pub fn width(&self, state: &DateInputState) -> u16 {
69        state.widget.mask.len() as u16 + 1
70    }
71
72    /// Preferred width.
73    pub fn height(&self) -> u16 {
74        1
75    }
76}
77
78impl<'a> StatefulWidget for &DateInput<'a> {
79    type State = DateInputState;
80
81    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
82        (&self.widget).render(area, buf, &mut state.widget);
83
84        state.area = state.widget.area;
85        state.inner = state.widget.inner;
86    }
87}
88
89impl StatefulWidget for DateInput<'_> {
90    type State = DateInputState;
91
92    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
93        self.widget.render(area, buf, &mut state.widget);
94
95        state.area = state.widget.area;
96        state.inner = state.widget.inner;
97    }
98}
99
100derive_text_widget_state!(DateInputState);
101
102impl Default for DateInputState {
103    fn default() -> Self {
104        Self {
105            area: Default::default(),
106            inner: Default::default(),
107            widget: Default::default(),
108            pattern: Default::default(),
109            locale: Default::default(),
110            non_exhaustive: NonExhaustive,
111        }
112    }
113}
114
115impl DateInputState {
116    /// New state.
117    pub fn new() -> Self {
118        Self::default()
119    }
120
121    pub fn named(name: &str) -> Self {
122        let mut z = Self::default();
123        z.widget.focus = z.widget.focus.with_name(name);
124        z
125    }
126
127    /// New state with a chrono date pattern.
128    pub fn with_pattern<S: AsRef<str>>(mut self, pattern: S) -> Result<Self, fmt::Error> {
129        self.set_format(pattern)?;
130        Ok(self)
131    }
132
133    /// New state with a localized chrono date pattern.
134    #[inline]
135    pub fn with_loc_pattern<S: AsRef<str>>(
136        mut self,
137        pattern: S,
138        locale: chrono::Locale,
139    ) -> Result<Self, fmt::Error> {
140        self.set_format_loc(pattern, locale)?;
141        Ok(self)
142    }
143
144    /// chrono format string.
145    #[inline]
146    pub fn format(&self) -> &str {
147        self.pattern.as_str()
148    }
149
150    /// chrono locale.
151    #[inline]
152    pub fn locale(&self) -> chrono::Locale {
153        self.locale
154    }
155
156    /// chrono format string.
157    ///
158    /// generates a mask according to the format and overwrites whatever
159    /// set_mask() did.
160    #[inline]
161    pub fn set_format<S: AsRef<str>>(&mut self, pattern: S) -> Result<(), fmt::Error> {
162        self.set_format_loc(pattern, chrono::Locale::default())
163    }
164
165    /// chrono format string.
166    ///
167    /// generates a mask according to the format and overwrites whatever
168    /// set_mask() did.
169    #[inline]
170    pub fn set_format_loc<S: AsRef<str>>(
171        &mut self,
172        pattern: S,
173        locale: chrono::Locale,
174    ) -> Result<(), fmt::Error> {
175        let mut mask = String::new();
176        let items = StrftimeItems::new_with_locale(pattern.as_ref(), locale)
177            .parse()
178            .map_err(|_| fmt::Error)?;
179        for t in &items {
180            match t {
181                Item::Literal(s) => {
182                    for c in s.graphemes(true) {
183                        mask.push('\\');
184                        mask.push_str(c);
185                    }
186                }
187                Item::OwnedLiteral(s) => {
188                    for c in s.graphemes(true) {
189                        mask.push('\\');
190                        mask.push_str(c);
191                    }
192                }
193                Item::Space(s) => {
194                    for c in s.graphemes(true) {
195                        mask.push_str(c);
196                    }
197                }
198                Item::OwnedSpace(s) => {
199                    for c in s.graphemes(true) {
200                        mask.push_str(c);
201                    }
202                }
203                Item::Numeric(v, Pad::None | Pad::Space) => match v {
204                    Numeric::Year | Numeric::IsoYear => mask.push_str("9999"),
205                    Numeric::YearDiv100
206                    | Numeric::YearMod100
207                    | Numeric::IsoYearDiv100
208                    | Numeric::IsoYearMod100
209                    | Numeric::Month
210                    | Numeric::Day
211                    | Numeric::WeekFromSun
212                    | Numeric::WeekFromMon
213                    | Numeric::IsoWeek
214                    | Numeric::Hour
215                    | Numeric::Hour12
216                    | Numeric::Minute
217                    | Numeric::Second => mask.push_str("99"),
218                    Numeric::NumDaysFromSun | Numeric::WeekdayFromMon => mask.push('9'),
219                    Numeric::Ordinal => mask.push_str("999"),
220                    Numeric::Nanosecond => mask.push_str("999999999"),
221                    Numeric::Timestamp => mask.push_str("###########"),
222                    _ => return Err(fmt::Error),
223                },
224                Item::Numeric(v, Pad::Zero) => match v {
225                    Numeric::Year | Numeric::IsoYear => mask.push_str("0000"),
226                    Numeric::YearDiv100
227                    | Numeric::YearMod100
228                    | Numeric::IsoYearDiv100
229                    | Numeric::IsoYearMod100
230                    | Numeric::Month
231                    | Numeric::Day
232                    | Numeric::WeekFromSun
233                    | Numeric::WeekFromMon
234                    | Numeric::IsoWeek
235                    | Numeric::Hour
236                    | Numeric::Hour12
237                    | Numeric::Minute
238                    | Numeric::Second => mask.push_str("00"),
239                    Numeric::NumDaysFromSun | Numeric::WeekdayFromMon => mask.push('0'),
240                    Numeric::Ordinal => mask.push_str("000"),
241                    Numeric::Nanosecond => mask.push_str("000000000"),
242                    Numeric::Timestamp => mask.push_str("#0000000000"),
243                    _ => return Err(fmt::Error),
244                },
245                Item::Fixed(v) => match v {
246                    Fixed::ShortMonthName => mask.push_str("___"),
247                    Fixed::LongMonthName => mask.push_str("_________"),
248                    Fixed::ShortWeekdayName => mask.push_str("___"),
249                    Fixed::LongWeekdayName => mask.push_str("________"),
250                    Fixed::LowerAmPm => mask.push_str("__"),
251                    Fixed::UpperAmPm => mask.push_str("__"),
252                    Fixed::Nanosecond => mask.push_str(".#########"),
253                    Fixed::Nanosecond3 => mask.push_str(".###"),
254                    Fixed::Nanosecond6 => mask.push_str(".######"),
255                    Fixed::Nanosecond9 => mask.push_str(".#########"),
256                    Fixed::TimezoneName => mask.push_str("__________"),
257                    Fixed::TimezoneOffsetColon | Fixed::TimezoneOffset => mask.push_str("+##:##"),
258                    Fixed::TimezoneOffsetDoubleColon => mask.push_str("+##:##:##"),
259                    Fixed::TimezoneOffsetTripleColon => mask.push_str("+##"),
260                    Fixed::TimezoneOffsetColonZ | Fixed::TimezoneOffsetZ => return Err(fmt::Error),
261                    Fixed::RFC2822 => {
262                        // 01 Jun 2016 14:31:46 -0700
263                        return Err(fmt::Error);
264                    }
265                    Fixed::RFC3339 => {
266                        // not supported, for now
267                        return Err(fmt::Error);
268                    }
269                    _ => return Err(fmt::Error),
270                },
271                Item::Error => return Err(fmt::Error),
272            }
273        }
274
275        self.locale = locale;
276        self.pattern = pattern.as_ref().to_string();
277        self.widget.set_mask(mask)?;
278        Ok(())
279    }
280}
281
282impl DateInputState {
283    /// Reset to empty.
284    #[inline]
285    pub fn clear(&mut self) {
286        self.widget.clear();
287    }
288
289    /// Set the date value.
290    #[inline]
291    pub fn set_value(&mut self, date: NaiveDate) {
292        let v = date.format(self.pattern.as_str()).to_string();
293        self.widget.set_text(v);
294    }
295
296    /// Parses the text according to the given pattern.
297    #[inline]
298    pub fn value(&self) -> Result<NaiveDate, chrono::ParseError> {
299        NaiveDate::parse_from_str(self.widget.text(), self.pattern.as_str())
300    }
301}
302
303impl HandleEvent<crossterm::event::Event, Regular, TextOutcome> for DateInputState {
304    fn handle(&mut self, event: &crossterm::event::Event, _keymap: Regular) -> TextOutcome {
305        self.widget.handle(event, Regular)
306    }
307}
308
309impl HandleEvent<crossterm::event::Event, ReadOnly, TextOutcome> for DateInputState {
310    fn handle(&mut self, event: &crossterm::event::Event, _keymap: ReadOnly) -> TextOutcome {
311        self.widget.handle(event, ReadOnly)
312    }
313}
314
315impl HandleEvent<crossterm::event::Event, MouseOnly, TextOutcome> for DateInputState {
316    fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> TextOutcome {
317        self.widget.handle(event, MouseOnly)
318    }
319}
320
321/// Handle all events.
322/// Text events are only processed if focus is true.
323/// Mouse events are processed if they are in range.
324pub fn handle_events(
325    state: &mut DateInputState,
326    focus: bool,
327    event: &crossterm::event::Event,
328) -> TextOutcome {
329    state.widget.focus.set(focus);
330    HandleEvent::handle(state, event, Regular)
331}
332
333/// Handle only navigation events.
334/// Text events are only processed if focus is true.
335/// Mouse events are processed if they are in range.
336pub fn handle_readonly_events(
337    state: &mut DateInputState,
338    focus: bool,
339    event: &crossterm::event::Event,
340) -> TextOutcome {
341    state.widget.focus.set(focus);
342    state.handle(event, ReadOnly)
343}
344
345/// Handle only mouse-events.
346pub fn handle_mouse_events(
347    state: &mut DateInputState,
348    event: &crossterm::event::Event,
349) -> TextOutcome {
350    HandleEvent::handle(state, event, MouseOnly)
351}