use chrono::{Datelike, Local, NaiveDate, Weekday};
use gpui::{Context, Render, white};
use crate::prelude::*;
const WEEKDAYS: [&str; 7] = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const MONTHS: [&str; 12] = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
fn month_grid(year: i32, month: u32) -> Vec<Option<u32>> {
let first = NaiveDate::from_ymd_opt(year, month, 1).expect("valid month");
let days_in_month = if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1)
}
.expect("valid month")
.signed_duration_since(first)
.num_days() as u32;
let leading = match first.weekday() {
Weekday::Sun => 0,
Weekday::Mon => 1,
Weekday::Tue => 2,
Weekday::Wed => 3,
Weekday::Thu => 4,
Weekday::Fri => 5,
Weekday::Sat => 6,
};
let mut cells = vec![None; leading as usize];
for day in 1..=days_in_month {
cells.push(Some(day));
}
while cells.len() % 7 != 0 {
cells.push(None);
}
cells
}
pub struct Calendar {
view_year: i32,
view_month: u32,
selected: Option<NaiveDate>,
disabled: Vec<NaiveDate>,
}
impl Calendar {
pub fn new() -> Self {
let today = Local::now().date_naive();
Self {
view_year: today.year(),
view_month: today.month(),
selected: None,
disabled: Vec::new(),
}
}
pub fn selected(mut self, date: NaiveDate) -> Self {
self.selected = Some(date);
self.view_year = date.year();
self.view_month = date.month();
self
}
pub fn disabled_dates(mut self, dates: impl IntoIterator<Item = NaiveDate>) -> Self {
self.disabled = dates.into_iter().collect();
self
}
pub fn selection(&self) -> Option<NaiveDate> {
self.selected
}
fn is_disabled(&self, date: NaiveDate) -> bool {
self.disabled.iter().any(|d| *d == date)
}
fn prev_month(&mut self, cx: &mut Context<Self>) {
if self.view_month == 1 {
self.view_month = 12;
self.view_year -= 1;
} else {
self.view_month -= 1;
}
cx.notify();
}
fn next_month(&mut self, cx: &mut Context<Self>) {
if self.view_month == 12 {
self.view_month = 1;
self.view_year += 1;
} else {
self.view_month += 1;
}
cx.notify();
}
}
impl Render for Calendar {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let today = Local::now().date_naive();
let year = self.view_year;
let month = self.view_month;
let selected = self.selected;
let grid = month_grid(year, month);
let month_label = format!("{} {}", MONTHS[month as usize - 1], year);
let weekday_header = h_flex().children(WEEKDAYS.iter().map(|day| {
div().flex_1().text_center().child(
Label::new(*day)
.size(LabelSize::XSmall)
.color(Color::Muted)
.weight(gpui::FontWeight::MEDIUM),
)
}));
let mut weeks = v_flex().gap_1();
for week in grid.chunks(7) {
let mut row = h_flex().gap_1();
for (col, day) in week.iter().enumerate() {
let Some(day) = *day else {
row = row.child(div().flex_1().h(px(32.)));
continue;
};
let date = NaiveDate::from_ymd_opt(year, month, day).expect("valid day");
let is_today = date == today;
let is_selected = selected == Some(date);
let is_disabled = self.is_disabled(date);
let hover = semantic::hover_bg(cx);
let mut cell = div()
.id(ElementId::Name(
format!("calendar-day-{year}-{month}-{day}").into(),
))
.flex_1()
.h(px(32.))
.flex()
.items_center()
.justify_center()
.rounded_md()
.cursor_pointer();
if is_disabled {
cell = cell.cursor_default().child(
Label::new(day.to_string())
.size(LabelSize::Small)
.color(Color::Muted),
);
} else if is_selected {
cell = cell.bg(palette::primary(600)).child(
Label::new(day.to_string())
.size(LabelSize::Small)
.color(Color::Custom(white())),
);
} else {
cell = cell
.debug_selector(move || format!("CALENDAR-DAY-{year}-{month}-{day}"))
.hover(move |s| s.bg(hover))
.when(is_today, |this| {
this.border_1().border_color(palette::primary(500))
})
.on_click(cx.listener(move |this, _, _, cx| {
this.selected = Some(date);
cx.notify();
}))
.child(Label::new(day.to_string()).size(LabelSize::Small));
}
row = row.child(cell);
let _ = col;
}
weeks = weeks.child(row);
}
v_flex()
.id("calendar")
.w(px(280.))
.p_3()
.gap_3()
.rounded_md()
.border_1()
.border_color(semantic::border(cx))
.bg(semantic::elevated_surface(cx))
.child(
h_flex()
.items_center()
.justify_between()
.child(
IconButton::new("calendar-prev", IconName::ChevronLeft)
.on_click(cx.listener(|this, _, _, cx| this.prev_month(cx))),
)
.child(Label::new(month_label).weight(gpui::FontWeight::SEMIBOLD))
.child(
IconButton::new("calendar-next", IconName::ChevronRight)
.on_click(cx.listener(|this, _, _, cx| this.next_month(cx))),
),
)
.child(weekday_header)
.child(weeks)
}
}
#[derive(IntoElement, RegisterComponent)]
pub struct CalendarPreview;
impl RenderOnce for CalendarPreview {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
cx.new(|_| Calendar::new())
}
}
impl Component for CalendarPreview {
fn scope() -> ComponentScope {
ComponentScope::Input
}
fn description() -> Option<&'static str> {
Some("Single-month date grid with day selection and today indicator.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
CalendarPreview.render(window, cx).into_any_element().into()
}
}