Skip to main content

rat_widget/calendar/
range_selection.rs

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