1use std::{fmt::Display, hash::Hash};
32
33pub use chrono::{
34 offset::{FixedOffset, Local, Utc},
35 Date,
36};
37use chrono::{prelude::*, Duration};
38use eframe::{
39 egui,
40 egui::{Area, Color32, DragValue, Frame, Id, Key, Order, Response, RichText, Ui, Widget},
41};
42use num_traits::FromPrimitive;
43
44pub struct DatePicker<'a, Tz>
50where
51 Tz: TimeZone,
52 Tz::Offset: Display,
53{
54 id: Id,
55 date: &'a mut Date<Tz>,
56 sunday_first: bool,
57 movable: bool,
58 format_string: String,
59 weekend_color: Color32,
60 weekend_func: fn(&Date<Tz>) -> bool,
61 highlight_weekend: bool,
62}
63
64impl<'a, Tz> DatePicker<'a, Tz>
65where
66 Tz: TimeZone,
67 Tz::Offset: Display,
68{
69 pub fn new<T: Hash>(id: T, date: &'a mut Date<Tz>) -> Self {
71 Self {
72 id: Id::new(id),
73 date,
74 sunday_first: false,
75 movable: false,
76 format_string: String::from("%Y-%m-%d"),
77 weekend_color: Color32::from_rgb(196, 0, 0),
78 weekend_func: |date| date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun,
79 highlight_weekend: true,
80 }
81 }
82
83 #[must_use]
86 pub fn sunday_first(mut self, flag: bool) -> Self {
87 self.sunday_first = flag;
88 self
89 }
90
91 #[must_use]
94 pub fn movable(mut self, flag: bool) -> Self {
95 self.movable = flag;
96 self
97 }
98
99 #[must_use]
102 pub fn date_format(mut self, new_format: &impl ToString) -> Self {
103 self.format_string = new_format.to_string();
104 self
105 }
106
107 #[must_use]
110 pub fn highlight_weekend(mut self, highlight: bool) -> Self {
111 self.highlight_weekend = highlight;
112 self
113 }
114
115 #[must_use]
117 pub fn highlight_weekend_color(mut self, color: Color32) -> Self {
118 self.weekend_color = color;
119 self
120 }
121
122 pub fn weekend_days(mut self, is_weekend: fn(&Date<Tz>) -> bool) -> Self {
124 self.weekend_func = is_weekend;
125 self
126 }
127
128 fn show_grid_header(&mut self, ui: &mut Ui) {
130 let day_indexes = if self.sunday_first {
131 [6, 0, 1, 2, 3, 4, 5]
132 } else {
133 [0, 1, 2, 3, 4, 5, 6]
134 };
135 for i in day_indexes {
136 let b = Weekday::from_u8(i).unwrap();
137 ui.label(b.to_string());
138 }
139 }
140
141 fn get_start_offset_of_calendar(&self, first_day: &Date<Tz>) -> u32 {
144 if self.sunday_first {
145 first_day.weekday().num_days_from_sunday()
146 } else {
147 first_day.weekday().num_days_from_monday()
148 }
149 }
150
151 fn get_end_offset_of_calendar(&self, first_day: &Date<Tz>) -> u32 {
154 if self.sunday_first {
155 (7 - (first_day).weekday().num_days_from_sunday()) % 7
156 } else {
157 (7 - (first_day).weekday().num_days_from_monday()) % 7
158 }
159 }
160
161 fn show_calendar_grid(&mut self, ui: &mut Ui) {
162 egui::Grid::new("calendar").show(ui, |ui| {
163 self.show_grid_header(ui);
164 let first_day_of_current_month = self.date.with_day(1).unwrap();
165 let start_offset = self.get_start_offset_of_calendar(&first_day_of_current_month);
166 let days_in_month = get_days_from_month(self.date.year(), self.date.month());
167 let first_day_of_next_month =
168 first_day_of_current_month.clone() + Duration::days(days_in_month);
169 let end_offset = self.get_end_offset_of_calendar(&first_day_of_next_month);
170 let start_date = first_day_of_current_month - Duration::days(start_offset.into());
171 for i in 0..(start_offset as i64 + days_in_month + end_offset as i64) {
172 if i % 7 == 0 {
173 ui.end_row();
174 }
175 let d = start_date.clone() + Duration::days(i);
176 self.show_day_button(d, ui);
177 }
178 });
179 }
180
181 fn show_day_button(&mut self, date: Date<Tz>, ui: &mut Ui) {
182 ui.add_enabled_ui(self.date != &date, |ui| {
183 ui.centered_and_justified(|ui| {
184 if self.date.month() != date.month() {
185 ui.style_mut().visuals.button_frame = false;
186 }
187 if self.highlight_weekend && (self.weekend_func)(&date) {
188 ui.style_mut().visuals.override_text_color = Some(self.weekend_color);
189 }
190 if ui.button(date.day().to_string()).clicked() {
191 *self.date = date;
192 }
193 });
194 });
195 }
196
197 fn show_header(&mut self, ui: &mut Ui) {
199 ui.horizontal(|ui| {
200 self.show_month_control(ui);
201 self.show_year_control(ui);
202 if ui.button("Today").clicked() {
203 *self.date = Utc::now().with_timezone(&self.date.timezone()).date();
204 }
205 });
206 }
207
208 fn date_step_button(&mut self, ui: &mut Ui, text: impl ToString, duration: Duration) {
210 if ui.button(text.to_string()).clicked() {
211 *self.date = self.date.clone() + duration;
212 }
213 }
214
215 fn show_year_control(&mut self, ui: &mut Ui) {
218 self.date_step_button(ui, "<", Duration::days(-365));
219 let mut drag_year = self.date.year();
220 ui.add(DragValue::new(&mut drag_year));
221 if drag_year != self.date.year() {
222 *self.date = self.date.with_year(drag_year).unwrap();
223 }
224 self.date_step_button(ui, ">", Duration::days(365));
225 }
226
227 fn show_month_control(&mut self, ui: &mut Ui) {
230 self.date_step_button(ui, "<", Duration::days(-30));
231 let month_string = chrono::Month::from_u32(self.date.month()).unwrap().name();
232 ui.add(egui::Label::new(
234 RichText::new(format!("{: <9}", month_string)).text_style(egui::TextStyle::Monospace),
235 ));
236 self.date_step_button(ui, ">", Duration::days(30));
246 }
247}
248
249impl<'a, Tz> Widget for DatePicker<'a, Tz>
250where
251 Tz: TimeZone,
252 Tz::Offset: Display,
253{
254 fn ui(mut self, ui: &mut Ui) -> Response {
255 let formated_date = self.date.format(&self.format_string);
256 let button_response = ui.button(formated_date.to_string());
257 if button_response.clicked() {
258 ui.memory().toggle_popup(self.id);
259 }
260
261 if ui.memory().is_popup_open(self.id) {
262 let mut area = Area::new(self.id)
263 .order(Order::Foreground)
264 .default_pos(button_response.rect.left_bottom());
265 if !self.movable {
266 area = area.movable(false);
267 }
268 let area_response = area
269 .show(ui.ctx(), |ui| {
270 Frame::popup(ui.style()).show(ui, |ui| {
271 self.show_header(ui);
272 self.show_calendar_grid(ui);
273 });
274 })
275 .response;
276
277 if !button_response.clicked()
278 && (ui.input().key_pressed(Key::Escape) || area_response.clicked_elsewhere())
279 {
280 ui.memory().toggle_popup(self.id);
281 }
282 }
283 button_response
284 }
285}
286
287fn get_days_from_month(year: i32, month: u32) -> i64 {
289 NaiveDate::from_ymd(
290 match month {
291 12 => year + 1,
292 _ => year,
293 },
294 match month {
295 12 => 1,
296 _ => month + 1,
297 },
298 1,
299 )
300 .signed_duration_since(NaiveDate::from_ymd(year, month, 1))
301 .num_days()
302}