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        let mut r = match event {
334            ct_event!(mouse any for m)
335                if self.mouse.drag(
336                    &[self.area_cal, self.area_weeknum], //
337                    m,
338                ) =>
339            {
340                if self.mouse.drag.get() == Some(0) {
341                    if let Some(sel) = item_at(&self.area_days, m.column, m.row) {
342                        self.select_day(sel, true)
343                    } else {
344                        let mut r = CalOutcome::Continue;
345
346                        let mut above = self.area_cal;
347                        above.y -= 2;
348                        above.height = 3;
349                        if above.contains((m.column, m.row).into()) {
350                            r = self.select_day(0, true);
351                        } else {
352                            let mut below = self.area_cal;
353                            below.y = below.bottom().saturating_sub(1);
354                            below.height = 2;
355                            if below.contains((m.column, m.row).into()) {
356                                r = self.select_day(self.end_date().day0() as usize, true);
357                            }
358                        }
359                        r
360                    }
361                } else if self.mouse.drag.get() == Some(1) {
362                    if let Some(sel) = item_at(&self.area_weeks, m.column, m.row) {
363                        self.select_week(sel, true)
364                    } else {
365                        let mut r = CalOutcome::Continue;
366
367                        let mut above = self.area_weeknum;
368                        above.y -= 2;
369                        above.height = 3;
370                        if above.contains((m.column, m.row).into()) {
371                            r = self.select_week(0, true);
372                        } else {
373                            let mut below = self.area_cal;
374                            below.y = below.bottom().saturating_sub(1);
375                            below.height = 2;
376                            if below.contains((m.column, m.row).into()) {
377                                r = self.select_week(self.end_date().day0() as usize, true);
378                            }
379                        }
380                        r
381                    }
382                } else {
383                    CalOutcome::Continue
384                }
385            }
386            _ => CalOutcome::Continue,
387        };
388
389        r = r.or_else(|| match event {
390            ct_event!(mouse down Left for x, y) => {
391                if let Some(sel) = item_at(&self.area_weeks, *x, *y) {
392                    self.select_week(sel, false)
393                } else if let Some(sel) = item_at(&self.area_days, *x, *y) {
394                    self.select_day(sel, false)
395                } else {
396                    CalOutcome::Continue
397                }
398            }
399            ct_event!(mouse down CONTROL-Left for x, y) => {
400                if let Some(sel) = item_at(&self.area_weeks, *x, *y) {
401                    self.select_week(sel, true)
402                } else if let Some(sel) = item_at(&self.area_days, *x, *y) {
403                    self.select_day(sel, true)
404                } else {
405                    CalOutcome::Continue
406                }
407            }
408            _ => CalOutcome::Continue,
409        });
410
411        r
412    }
413}
414
415impl<const N: usize> HandleEvent<Event, Regular, CalOutcome> for CalendarState<N, RangeSelection> {
416    fn handle(&mut self, event: &Event, _qualifier: Regular) -> CalOutcome {
417        let mut r = 'f: {
418            for month in &mut self.months {
419                let r = month.handle(event, Regular);
420                if r == CalOutcome::Selected {
421                    self.focus_lead();
422                    break 'f r;
423                }
424            }
425            CalOutcome::Continue
426        };
427        // Enable drag for all months.
428        if r.is_consumed() {
429            let mut drag = None;
430            for m in &self.months {
431                if let Some(d) = m.mouse.drag.get() {
432                    drag = Some(d);
433                    break;
434                }
435            }
436            if drag.is_some() {
437                for m in &self.months {
438                    m.mouse.drag.set(drag);
439                }
440            }
441        }
442
443        r = r.or_else(|| {
444            if self.is_focused() {
445                match event {
446                    ct_event!(key press CONTROL-'a') => {
447                        if self.select_month(self.months[self.primary_idx()].start_date(), false) {
448                            CalOutcome::Selected
449                        } else {
450                            CalOutcome::Continue
451                        }
452                    }
453                    ct_event!(keycode press CONTROL-Home) => self.move_to_today(),
454                    ct_event!(keycode press PageUp) => {
455                        self.move_to_prev(Months::new(1), Days::new(0))
456                    }
457                    ct_event!(keycode press PageDown) => {
458                        self.move_to_next(Months::new(1), Days::new(0))
459                    }
460                    ct_event!(keycode press SHIFT-PageUp) => self.prev_month(1, true),
461                    ct_event!(keycode press SHIFT-PageDown) => self.next_month(1, true),
462
463                    ct_event!(keycode press Up) => self.prev_day(7, false),
464                    ct_event!(keycode press Down) => self.next_day(7, false),
465                    ct_event!(keycode press Left) => self.prev_day(1, false),
466                    ct_event!(keycode press Right) => self.next_day(1, false),
467                    ct_event!(keycode press SHIFT-Up) => self.prev_day(7, true),
468                    ct_event!(keycode press SHIFT-Down) => self.next_day(7, true),
469                    ct_event!(keycode press SHIFT-Left) => self.prev_day(1, true),
470                    ct_event!(keycode press SHIFT-Right) => self.next_day(1, true),
471
472                    ct_event!(keycode press ALT-Up) => self.prev_week(1, false),
473                    ct_event!(keycode press ALT-Down) => self.next_week(1, false),
474                    ct_event!(keycode press ALT_SHIFT-Up) => self.prev_week(1, true),
475                    ct_event!(keycode press ALT_SHIFT-Down) => self.next_week(1, true),
476
477                    _ => CalOutcome::Continue,
478                }
479            } else {
480                CalOutcome::Continue
481            }
482        });
483
484        r.or_else(|| self.handle(event, MouseOnly))
485    }
486}
487
488impl<const N: usize> HandleEvent<Event, MouseOnly, CalOutcome>
489    for CalendarState<N, RangeSelection>
490{
491    fn handle(&mut self, event: &Event, _qualifier: MouseOnly) -> CalOutcome {
492        for i in 0..self.months.len() {
493            if self.months[i].gained_focus() {
494                self.set_primary_idx(i);
495                break;
496            }
497        }
498
499        let all_areas = self
500            .months
501            .iter()
502            .map(|v| v.area)
503            .reduce(|v, w| v.union(w))
504            .unwrap_or_default();
505        match event {
506            ct_event!(scroll up for x,y) if all_areas.contains((*x, *y).into()) => {
507                self.scroll_back(self.step())
508            }
509            ct_event!(scroll down for x,y) if all_areas.contains((*x, *y).into()) => {
510                self.scroll_forward(self.step())
511            }
512            _ => CalOutcome::Continue,
513        }
514    }
515}