Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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}")),
        })
    }
}