Skip to main content

ui/
datepick.rs

1// datepick -- date and time input
2//
3//   time_box(id, &mut hour, &mut minute) -> EV    two number boxes "HH : MM" (EV_ACT changed)
4//   weekday_toggles(id, &mut mask) -> EV          seven toggle buttons, bit 0 = Monday ..
5//                                                 bit 6 = Sunday (EV_ACT changed); they wrap
6//                                                 where the row is narrower (BOX_WRAP)
7//   date_picker(id, &mut y, &mut m, &mut d) -> EV a button with the date (the user's format,
8//                                                 i18n::fmt_date) that opens a month calendar
9//                                                 starting on the locale's first weekday
10//                                                 (EV_ACT when a day was picked; it closes the
11//                                                 calendar only, a flyout around it stays)
12//   calendar(id, &mut y, &mut m, &mut d, &DateRange) -> EV
13//                                                 the calendar in place, for any panel or flyout:
14//                                                 a month of days (CAL_DAY), of ISO weeks
15//                                                 (CAL_WEEK: a row is a week, the date its
16//                                                 Monday) or a year of months (CAL_MONTH: the 1st);
17//                                                 only dates between min and max can be picked
18//                                                 (EV_ACT when the user picked)
19//   The civil helpers (days in a month, weekday, ISO weeks) are jflib::datetime's.
20//
21// Ids: parts (id << 3 | k) | 0x80000000; calendar cells ((id ^ 0x3C3C) << 6 | cell) | 0x80000000
22// (a day 0 .. 41, a week row 0 .. 5, a month 0 .. 11).
23
24use alloc::format;
25
26use crate::consts::*;
27use crate::i18n;
28use crate::keys::*;
29use crate::{Sp, Ui};
30
31/// what a calendar picks (DateRange::mode): a day
32pub const CAL_DAY: u32 = 0;
33/// an ISO 8601 week (Monday to Sunday): the date is its Monday, the rows start on Monday and show
34/// the week's number (datetime::iso_week)
35pub const CAL_WEEK: u32 = 1;
36/// a month: the date is its 1st, the calendar shows the twelve months of a year
37pub const CAL_MONTH: u32 = 2;
38
39/// what a calendar picks and between which dates (calendar): `mode` CAL_DAY, CAL_WEEK or
40/// CAL_MONTH; min and max (year, month, day) inclusive -- in CAL_WEEK the weeks that hold them, in
41/// CAL_MONTH their months
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct DateRange {
44    pub mode: u32,
45    pub min: (i32, u32, u32),
46    pub max: (i32, u32, u32),
47}
48
49impl DateRange {
50    /// days of the years 1 to 9999
51    pub const DAYS: DateRange = DateRange::new(CAL_DAY, (1, 1, 1), (9999, 12, 31));
52
53    pub const fn new(mode: u32, min: (i32, u32, u32), max: (i32, u32, u32)) -> DateRange {
54        DateRange { mode, min, max }
55    }
56
57    // a date in the mode's unit: its day, the Monday of its week, its month
58    fn key(&self, (y, m, d): (i32, u32, u32)) -> i64 {
59        match self.mode {
60            CAL_WEEK => days(y, m, d) - wday(y, m, d) as i64,
61            CAL_MONTH => y as i64 * 12 + m as i64 - 1,
62            _ => days(y, m, d),
63        }
64    }
65
66    // the date can be picked
67    fn has(&self, date: (i32, u32, u32)) -> bool {
68        let k = self.key(date);
69        self.key(self.min) <= k && k <= self.key(self.max)
70    }
71}
72
73fn part(id: u32, k: u32) -> u32 {
74    id.wrapping_mul(8).wrapping_add(k) | 0x8000_0000
75}
76
77fn cell_id(id: u32, cell: u32) -> u32 {
78    ((id ^ 0x3C3C).wrapping_mul(64).wrapping_add(cell)) | 0x8000_0000
79}
80
81// the civil helpers of datetime with the widgets' types (month and day u32)
82fn dim(y: i32, m: u32) -> u32 {
83    datetime::days_in_month(y, m.clamp(1, 12) as u8) as u32
84}
85
86fn days(y: i32, m: u32, d: u32) -> i64 {
87    datetime::days_from_civil(y, m as u8, d.min(31) as u8)
88}
89
90fn wday(y: i32, m: u32, d: u32) -> u32 {
91    datetime::weekday(y, m as u8, d.min(31) as u8) as u32
92}
93
94fn civil(z: i64) -> (i32, u32, u32) {
95    let (y, m, d) = datetime::civil_from_days(z);
96    (y, m as u32, d as u32)
97}
98
99impl Ui {
100    /// ui_time_box(id, &mut hour, &mut minute) -> EV_* (EV_ACT changed)
101    #[cfg_attr(jfgui_dev, track_caller)]
102    pub fn time_box(&mut self, id: u32, hour: &mut i32, minute: &mut i32) -> u32 {
103        let _src = self.src_scope();
104        let (h0, m0) = (*hour, *minute);
105        self.box_begin(0, BOX_H | BOX_XCENTER, 4);
106        self.next_size(64, 0);
107        let e1 = self.number_box(part(id, 1), hour, &Range::new(0, 23, 1, 0));
108        self.text(TS_BODY_STRONG, ":");
109        self.next_size(64, 0);
110        let e2 = self.number_box(part(id, 2), minute, &Range::new(0, 59, 1, 0));
111        self.box_end();
112        let ev = (e1 | e2) & !EV_ACT;
113        if *hour != h0 || *minute != m0 { ev | EV_ACT } else { ev }
114    }
115
116    /// ui_weekday_toggles(id, &mut mask) -> EV_* (EV_ACT changed); bit 0 = Monday.  The
117    /// buttons wrap where the row has less room than the seven of them (a narrow card)
118    #[cfg_attr(jfgui_dev, track_caller)]
119    pub fn weekday_toggles(&mut self, id: u32, mask: &mut u32) -> u32 {
120        let _src = self.src_scope();
121        let old = *mask;
122        self.box_begin(0, BOX_H | BOX_XCENTER | BOX_WRAP, 4);
123        for d in 0..7u32 {
124            let on = *mask & (1 << d) != 0;
125            self.next_size(40, 0);
126            self.next_flags(ST_COMPACT | if on { ST_CHECKED | ST_ACCENT } else { 0 });
127            if self.button(part(id, 1 + d), i18n::weekday_short(d)) & EV_ACT != 0 {
128                *mask ^= 1 << d;
129                self.repass();
130            }
131        }
132        self.box_end();
133        if *mask != old { EV_ACT } else { 0 }
134    }
135
136    /// ui_date_picker(id, &mut y, &mut m, &mut d) -> EV_* (EV_ACT when a day was picked).  A
137    /// pick closes the calendar's popup and the popups inside it, not a flyout the picker sits
138    /// in (popup_close_id)
139    #[cfg_attr(jfgui_dev, track_caller)]
140    pub fn date_picker(&mut self, id: u32, y: &mut i32, m: &mut u32, d: &mut u32) -> u32 {
141        let _src = self.src_scope();
142        // the date in the user's format
143        let label = i18n::fmt_date(&datetime::Civil { year: *y, month: *m as u8, day: *d as u8, ..Default::default() }, i18n::DATE_SHORT);
144        self.next_icon(IC_CALENDAR);
145        let ev = self.dropdown_button(id, &label);
146        let mut out = ev & !EV_ACT;
147        // closed: the calendar shows the date's month when it opens again
148        if ev & EV_OPEN == 0 {
149            self.slot(part(id, 7)).aux = 0;
150        }
151        if self.flyout_begin(id, 0) {
152            if self.calendar(id, y, m, d, &DateRange::DAYS) & EV_ACT != 0 {
153                out |= EV_ACT;
154                self.popup_close_id(id);
155            }
156            // today
157            self.next_flags(ST_SUBTLE | ST_COMPACT);
158            if self.button(part(id, 3), i18n::tr("Heute")) & EV_ACT != 0 {
159                let today = plat::date().0;
160                *y = today.year as i32;
161                *m = today.month as u32;
162                *d = today.day as u32;
163                out |= EV_ACT;
164                self.popup_close_id(id);
165                self.repass();
166            }
167        }
168        self.flyout_end();
169        out
170    }
171
172    /// ui_calendar(id, &mut y, &mut m, &mut d, &range) -> EV_* (EV_ACT when the user picked):
173    /// a month calendar in place, no popup -- a panel's, or the content of a flyout of the app's
174    /// own.  range.mode picks a day (CAL_DAY: the weeks start on the locale's first weekday), an
175    /// ISO week (CAL_WEEK: rows from Monday with the week's number; y, m, d become its Monday) or
176    /// a month (CAL_MONTH: a year of months; y, m, d become its 1st); what lies outside
177    /// range.min .. range.max is drawn disabled.  The arrows show the month (the year) before and
178    /// after; the calendar shows the month of y, m, d again when the app changes the date.
179    #[cfg_attr(jfgui_dev, track_caller)]
180    pub fn calendar(&mut self, id: u32, y: &mut i32, m: &mut u32, d: &mut u32, range: &DateRange) -> u32 {
181        let _src = self.src_scope();
182        let months = range.mode == CAL_MONTH;
183        // the view (slot scrollx: year * 12 + month - 1, or the year in CAL_MONTH) follows the
184        // date whenever it or the mode changes (scrolly: the date's day number; aux: the mode + 1,
185        // 0 not set)
186        let vid = part(id, 7);
187        let now = days(*y, *m, *d) as i32;
188        let tag = range.mode as i32 + 1;
189        {
190            let s = self.slot(vid);
191            if s.aux != tag || s.scrolly != now {
192                s.scrollx = if months { *y } else { *y * 12 + *m as i32 - 1 };
193                s.scrolly = now;
194                s.aux = tag;
195            }
196        }
197        let mut view = self.slot(vid).scrollx;
198        let (vy, vm) = if months { (view, 1) } else { (view.div_euclid(12), (view.rem_euclid(12) + 1) as u32) };
199        // header: previous, the month (the year), next; an arrow is off where nothing could be
200        // picked beyond it
201        let (before, after) = if months {
202            (range.key((vy - 1, 12, 31)) < range.key(range.min), range.key((vy + 1, 1, 1)) > range.key(range.max))
203        } else {
204            let (py, pm) = if vm == 1 { (vy - 1, 12) } else { (vy, vm - 1) };
205            let (ny, nm) = if vm == 12 { (vy + 1, 1) } else { (vy, vm + 1) };
206            (range.key((py, pm, dim(py, pm))) < range.key(range.min), range.key((ny, nm, 1)) > range.key(range.max))
207        };
208        let prev = if months { i18n::tr("Vorheriges Jahr") } else { i18n::tr("Vorheriger Monat") };
209        let next = if months { i18n::tr("Nächstes Jahr") } else { i18n::tr("Nächster Monat") };
210        self.box_begin(0, BOX_H | BOX_XCENTER, 4);
211        self.next_icon(IC_CHEV_LEFT);
212        self.next_flags(ST_SUBTLE | ST_ICON_ONLY | if before { ST_DISABLED } else { 0 });
213        if self.button(part(id, 1), prev) & EV_ACT != 0 {
214            view -= 1;
215        }
216        self.next_size(-1, 0);
217        let title = if months { format!("{vy}") } else { format!("{} {}", i18n::month_name(vm), vy) };
218        self.text(TS_BODY_STRONG | TX_CENTER, &title);
219        self.next_icon(IC_CHEV_RIGHT);
220        self.next_flags(ST_SUBTLE | ST_ICON_ONLY | if after { ST_DISABLED } else { 0 });
221        if self.button(part(id, 2), next) & EV_ACT != 0 {
222            view += 1;
223        }
224        self.box_end();
225        if view != self.slot(vid).scrollx {
226            self.slot(vid).scrollx = view;
227            self.repass();
228        }
229        let picked = if months { self.cal_months(id, vy, (*y, *m, *d), range) } else { self.cal_days(id, vy, vm, (*y, *m, *d), range) };
230        match picked {
231            Some(date) => {
232                (*y, *m, *d) = date;
233                self.repass();
234                EV_ACT
235            }
236            None => 0,
237        }
238    }
239
240    // the weekday header and the 6 x 7 days of month vm of year vy (CAL_DAY, CAL_WEEK) -> the
241    // date picked
242    fn cal_days(&mut self, id: u32, vy: i32, vm: u32, value: (i32, u32, u32), range: &DateRange) -> Option<(i32, u32, u32)> {
243        let weeks = range.mode == CAL_WEEK;
244        // ISO weeks start on Monday; days on the locale's first weekday
245        let fw = if weeks { 0 } else { i18n::locale().first_weekday() };
246        let cw = self.dp(36);
247        let ch = self.dp(32);
248        // CAL_WEEK: a column of week numbers before the days
249        let x0 = if weeks { cw } else { 0 };
250        let grid = self.place(x0 + cw * 7, ch * 7);
251        let t3 = self.color(T_TEXT3);
252        for c in 0..7u32 {
253            let s = i18n::weekday_short((c + fw) % 7);
254            self.ts_font(TS_CAPTION);
255            let tw = self.text.width(s);
256            let x = grid[0] + x0 + cw * c as i32;
257            crate::stock::text_at(self, TS_CAPTION, x + ((cw - tw) >> 1), [x, grid[1], cw, ch], s, t3, 0);
258        }
259        // the first cell: the day on or before the 1st that starts a row
260        let start = days(vy, vm, 1) - ((wday(vy, vm, 1) + 7 - fw) % 7) as i64;
261        let today = plat::date().0;
262        let today = (today.year as i32, today.month as u32, today.day as u32);
263        let sel_week = range.key(value);
264        let sh = self.style.shell();
265        let mut picked = None;
266        for row in 0..6i32 {
267            let ry = grid[1] + ch * (1 + row);
268            let monday = civil(start + row as i64 * 7);
269            // CAL_WEEK: the row is one target: its hover and press, the week marked
270            let (wev, wsel) = if weeks {
271                let rid = cell_id(id, row as u32);
272                let on = range.has(monday);
273                let ev = self.hit_rect(rid, grid[0], ry, x0 + cw * 7, ch, if on { 0 } else { HB_DISABLED });
274                let (wy, wn) = datetime::iso_week(monday.0, monday.1 as u8, monday.2 as u8);
275                let sel = range.key(monday) == sel_week;
276                if sel {
277                    self.wp.ctx.last_state |= SS_SELECTED as u32;
278                }
279                let name = format!("{wy}-W{wn:02}");
280                self.sem(K_BUTTON, rid, Some(&name), wn as i32);
281                // the row's band: the accent when chosen, the hover's colour under the pointer
282                let band = if sel { self.color(T_ACCENT) } else if ev & EV_HOVER != 0 { self.color(T_SUBTLE_HOVER) } else { 0 };
283                if band != 0 {
284                    self.cmd_rrect(grid[0], ry + self.dp(2), x0 + cw * 7, ch - self.dp(4), (ch - self.dp(4)) >> 1, 0, band, 0, 0);
285                }
286                let num = format!("{wn}");
287                self.ts_font(TS_CAPTION);
288                let tw = self.text.width(&num);
289                let c = if sel { self.color(T_ON_ACCENT) } else { t3 };
290                crate::stock::text_at(self, TS_CAPTION, grid[0] + ((cw - tw) >> 1), [grid[0], ry, cw, ch], &num, c, 0);
291                if ev & EV_ACT != 0 && on {
292                    picked = Some(monday);
293                }
294                (ev & !EV_ACT, sel)
295            } else {
296                (0, false)
297            };
298            for col in 0..7i32 {
299                let cell = (row * 7 + col) as u32;
300                let date = civil(start + cell as i64);
301                let other = (date.0, date.1) != (vy, vm);
302                let on = range.has(date);
303                let r = [grid[0] + x0 + cw * col, ry, cw, ch];
304                let s = format!("{}", date.2);
305                let (dev, selected) = if weeks {
306                    (wev, wsel)
307                } else {
308                    let did = cell_id(id, cell);
309                    let dev = self.hit_rect(did, r[0], r[1], r[2], r[3], if on { 0 } else { HB_DISABLED });
310                    let selected = date == value;
311                    if selected {
312                        self.wp.ctx.last_state |= SS_SELECTED as u32;
313                    }
314                    self.sem(K_BUTTON, did, Some(&s), date.2 as i32);
315                    if dev & EV_ACT != 0 && on {
316                        picked = Some(date);
317                    }
318                    (dev, selected)
319                };
320                let mut sp = Sp { ev: dev, value: selected as i32, value2: (date == today) as i32, flags: other as u32, label: Some(&s), ..Sp::default() };
321                sp.state = if on { 0 } else { 3 };
322                sp.place(r);
323                sh.calendar_day(self, &sp);
324            }
325        }
326        picked
327    }
328
329    // the twelve months of year vy, three in a row (CAL_MONTH) -> the date picked (the 1st)
330    fn cal_months(&mut self, id: u32, vy: i32, value: (i32, u32, u32), range: &DateRange) -> Option<(i32, u32, u32)> {
331        let (cw, ch) = (self.dp(36) * 7 / 3, self.dp(48));
332        let grid = self.place(cw * 3, ch * 4);
333        let today = plat::date().0;
334        let sh = self.style.shell();
335        let mut picked = None;
336        for k in 0..12u32 {
337            let date = (vy, k + 1, 1);
338            let on = range.has(date);
339            let r = [grid[0] + cw * (k % 3) as i32, grid[1] + ch * (k / 3) as i32, cw, ch];
340            let mid = cell_id(id, k);
341            let ev = self.hit_rect(mid, r[0], r[1], r[2], r[3], if on { 0 } else { HB_DISABLED });
342            let selected = (value.0, value.1) == (vy, k + 1);
343            if selected {
344                self.wp.ctx.last_state |= SS_SELECTED as u32;
345            }
346            let name = format!("{} {vy}", i18n::month_name(k + 1));
347            self.sem(K_BUTTON, mid, Some(&name), (k + 1) as i32);
348            let is_now = (today.year as i32, today.month as u32) == (vy, k + 1);
349            let mut sp = Sp { ev, value: selected as i32, value2: is_now as i32, label: Some(i18n::month_short(k + 1)), ..Sp::default() };
350            sp.state = if on { 0 } else { 3 };
351            sp.place(r);
352            sh.calendar_day(self, &sp);
353            if ev & EV_ACT != 0 && on {
354                picked = Some(date);
355            }
356        }
357        picked
358    }
359}