ez_tui/components/concrete/forms/inputs/
date_input.rs1use crate::components::concrete::forms::inputs::core::{FormFieldValue, InputCpt};
2use crate::components::concrete::forms::inputs::date_buffer::DateBuffer;
3use crate::forms::field::DateTimeConfig;
4use crate::{
5 AttrValue, Attribute, Component, EzArgs, EzCptIds, EzEvent, EzMsg, EzState, FormField,
6 FormFieldType, MockComponent, MockProps, Props, State, Theme,
7};
8use chrono::{DateTime, Duration, Local};
9use crossterm::event::KeyCode;
10use eztui_derive::MockProps;
11use ratatui::buffer::Buffer;
12use ratatui::layout::{Alignment, Layout, Rect};
13use ratatui::prelude::{Color, Line, Stylize, Widget};
14use ratatui::style::Style;
15use ratatui::style::palette::tailwind::GRAY;
16use ratatui::text::Span;
17use ratatui::widgets::calendar::{CalendarEventStore, Monthly};
18use ratatui::widgets::{BorderType, Paragraph};
19use std::fmt::Debug;
20use time::{Date, OffsetDateTime, UtcOffset};
21
22#[derive(Debug, Eq, PartialEq, Clone, Default)]
23pub(crate) enum TimePart {
24 #[default]
25 Hour,
26 Minute,
27 Second,
28 Nanosecond,
29}
30
31impl TimePart {
32 fn next(&self) -> Option<Self> {
33 match self {
34 TimePart::Hour => Some(TimePart::Minute),
35 TimePart::Minute => Some(TimePart::Second),
36 TimePart::Second => Some(TimePart::Nanosecond),
37 TimePart::Nanosecond => None,
38 }
39 }
40}
41#[derive(Debug, MockProps)]
43pub struct DateTimeInputCpt<FID>
44where
45 FID: EzCptIds,
46{
47 props: Props,
48 config: DateTimeConfig,
49 #[allow(dead_code)]
50 field: FormField<FID>,
52 value: DateTime<Local>,
53 selected_time_part: Option<TimePart>,
54 buffer: DateBuffer,
55 local: bool,
56}
57
58impl<FID> DateTimeInputCpt<FID>
59where
60 FID: EzCptIds,
61{
62 #[must_use]
64 pub(crate) fn datetime(field: FormField<FID>) -> Self {
65 assert_eq!(field.field_type(), &FormFieldType::DateTime {});
66 Self::init(field, DateTimeConfig::DateTime)
67 }
68
69 fn init(field: FormField<FID>, config: DateTimeConfig) -> DateTimeInputCpt<FID> {
70 let mut props = Props::default();
71 props.set(
72 Attribute::Title,
73 AttrValue::Title((field.name(), Alignment::Left)),
74 );
75 let buffer = DateBuffer::new(config == DateTimeConfig::Time);
76 let time_part = if config == DateTimeConfig::Time {
77 Some(TimePart::default())
78 } else {
79 None
80 };
81 Self {
82 props,
83 config,
84 field,
85 value: chrono::Utc::now().with_timezone(&Local),
86 buffer,
87 selected_time_part: time_part,
88 local: false,
89 }
90 }
91
92 #[must_use]
94 pub(crate) fn time_only(field: FormField<FID>) -> Self {
95 assert_eq!(field.field_type(), &FormFieldType::Time {});
96 Self::init(field, DateTimeConfig::Time)
97 }
98 #[must_use]
100 pub(crate) fn date_only(field: FormField<FID>) -> Self {
101 assert_eq!(field.field_type(), &FormFieldType::Date {});
102 Self::init(field, DateTimeConfig::Date)
103 }
104 pub(crate) fn split_layout(&self, area: Rect) -> (Rect, Rect) {
105 let constraint_date = if self.config == DateTimeConfig::Time {
106 ratatui::layout::Constraint::Max(0)
107 } else {
108 ratatui::layout::Constraint::Min(7)
109 };
110 let constraint_time = if self.config == DateTimeConfig::Date {
111 ratatui::layout::Constraint::Max(0)
112 } else {
113 ratatui::layout::Constraint::Min(1)
114 };
115 let areas = Layout::vertical(vec![constraint_date, constraint_time]).split(area);
116 (areas[0], areas[1])
117 }
118 fn make_dates(date: Date, color: Color) -> CalendarEventStore {
119 let mut list = CalendarEventStore::today(Style::default().bold().fg(GRAY.c500));
120 list.add(date, Style::default().bold().fg(color));
121 list
122 }
123 fn increment_time_part(&mut self, amount: i8) {
124 let delta = i64::from(amount);
125
126 let updated = match self.selected_time_part {
127 Some(TimePart::Hour) => self.value + Duration::hours(delta),
128 Some(TimePart::Minute) => self.value + Duration::minutes(delta),
129 Some(TimePart::Second) => self.value + Duration::seconds(delta),
130 Some(TimePart::Nanosecond) => self.value + Duration::nanoseconds(delta),
131 None => return,
132 };
133
134 self.value = updated;
135 }
136}
137
138impl<FID, CID, CA, CS, CM> InputCpt<CID, CA, CS, CM> for DateTimeInputCpt<FID>
139where
140 FID: EzCptIds,
141 CID: EzCptIds,
142 CA: EzArgs,
143 CS: EzState,
144 CM: EzMsg,
145{
146 fn get_value(&self) -> FormFieldValue {
147 FormFieldValue::DateTime(self.value.to_utc())
148 }
149
150 fn capture_validation(&mut self) -> bool {
151 if self.buffer.raw().is_empty() {
152 false
153 } else {
154 if let Some(date) = self.buffer.parsed() {
155 self.value = date.with_timezone(&Local);
156 self.buffer.reset();
157 }
158 true
159 }
160 }
161}
162impl<FID> MockComponent for DateTimeInputCpt<FID>
163where
164 FID: EzCptIds,
165{
166 fn draw(&mut self, area: Rect, buf: &mut Buffer, theme: &Theme) {
167 let ts = self.value.to_utc().timestamp();
168 let offset = if self.local {
169 OffsetDateTime::now_local().map_or(UtcOffset::UTC, OffsetDateTime::offset)
170 } else {
171 UtcOffset::UTC
172 };
173
174 let offset_date = OffsetDateTime::from_unix_timestamp(ts)
175 .expect("Failed to convert timestamp to OffsetDateTime")
176 .to_offset(offset);
177 let time = offset_date.time();
178 let date = offset_date.date();
179
180 let block = theme
181 .block(self.props())
182 .border_type(BorderType::Double)
183 .title_bottom(format!(" {} ", self.buffer.raw()));
184 let b_area = block.inner(area);
185 block.render(area, buf);
186 let (date_area, time_area) = self.split_layout(b_area);
187
188 if self.config != DateTimeConfig::Time {
190 Monthly::new(
191 date,
192 Self::make_dates(
193 date,
194 theme.accent_or_text(self.selected_time_part.is_none()).c500,
195 ),
196 )
197 .show_month_header(Style::new().bold())
198 .show_weekdays_header(Style::new().italic())
199 .render(date_area, buf);
200 }
201
202 if self.config != DateTimeConfig::Date {
204 let hour = time.hour();
205 let minute = time.minute();
206 let second = time.second();
207 let nanosecond = time.nanosecond();
208 let time_line = Line::from(vec![
209 Span::styled(
210 format!("{hour:02}"),
211 theme
212 .accent_or_text(self.selected_time_part == Some(TimePart::Hour))
213 .c500,
214 ),
215 Span::styled(":", Style::default().fg(Color::Gray)),
216 Span::styled(
217 format!("{minute:02}"),
218 theme
219 .accent_or_text(self.selected_time_part == Some(TimePart::Minute))
220 .c500,
221 ),
222 Span::styled(":", Style::default().fg(Color::Gray)),
223 Span::styled(
224 format!("{second:02}"),
225 theme
226 .accent_or_text(self.selected_time_part == Some(TimePart::Second))
227 .c500,
228 ),
229 Span::styled(".", Style::default().fg(Color::Gray)),
230 Span::styled(
231 format!("{nanosecond:09}"),
232 theme
233 .accent_or_text(self.selected_time_part == Some(TimePart::Nanosecond))
234 .c500,
235 ),
236 ]);
237
238 Paragraph::new(time_line).render(time_area, buf);
239 }
240 }
241}
242
243impl<FID, CID, CA, CS, CM> Component<CID, CA, CS, CM> for DateTimeInputCpt<FID>
244where
245 FID: EzCptIds,
246 CID: EzCptIds,
247 CA: EzArgs,
248 CS: EzState,
249 CM: EzMsg,
250{
251 fn on_event(
252 &mut self,
253 event: EzEvent<CID, CM>,
254 _state: &mut State<CS>,
255 ) -> Vec<EzEvent<CID, CM>> {
256 if let EzEvent::Keyboard(kev) = event {
257 if let KeyCode::PageUp = kev.code {
258 if self.selected_time_part.is_none() {
259 self.value -= Duration::days(30);
260 } else {
261 self.increment_time_part(5);
262 }
263 } else if let KeyCode::PageDown = kev.code {
264 if self.selected_time_part.is_none() {
265 self.value += Duration::days(30);
266 } else {
267 self.increment_time_part(-5);
268 }
269 } else if let KeyCode::Up = kev.code {
270 if self.selected_time_part.is_none() {
271 self.value -= Duration::days(7);
272 } else {
273 self.increment_time_part(1);
274 }
275 } else if let KeyCode::Down = kev.code {
276 if self.selected_time_part.is_none() {
277 self.value += Duration::days(7);
278 } else {
279 self.increment_time_part(-1);
280 }
281 } else if let KeyCode::Left = kev.code {
282 if self.selected_time_part.is_none() {
283 self.value -= Duration::days(1);
284 }
285 } else if let KeyCode::Right = kev.code {
286 if self.selected_time_part.is_none() {
287 self.value += Duration::days(1);
288 }
289 } else if let KeyCode::PrintScreen = kev.code {
290 self.local = !self.local;
291 } else if let KeyCode::Backspace = kev.code {
292 self.buffer.pop();
293 } else if let KeyCode::Char(' ') = kev.code {
294 self.selected_time_part = match &self.selected_time_part {
295 None => {
296 if self.config == DateTimeConfig::Time {
297 None
298 } else {
299 Some(TimePart::default())
300 }
301 }
302 Some(part) => part.next(),
303 }
304 } else if let KeyCode::Esc = kev.code {
305 self.buffer.reset();
306 } else {
307 self.buffer.handle(kev);
308 }
309 }
310 vec![]
311 }
312
313 fn focusable(&self) -> bool {
314 true
315 }
316}