Skip to main content

gpui_kit/datetime/
adapter.rs

1//! The seam between the date components and whoever owns the calendar.
2//!
3//! This crate has no calendar system, no time zone, no locale, and no notion
4//! of what day it is. Every one of those facts arrives through
5//! [`DateAdapter`], which the host implements over whatever date library it
6//! already depends on. A [`Day`] is an opaque token: the components carry it,
7//! compare it, and hand it back, and only the adapter that produced it knows
8//! what it means.
9
10use std::rc::Rc;
11
12use gpui::SharedString;
13
14/// One day, meaningful only to the adapter that produced it.
15///
16/// The ordering is part of the contract: an adapter must number days so that
17/// an earlier day compares less than a later one, because a range picker has
18/// to be able to say that an end comes before its start.
19#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
20pub struct Day(pub i64);
21
22/// One month, meaningful only to the adapter that produced it.
23///
24/// The components never take it apart and never do arithmetic on it. Moving to
25/// the next month is [`DateAdapter::shift_month`], because how many months a
26/// year has, and whether the answer is the same every year, is calendar work.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct MonthKey(pub i64);
29
30/// One slot in a month grid.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum MonthCell {
33    /// Nothing at all sits here. A grid that pads its first week with blanks
34    /// rather than with the previous month's days uses this.
35    Empty,
36    /// A day of the month the grid was asked for.
37    Day(Day),
38    /// A day of the month before or after, drawn so the grid is rectangular.
39    Adjacent(Day),
40}
41
42impl MonthCell {
43    pub fn day(self) -> Option<Day> {
44        match self {
45            Self::Empty => None,
46            Self::Day(day) | Self::Adjacent(day) => Some(day),
47        }
48    }
49
50    pub fn is_adjacent(self) -> bool {
51        matches!(self, Self::Adjacent(_))
52    }
53}
54
55/// A month laid out in weeks.
56///
57/// Every week must hold as many cells as [`DateAdapter::weekday_labels`]
58/// returns, in the same order, because the header and the body are drawn as
59/// one table.
60#[derive(Debug, Clone, Default, PartialEq, Eq)]
61pub struct MonthGrid {
62    pub weeks: Vec<Vec<MonthCell>>,
63}
64
65impl MonthGrid {
66    pub fn new(weeks: impl IntoIterator<Item = Vec<MonthCell>>) -> Self {
67        Self {
68            weeks: weeks.into_iter().collect(),
69        }
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.weeks
74            .iter()
75            .all(|week| week.iter().all(|cell| matches!(cell, MonthCell::Empty)))
76    }
77
78    /// Where `day` sits, as a week and a position inside it.
79    pub fn position_of(&self, day: Day) -> Option<(usize, usize)> {
80        self.weeks.iter().enumerate().find_map(|(week, cells)| {
81            cells
82                .iter()
83                .position(|cell| cell.day() == Some(day))
84                .map(|column| (week, column))
85        })
86    }
87}
88
89/// Whether a day may be picked, and if not, why not.
90///
91/// The reason is the host's own wording and is shown verbatim. The components
92/// never author one.
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum Selectability {
95    Selectable,
96    Blocked { reason: SharedString },
97}
98
99impl Selectability {
100    pub fn blocked(reason: impl Into<SharedString>) -> Self {
101        Self::Blocked {
102            reason: reason.into(),
103        }
104    }
105
106    pub fn is_selectable(&self) -> bool {
107        matches!(self, Self::Selectable)
108    }
109
110    pub fn reason(&self) -> Option<&SharedString> {
111        match self {
112            Self::Selectable => None,
113            Self::Blocked { reason } => Some(reason),
114        }
115    }
116}
117
118/// The clock the host keeps: how far its hours run, and what it calls the two
119/// halves of a twelve-hour day.
120///
121/// A crate that hard-coded `AM` and `PM` would be hard-coding a language, so
122/// the labels arrive here or the clock has none.
123#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct Clock {
125    pub hour_min: u32,
126    pub hour_max: u32,
127    pub minute_max: u32,
128    pub second_max: u32,
129    /// The two meridiem labels in order, when the host's clock has them.
130    pub meridiem: Option<(SharedString, SharedString)>,
131}
132
133impl Clock {
134    pub fn is_twelve_hour(&self) -> bool {
135        self.meridiem.is_some()
136    }
137
138    pub fn meridiem_label(&self, index: usize) -> Option<SharedString> {
139        let (first, second) = self.meridiem.as_ref()?;
140        match index {
141            0 => Some(first.clone()),
142            1 => Some(second.clone()),
143            _ => None,
144        }
145    }
146}
147
148/// A time of day, in the host's own clock.
149///
150/// `meridiem` indexes [`Clock::meridiem`] rather than naming a half of the
151/// day, because the naming belongs to the adapter.
152#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
153pub struct TimeOfDay {
154    pub hour: u32,
155    pub minute: u32,
156    pub second: Option<u32>,
157    pub meridiem: Option<usize>,
158}
159
160impl TimeOfDay {
161    pub fn new(hour: u32, minute: u32) -> Self {
162        Self {
163            hour,
164            minute,
165            second: None,
166            meridiem: None,
167        }
168    }
169
170    pub fn with_second(mut self, second: u32) -> Self {
171        self.second = Some(second);
172        self
173    }
174
175    pub fn with_meridiem(mut self, index: usize) -> Self {
176        self.meridiem = Some(index);
177        self
178    }
179}
180
181/// Everything the date components are not allowed to know.
182///
183/// One implementation of this is the whole seam. The components read facts
184/// from it, hand [`Day`] values back to it, and never derive a date fact
185/// themselves — including which month a keystroke moved to, which is
186/// [`shift_month`](DateAdapter::shift_month) rather than an addition.
187pub trait DateAdapter {
188    /// What day it is, or `None` when the host has not established one.
189    ///
190    /// `None` is a real answer and is rendered as one: no today ring is drawn
191    /// and no month is guessed from it.
192    fn today(&self) -> Option<Day>;
193
194    fn month_of(&self, day: Day) -> MonthKey;
195
196    fn month_grid(&self, month: MonthKey) -> MonthGrid;
197
198    /// What the month is called, already in the host's language.
199    fn month_label(&self, month: MonthKey) -> SharedString;
200
201    /// The weekday headings, already in the host's first-day-of-week order.
202    fn weekday_labels(&self) -> Vec<SharedString>;
203
204    /// The whole day, as a field would show it.
205    fn format_day(&self, day: Day) -> SharedString;
206
207    /// What the grid draws in the day's own cell, usually its number.
208    fn day_label(&self, day: Day) -> SharedString;
209
210    /// Reads a typed date. The error is the message shown to the typist, word
211    /// for word; the components never write one of their own.
212    fn parse_day(&self, text: &str) -> Result<Day, SharedString>;
213
214    /// The month `delta` months away, or `None` when the host will not go
215    /// there.
216    fn shift_month(&self, month: MonthKey, delta: i32) -> Option<MonthKey>;
217
218    fn is_selectable(&self, day: Day) -> Selectability;
219
220    /// Every day from `start` to `end` inclusive, or `None` when the host
221    /// cannot enumerate them.
222    ///
223    /// A range picker uses this to name the blocked days inside a range. With
224    /// `None` it says it could not check, rather than claiming a range is
225    /// clear because it never looked.
226    fn days_in(&self, start: Day, end: Day) -> Option<Vec<Day>>;
227
228    fn clock(&self) -> Clock;
229
230    /// The time as the host would write it.
231    fn format_time(&self, time: TimeOfDay) -> SharedString;
232}
233
234/// The adapter as the components hold it.
235pub type SharedDateAdapter = Rc<dyn DateAdapter>;
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn a_cell_reports_the_day_it_carries_whichever_month_owns_it() {
243        assert_eq!(MonthCell::Empty.day(), None);
244        assert_eq!(MonthCell::Day(Day(3)).day(), Some(Day(3)));
245        assert_eq!(MonthCell::Adjacent(Day(4)).day(), Some(Day(4)));
246        assert!(MonthCell::Adjacent(Day(4)).is_adjacent());
247        assert!(!MonthCell::Day(Day(4)).is_adjacent());
248    }
249
250    #[test]
251    fn a_day_is_found_by_identity_rather_than_by_position() {
252        let grid = MonthGrid::new([
253            vec![MonthCell::Empty, MonthCell::Day(Day(1))],
254            vec![MonthCell::Day(Day(2)), MonthCell::Adjacent(Day(3))],
255        ]);
256        assert_eq!(grid.position_of(Day(1)), Some((0, 1)));
257        assert_eq!(grid.position_of(Day(3)), Some((1, 1)));
258        assert_eq!(grid.position_of(Day(9)), None);
259    }
260
261    #[test]
262    fn a_grid_of_nothing_but_blanks_is_empty() {
263        assert!(MonthGrid::new([vec![MonthCell::Empty; 7]]).is_empty());
264        assert!(!MonthGrid::new([vec![MonthCell::Day(Day(1))]]).is_empty());
265    }
266}