teksilo_widgets/common/datetime/types.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Stable type aliases for the date/time types used across the four
5//! datetime widgets. Routes through `jiff` today; swap-friendly because
6//! the widgets never name `jiff` directly.
7
8pub use jiff::civil::{Date, DateTime, Time, Weekday};
9
10/// Year + month pair, with no day component. Used by `Calendar` to track
11/// the visible month independently of the current selection.
12///
13/// Stored as a `Date` anchored on the first of the month; the widget
14/// never reads the day field.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
16pub struct YearMonth {
17 year: i16,
18 month: i8,
19}
20
21impl YearMonth {
22 /// Construct a `YearMonth`. Year clamps to jiff's `-9999..=9999`
23 /// range; month clamps to `1..=12`.
24 pub fn new(year: i16, month: i8) -> Self {
25 let year = year.clamp(-9999, 9999);
26 let month = month.clamp(1, 12);
27 Self { year, month }
28 }
29
30 /// Year as i16. Range matches jiff: `-9999..=9999`.
31 pub fn year(self) -> i16 {
32 self.year
33 }
34
35 /// Month as i8 in `1..=12`.
36 pub fn month(self) -> i8 {
37 self.month
38 }
39
40 /// First day of this month as a `Date`.
41 pub fn first_day(self) -> Date {
42 Date::constant(self.year, self.month, 1)
43 }
44
45 /// Last day of this month as a `Date`.
46 pub fn last_day(self) -> Date {
47 self.first_day().last_of_month()
48 }
49
50 /// `YearMonth` containing the given date.
51 pub fn from_date(d: Date) -> Self {
52 Self {
53 year: d.year(),
54 month: d.month(),
55 }
56 }
57
58 /// The next calendar month (wraps year on December → January).
59 pub fn next_month(self) -> Self {
60 if self.month == 12 {
61 Self::new(self.year.saturating_add(1), 1)
62 } else {
63 Self::new(self.year, self.month + 1)
64 }
65 }
66
67 /// The previous calendar month (wraps year on January → December).
68 pub fn prev_month(self) -> Self {
69 if self.month == 1 {
70 Self::new(self.year.saturating_sub(1), 12)
71 } else {
72 Self::new(self.year, self.month - 1)
73 }
74 }
75
76 /// `n` months later (negative for earlier).
77 pub fn offset_months(self, n: i32) -> Self {
78 let total = self.year as i32 * 12 + (self.month as i32 - 1) + n;
79 let year = (total.div_euclid(12)).clamp(-9999, 9999) as i16;
80 let month = (total.rem_euclid(12) + 1) as i8;
81 Self::new(year, month)
82 }
83}
84
85/// `Weekday::from_monday_zero_offset` clamped to `0..=6` and
86/// infallible — the rest of the date code only ever traffics in valid
87/// offsets, and a panic here would point at a bug, not a user error.
88pub fn weekday_from_monday_zero(offset: i8) -> Weekday {
89 Weekday::from_monday_zero_offset(offset.rem_euclid(7))
90 .expect("monday-zero offset already in 0..=6")
91}
92
93/// Today's date in the system's local time zone.
94///
95/// Single source of truth for "today" across the calendar / date
96/// widgets — used by today-ring rendering, the today-button, the `T`
97/// keyboard shortcut, and any default-when-no-value selection. Falls
98/// back to a fixed sentinel only if the platform refuses to give us a
99/// local zone (jiff returns `Zoned::now` infallibly on Linux/macOS/
100/// Windows; other platforms with broken tzdb fall back to UTC then to
101/// the sentinel).
102pub fn today_local() -> Date {
103 jiff::Zoned::now().date()
104}