egui_datepicker/
lib.rs

1//! egui-datepicker adds a simple date picker widget.
2//! Checkout the [example][ex]
3//!
4//!
5//! ```no_run
6//! use eframe::egui::Ui;
7//! use chrono::prelude::*;
8//! use std::fmt::Display;
9//! use egui_datepicker::DatePicker;
10//!
11//! struct App<Tz>
12//! where
13//!     Tz: TimeZone,
14//!     Tz::Offset: Display,
15//! {
16//!     date: chrono::Date<Tz>
17//! }
18//! impl<Tz> App<Tz>
19//! where
20//!     Tz: TimeZone,
21//!     Tz::Offset: Display,
22//! {
23//!     fn draw_datepicker(&mut self, ui: &mut Ui) {
24//!         ui.add(DatePicker::new("super_unique_id", &mut self.date));
25//!     }
26//! }
27//! ```
28//!
29//! [ex]: ./examples/simple.rs
30
31use 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
44/// Default values of fields are:
45/// - sunday_first: `false`
46/// - movable: `false`
47/// - format_string: `"%Y-%m-%d"`
48/// - weekend_func: `date.weekday() == Weekday::Sat || date.weekday() == Weekday::Sun`
49pub 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    /// Create new date picker with unique id and mutable reference to date.
70    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    /// If flag is set to true then first day in calendar will be sunday otherwise monday.
84    /// Default is false
85    #[must_use]
86    pub fn sunday_first(mut self, flag: bool) -> Self {
87        self.sunday_first = flag;
88        self
89    }
90
91    /// If flag is set to true then date picker popup will be movable.
92    /// Default is false
93    #[must_use]
94    pub fn movable(mut self, flag: bool) -> Self {
95        self.movable = flag;
96        self
97    }
98
99    ///Set date format.
100    ///See the [chrono::format::strftime](https://docs.rs/chrono/0.4.19/chrono/format/strftime/index.html) for the specification.
101    #[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    ///If highlight is true then weekends text color will be `weekend_color` instead default text
108    ///color.
109    #[must_use]
110    pub fn highlight_weekend(mut self, highlight: bool) -> Self {
111        self.highlight_weekend = highlight;
112        self
113    }
114
115    ///Set weekends highlighting color.
116    #[must_use]
117    pub fn highlight_weekend_color(mut self, color: Color32) -> Self {
118        self.weekend_color = color;
119        self
120    }
121
122    /// Set function, which will decide if date is a weekend day or not.
123    pub fn weekend_days(mut self, is_weekend: fn(&Date<Tz>) -> bool) -> Self {
124        self.weekend_func = is_weekend;
125        self
126    }
127
128    /// Draw names of week days as 7 columns of grid without calling `Ui::end_row`
129    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    /// Get number of days between first day of the month and Monday ( or Sunday if field
142    /// `sunday_first` is set to `true` )
143    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    /// Get number of days between first day of the next month and Monday ( or Sunday if field
152    /// `sunday_first` is set to `true` )
153    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    /// Draw current month and buttons for next and previous month.
198    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    /// Draw button with text and add duration to current date when that button is clicked.
209    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    /// Draw drag value widget with current year and two buttons which substract and add 365 days
216    /// to current date.
217    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    /// Draw label(will be combobox in future) with current month and two buttons which substract and add 30 days
228    /// to current date.
229    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        // TODO: When https://github.com/emilk/egui/pull/543 is merged try to change label to combo box.
233        ui.add(egui::Label::new(
234            RichText::new(format!("{: <9}", month_string)).text_style(egui::TextStyle::Monospace),
235        ));
236        // let mut selected = self.date.month0() as usize;
237        // egui::ComboBox::from_id_source(self.id.with("month_combo_box"))
238        //     .selected_text(selected)
239        //     .show_index(ui, &mut selected, 12, |i| {
240        //         chrono::Month::from_usize(i + 1).unwrap().name().to_string()
241        //     });
242        // if selected != self.date.month0() as usize {
243        //     *self.date = self.date.with_month0(selected as u32).unwrap();
244        // }
245        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
287// https://stackoverflow.com/a/58188385
288fn 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}