rat_widget/calendar/
range_selection.rs

1use crate::calendar::calendar::CalendarState;
2use crate::calendar::event::CalOutcome;
3use crate::calendar::{
4    first_day_of_month, is_first_day_of_month, is_last_day_of_month, is_same_month, is_same_week,
5    last_day_of_month, CalendarSelection, MonthState,
6};
7use chrono::{Datelike, Days, Months, NaiveDate, Weekday};
8use rat_event::util::item_at;
9use rat_event::ConsumedEvent;
10use rat_event::{ct_event, flow, HandleEvent, MouseOnly, Regular};
11use rat_focus::HasFocus;
12use std::ops::RangeInclusive;
13
14/// Can select a date range.
15///
16/// - Movement with the arrow-keys and PageUp/PageDown.
17/// - Ctrl+Home moves to today.
18///
19/// - Ctrl+A selects the current month.
20/// - Shift+Arrow extends the selection.
21/// - Shift+PageUp/PageDown extends the selection by a whole month.
22/// - Alt+Shift+Up/Down extends the selection by a whole week.
23///
24#[derive(Debug, Default, Clone)]
25pub struct RangeSelection {
26    anchor: Option<NaiveDate>,
27    lead: Option<NaiveDate>,
28}
29
30impl CalendarSelection for RangeSelection {
31    /// Length of the selection.
32    fn count(&self) -> usize {
33        if let Some(anchor) = self.anchor {
34            if let Some(lead) = self.lead {
35                (lead - anchor).num_days().unsigned_abs() as usize + 1
36            } else {
37                unreachable!()
38            }
39        } else {
40            0
41        }
42    }
43
44    fn is_selected(&self, date: NaiveDate) -> bool {
45        if let Some(lead) = self.lead {
46            if let Some(anchor) = self.anchor {
47                if lead > anchor {
48                    date >= anchor && date <= lead
49                } else {
50                    date >= lead && date <= anchor
51                }
52            } else {
53                unreachable!()
54            }
55        } else {
56            false
57        }
58    }
59
60    fn lead_selection(&self) -> Option<NaiveDate> {
61        self.lead
62    }
63}
64
65impl RangeSelection {
66    /// Clear the selection.
67    pub fn clear(&mut self) {
68        self.anchor = None;
69        self.lead = None;
70    }
71
72    /// Select the week of the given date.
73    ///
74    /// If extend is used, this will extend the selection to include
75    /// the new week. If the current selection doesn't cover full weeks
76    /// it will be buffed up to do so afterwards.
77    ///
78    pub fn select_month(&mut self, date: NaiveDate, extend: bool) -> bool {
79        let old = (self.anchor, self.lead);
80
81        let new_start = first_day_of_month(date);
82        let new_end = last_day_of_month(date);
83
84        if extend {
85            if let Some(mut lead) = self.lead {
86                let Some(mut anchor) = self.anchor else {
87                    unreachable!();
88                };
89
90                // fill out week
91                if lead <= anchor {
92                    if !is_first_day_of_month(lead) || !is_last_day_of_month(anchor) {
93                        lead = first_day_of_month(lead);
94                        anchor = last_day_of_month(anchor);
95                        self.lead = Some(lead);
96                        self.anchor = Some(anchor);
97                    }
98                } else {
99                    if !is_last_day_of_month(lead) || !is_first_day_of_month(anchor) {
100                        lead = last_day_of_month(lead);
101                        anchor = first_day_of_month(anchor);
102                        self.lead = Some(lead);
103                        self.anchor = Some(anchor);
104                    }
105                }
106
107                if is_same_month(lead, anchor) {
108                    // A single month can change direction.
109                    if lead <= anchor {
110                        if new_start < anchor {
111                            self.lead = Some(new_start);
112                        } else {
113                            self.lead = Some(new_end);
114                            self.anchor = Some(lead);
115                        }
116                    } else {
117                        if new_start > anchor {
118                            self.lead = Some(new_end);
119                        } else {
120                            self.lead = Some(new_start);
121                            self.anchor = Some(lead);
122                        }
123                    }
124                } else {
125                    // keep direction and reduce/extend in that direction.
126                    if lead < anchor {
127                        if new_start <= lead {
128                            self.lead = Some(new_start);
129                        } else if new_end <= anchor {
130                            self.lead = Some(new_start);
131                        } else {
132                            self.lead = Some(new_end);
133                        }
134                    } else {
135                        if new_end <= lead {
136                            self.lead = Some(new_end);
137                        } else if new_start >= anchor {
138                            self.lead = Some(new_end);
139                        } else {
140                            self.lead = Some(new_start);
141                        }
142                    }
143                }
144            } else {
145                self.lead = Some(new_start);
146                self.anchor = Some(new_end);
147            }
148        } else {
149            self.lead = Some(new_start);
150            self.anchor = Some(new_end);
151        }
152
153        old != (self.anchor, self.lead)
154    }
155
156    /// Select the week of the given date.
157    ///
158    /// If extend is used, this will extend the selection to include
159    /// the new week. If the current selection doesn't cover full weeks
160    /// it will be buffed up to do so afterwards.
161    ///
162    pub fn select_week(&mut self, date: NaiveDate, extend: bool) -> bool {
163        let old = (self.anchor, self.lead);
164
165        let new_start = date.week(Weekday::Mon).first_day();
166        let new_end = date.week(Weekday::Mon).last_day();
167
168        if extend {
169            if let Some(mut lead) = self.lead {
170                let Some(mut anchor) = self.anchor else {
171                    unreachable!();
172                };
173
174                // fill out week
175                if lead <= anchor {
176                    if lead.weekday() != Weekday::Mon || anchor.weekday() != Weekday::Sun {
177                        lead = lead.week(Weekday::Mon).first_day();
178                        anchor = anchor.week(Weekday::Mon).last_day();
179                        self.lead = Some(lead);
180                        self.anchor = Some(anchor);
181                    }
182                } else {
183                    if lead.weekday() != Weekday::Sun || anchor.weekday() != Weekday::Mon {
184                        lead = lead.week(Weekday::Mon).last_day();
185                        anchor = anchor.week(Weekday::Mon).first_day();
186                        self.lead = Some(lead);
187                        self.anchor = Some(anchor);
188                    }
189                }
190
191                if is_same_week(lead, anchor) {
192                    // A single week can change direction.
193                    if lead <= anchor {
194                        if new_start < anchor {
195                            self.lead = Some(new_start);
196                        } else {
197                            self.lead = Some(new_end);
198                            self.anchor = Some(lead);
199                        }
200                    } else {
201                        if new_start > anchor {
202                            self.lead = Some(new_end);
203                        } else {
204                            self.lead = Some(new_start);
205                            self.anchor = Some(lead);
206                        }
207                    }
208                } else {
209                    // keep direction and reduce/extend in that direction.
210                    if lead < anchor {
211                        if new_start <= lead {
212                            self.lead = Some(new_start);
213                        } else if new_end <= anchor {
214                            self.lead = Some(new_start);
215                        } else {
216                            self.lead = Some(new_end);
217                        }
218                    } else {
219                        if new_end <= lead {
220                            self.lead = Some(new_end);
221                        } else if new_start >= anchor {
222                            self.lead = Some(new_end);
223                        } else {
224                            self.lead = Some(new_start);
225                        }
226                    }
227                }
228            } else {
229                self.lead = Some(new_start);
230                self.anchor = Some(new_end);
231            }
232        } else {
233            self.lead = Some(new_start);
234            self.anchor = Some(new_end);
235        }
236
237        old != (self.anchor, self.lead)
238    }
239
240    /// Select a date.
241    pub fn select_day(&mut self, date: NaiveDate, extend: bool) -> bool {
242        let old = (self.anchor, self.lead);
243
244        if extend {
245            self.lead = Some(date);
246            if self.anchor.is_none() {
247                self.anchor = Some(date);
248            }
249        } else {
250            self.anchor = Some(date);
251            self.lead = Some(date);
252        }
253
254        old != (self.anchor, self.lead)
255    }
256
257    /// Select range as (anchor, lead) pair.
258    pub fn select(&mut self, selection: (NaiveDate, NaiveDate)) -> bool {
259        let old = (self.anchor, self.lead);
260
261        self.anchor = Some(selection.0);
262        self.lead = Some(selection.1);
263
264        old != (self.anchor, self.lead)
265    }
266
267    /// Selection as (anchor, lead) pair.
268    pub fn selected(&self) -> Option<(NaiveDate, NaiveDate)> {
269        if let Some(anchor) = self.anchor {
270            if let Some(lead) = self.lead {
271                Some((anchor, lead))
272            } else {
273                unreachable!()
274            }
275        } else {
276            None
277        }
278    }
279
280    /// Selection as date-range.
281    pub fn selected_range(&self) -> Option<RangeInclusive<NaiveDate>> {
282        if let Some(anchor) = self.anchor {
283            if let Some(lead) = self.lead {
284                if lead > anchor {
285                    Some(anchor..=lead)
286                } else {
287                    Some(lead..=anchor)
288                }
289            } else {
290                unreachable!()
291            }
292        } else {
293            None
294        }
295    }
296}
297
298impl HandleEvent<crossterm::event::Event, Regular, CalOutcome> for MonthState<RangeSelection> {
299    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CalOutcome {
300        if self.is_focused() {
301            flow!(match event {
302                ct_event!(keycode press Home) => self.select_day(0, false),
303                ct_event!(keycode press End) => self.select_last(false),
304                ct_event!(keycode press SHIFT-Home) => self.select_day(0, true),
305                ct_event!(keycode press SHIFT-End) => self.select_last(true),
306
307                ct_event!(keycode press Up) => self.prev_day(7, false),
308                ct_event!(keycode press Down) => self.next_day(7, false),
309                ct_event!(keycode press Left) => self.prev_day(1, false),
310                ct_event!(keycode press Right) => self.next_day(1, false),
311                ct_event!(keycode press SHIFT-Up) => self.prev_day(7, true),
312                ct_event!(keycode press SHIFT-Down) => self.next_day(7, true),
313                ct_event!(keycode press SHIFT-Left) => self.prev_day(1, true),
314                ct_event!(keycode press SHIFT-Right) => self.next_day(1, true),
315
316                ct_event!(keycode press ALT-Up) => self.prev_week(1, false),
317                ct_event!(keycode press ALT-Down) => self.next_week(1, false),
318                ct_event!(keycode press ALT_SHIFT-Up) => self.prev_week(1, true),
319                ct_event!(keycode press ALT_SHIFT-Down) => self.next_week(1, true),
320                _ => CalOutcome::Continue,
321            })
322        }
323
324        self.handle(event, MouseOnly)
325    }
326}
327
328impl HandleEvent<crossterm::event::Event, MouseOnly, CalOutcome> for MonthState<RangeSelection> {
329    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> CalOutcome {
330        let mut r = match event {
331            ct_event!(mouse any for m)
332                if self.mouse.drag(
333                    &[self.area_cal, self.area_weeknum], //
334                    m,
335                ) =>
336            {
337                if self.mouse.drag.get() == Some(0) {
338                    if let Some(sel) = item_at(&self.area_days, m.column, m.row) {
339                        self.select_day(sel, true)
340                    } else {
341                        let mut r = CalOutcome::Continue;
342
343                        let mut above = self.area_cal;
344                        above.y -= 2;
345                        above.height = 3;
346                        if above.contains((m.column, m.row).into()) {
347                            r = self.select_day(0, true);
348                        } else {
349                            let mut below = self.area_cal;
350                            below.y = below.bottom().saturating_sub(1);
351                            below.height = 2;
352                            if below.contains((m.column, m.row).into()) {
353                                r = self.select_day(self.end_date().day0() as usize, true);
354                            }
355                        }
356                        r
357                    }
358                } else if self.mouse.drag.get() == Some(1) {
359                    if let Some(sel) = item_at(&self.area_weeks, m.column, m.row) {
360                        self.select_week(sel, true)
361                    } else {
362                        let mut r = CalOutcome::Continue;
363
364                        let mut above = self.area_weeknum;
365                        above.y -= 2;
366                        above.height = 3;
367                        if above.contains((m.column, m.row).into()) {
368                            r = self.select_week(0, true);
369                        } else {
370                            let mut below = self.area_cal;
371                            below.y = below.bottom().saturating_sub(1);
372                            below.height = 2;
373                            if below.contains((m.column, m.row).into()) {
374                                r = self.select_week(self.end_date().day0() as usize, true);
375                            }
376                        }
377                        r
378                    }
379                } else {
380                    CalOutcome::Continue
381                }
382            }
383            _ => CalOutcome::Continue,
384        };
385
386        r = r.or_else(|| match event {
387            ct_event!(mouse down Left for x, y) => {
388                if let Some(sel) = item_at(&self.area_weeks, *x, *y) {
389                    self.select_week(sel, false)
390                } else if let Some(sel) = item_at(&self.area_days, *x, *y) {
391                    self.select_day(sel, false)
392                } else {
393                    CalOutcome::Continue
394                }
395            }
396            ct_event!(mouse down CONTROL-Left for x, y) => {
397                if let Some(sel) = item_at(&self.area_weeks, *x, *y) {
398                    self.select_week(sel, true)
399                } else if let Some(sel) = item_at(&self.area_days, *x, *y) {
400                    self.select_day(sel, true)
401                } else {
402                    CalOutcome::Continue
403                }
404            }
405            _ => CalOutcome::Continue,
406        });
407
408        r
409    }
410}
411
412impl<const N: usize> HandleEvent<crossterm::event::Event, Regular, CalOutcome>
413    for CalendarState<N, RangeSelection>
414{
415    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> CalOutcome {
416        let mut r = 'f: {
417            for month in &mut self.months {
418                let r = month.handle(event, Regular);
419                if r == CalOutcome::Selected {
420                    self.focus_lead();
421                    break 'f r;
422                }
423            }
424            CalOutcome::Continue
425        };
426        // Enable drag for all months.
427        if r.is_consumed() {
428            let mut drag = None;
429            for m in &self.months {
430                if let Some(d) = m.mouse.drag.get() {
431                    drag = Some(d);
432                    break;
433                }
434            }
435            if drag.is_some() {
436                for m in &self.months {
437                    m.mouse.drag.set(drag);
438                }
439            }
440        }
441
442        r = r.or_else(|| {
443            if self.is_focused() {
444                match event {
445                    ct_event!(key press CONTROL-'a') => {
446                        if self.select_month(self.months[self.primary_idx()].start_date(), false) {
447                            CalOutcome::Selected
448                        } else {
449                            CalOutcome::Continue
450                        }
451                    }
452                    ct_event!(keycode press CONTROL-Home) => self.move_to_today(),
453                    ct_event!(keycode press PageUp) => {
454                        self.move_to_prev(Months::new(1), Days::new(0))
455                    }
456                    ct_event!(keycode press PageDown) => {
457                        self.move_to_next(Months::new(1), Days::new(0))
458                    }
459                    ct_event!(keycode press SHIFT-PageUp) => self.prev_month(1, true),
460                    ct_event!(keycode press SHIFT-PageDown) => self.next_month(1, true),
461
462                    ct_event!(keycode press Up) => self.prev_day(7, false),
463                    ct_event!(keycode press Down) => self.next_day(7, false),
464                    ct_event!(keycode press Left) => self.prev_day(1, false),
465                    ct_event!(keycode press Right) => self.next_day(1, false),
466                    ct_event!(keycode press SHIFT-Up) => self.prev_day(7, true),
467                    ct_event!(keycode press SHIFT-Down) => self.next_day(7, true),
468                    ct_event!(keycode press SHIFT-Left) => self.prev_day(1, true),
469                    ct_event!(keycode press SHIFT-Right) => self.next_day(1, true),
470
471                    ct_event!(keycode press ALT-Up) => self.prev_week(1, false),
472                    ct_event!(keycode press ALT-Down) => self.next_week(1, false),
473                    ct_event!(keycode press ALT_SHIFT-Up) => self.prev_week(1, true),
474                    ct_event!(keycode press ALT_SHIFT-Down) => self.next_week(1, true),
475
476                    _ => CalOutcome::Continue,
477                }
478            } else {
479                CalOutcome::Continue
480            }
481        });
482
483        r.or_else(|| self.handle(event, MouseOnly))
484    }
485}
486
487impl<const N: usize> HandleEvent<crossterm::event::Event, MouseOnly, CalOutcome>
488    for CalendarState<N, RangeSelection>
489{
490    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: MouseOnly) -> CalOutcome {
491        for i in 0..self.months.len() {
492            if self.months[i].gained_focus() {
493                self.set_primary_idx(i);
494                break;
495            }
496        }
497
498        let all_areas = self
499            .months
500            .iter()
501            .map(|v| v.area)
502            .reduce(|v, w| v.union(w))
503            .unwrap_or_default();
504        match event {
505            ct_event!(scroll up for x,y) if all_areas.contains((*x, *y).into()) => {
506                self.scroll_back(self.step())
507            }
508            ct_event!(scroll down for x,y) if all_areas.contains((*x, *y).into()) => {
509                self.scroll_forward(self.step())
510            }
511            _ => CalOutcome::Continue,
512        }
513    }
514}