facett-app/src/datepicker.rs
//! **`DatePicker` — a month calendar.** A self-contained date picker (no `chrono`
//! / `time` dep — day-of-week via Sakamoto's algorithm, so it builds anywhere incl.
//! wasm). The viewed month + selected date live in the struct (FC-1), headless
//! (`select`, `prev_month`/`next_month`), observable (`state_json`).
use serde_json::{Value, json};
const MONTHS: [&str; 12] = [
"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December",
];
fn is_leap(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
fn days_in_month(y: i32, m: u32) -> u32 {
match m {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap(y) {
29
} else {
28
}
}
_ => 30,
}
}
/// Sakamoto's day-of-week: 0 = Sunday … 6 = Saturday.
fn day_of_week(y: i32, m: u32, d: u32) -> u32 {
const T: [i32; 12] = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
let yy = if m < 3 { y - 1 } else { y };
(yy + yy / 4 - yy / 100 + yy / 400 + T[(m - 1) as usize] + d as i32).rem_euclid(7) as u32
}
/// A month-grid date picker.
pub struct DatePicker {
/// The viewed month.
year: i32,
month: u32,
selected: Option<(i32, u32, u32)>,
}
impl DatePicker {
/// View a given month (`month` is 1–12).
pub fn new(year: i32, month: u32) -> Self {
Self { year, month: month.clamp(1, 12), selected: None }
}
pub fn with_selected(mut self, y: i32, m: u32, d: u32) -> Self {
self.select(y, m, d);
self.year = y;
self.month = m.clamp(1, 12);
self
}
pub fn selected(&self) -> Option<(i32, u32, u32)> {
self.selected
}
/// Select a date (clamps the day to the month length). Also views that month.
pub fn select(&mut self, y: i32, m: u32, d: u32) {
let m = m.clamp(1, 12);
let d = d.clamp(1, days_in_month(y, m));
self.selected = Some((y, m, d));
}
pub fn prev_month(&mut self) {
if self.month == 1 {
self.month = 12;
self.year -= 1;
} else {
self.month -= 1;
}
}
pub fn next_month(&mut self) {
if self.month == 12 {
self.month = 1;
self.year += 1;
} else {
self.month += 1;
}
}
/// Draw the calendar; returns the date newly picked this frame.
pub fn show(&mut self, ui: &mut egui::Ui) -> Option<(i32, u32, u32)> {
let mut picked = None;
ui.horizontal(|ui| {
if ui.button("◀").clicked() {
self.prev_month();
}
ui.strong(format!("{} {}", MONTHS[(self.month - 1) as usize], self.year));
if ui.button("▶").clicked() {
self.next_month();
}
});
ui.horizontal(|ui| {
for wd in ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"] {
ui.add_sized([26.0, 16.0], egui::Label::new(egui::RichText::new(wd).weak()));
}
});
let first_dow = day_of_week(self.year, self.month, 1);
let dim = days_in_month(self.year, self.month);
let mut day = 1u32;
for _week in 0..6 {
if day > dim {
break;
}
ui.horizontal(|ui| {
for col in 0..7 {
let cell = _week * 7 + col;
if cell < first_dow || day > dim {
ui.add_sized([26.0, 22.0], egui::Label::new(""));
} else {
let is_sel = self.selected == Some((self.year, self.month, day));
if ui.add_sized([26.0, 22.0], egui::Button::selectable(is_sel, day.to_string())).clicked() {
self.selected = Some((self.year, self.month, day));
picked = Some((self.year, self.month, day));
}
day += 1;
}
}
});
}
picked
}
pub fn state_json(&self) -> Value {
json!({
"view": format!("{:04}-{:02}", self.year, self.month),
"selected": self.selected.map(|(y, m, d)| format!("{y:04}-{m:02}-{d:02}")),
})
}
}