use chrono::{Datelike, Local};
use hikari_palette::classes::{CalendarClass, ClassesBuilder, TypedClass};
use crate::prelude::*;
use crate::styled::StyledComponent;
fn get_current_date() -> (i32, u32) {
let now = Local::now();
(now.year(), now.month())
}
pub struct CalendarComponent;
#[define_props]
pub struct CalendarProps {
#[default(2026)]
pub default_year: i32,
#[default(1)]
pub default_month: u32,
pub on_date_select: Option<EventHandler<(i32, u32, u32)>>,
#[default(1970)]
pub min_year: i32,
#[default(2100)]
pub max_year: i32,
pub class: String,
pub style: String,
}
fn is_leap_year(year: i32) -> bool {
(year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
}
fn days_in_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if is_leap_year(year) {
29
} else {
28
}
}
_ => 30,
}
}
fn first_day_of_month(year: i32, month: u32) -> u32 {
let m = month as i32;
let y = if m < 3 { year - 1 } else { year };
let k = y % 100;
let j = year / 100;
let adjusted_m = if m < 3 { m + 12 } else { m };
let h = (1i32 + (13 * (adjusted_m + 1)) / 5 + k + k / 4 + j / 4 + 5 * j) % 7;
((h + 1) % 7) as u32
}
const MONTH_NAMES: [&str; 12] = [
"一月",
"二月",
"三月",
"四月",
"五月",
"六月",
"七月",
"八月",
"九月",
"十月",
"十一月",
"十二月",
];
const WEEKDAY_NAMES: [&str; 7] = ["日", "一", "二", "三", "四", "五", "六"];
#[component]
pub fn Calendar(props: CalendarProps) -> Element {
let current_year = use_signal(|| props.default_year);
let current_month = use_signal(|| props.default_month);
let selected_day = use_signal(|| 1u32);
let month = current_month.read();
let year = current_year.read();
let sel_day = selected_day.read();
let days_count = days_in_month(year, month);
let first_day = first_day_of_month(year, month);
let cal_class = CalendarClass::Calendar.class_name();
let header_class = CalendarClass::CalendarHeader.class_name();
let nav_class = CalendarClass::CalendarNav.class_name();
let nav_btn_class = CalendarClass::CalendarNavButton.class_name();
let title_class = CalendarClass::CalendarTitle.class_name();
let weekdays_class = CalendarClass::CalendarWeekdays.class_name();
let weekday_class = CalendarClass::CalendarWeekday.class_name();
let grid_class = CalendarClass::CalendarGrid.class_name();
let day_cell_class = CalendarClass::CalendarDayCell.class_name();
let day_class = CalendarClass::CalendarDay.class_name();
let selected_day_class = CalendarClass::CalendarDaySelected.class_name();
let calendar_classes = ClassesBuilder::new()
.add_typed(CalendarClass::Calendar)
.add(&props.class)
.build();
let weekday_headers: Vec<VNode> = WEEKDAY_NAMES
.iter()
.map(|weekday| {
VNode::Element(
VElement::new("div")
.class(weekday_class)
.child(VNode::Text(VText::new(weekday))),
)
})
.collect();
let empty_cells: Vec<VNode> = (0..first_day)
.map(|_| VNode::Element(VElement::new("div").class(day_cell_class)))
.collect();
let day_cells: Vec<VNode> = (1..=days_count)
.map(|day| {
let is_selected = day == sel_day;
let day_cell_cls = day_cell_class;
let inner_class = if is_selected {
format!("{} {}", day_class, selected_day_class)
} else {
day_class.to_string()
};
let cy = current_year.clone();
let cm = current_month.clone();
let sd = selected_day.clone();
let on_date_select = props.on_date_select.clone();
let y = year;
let m = month;
VNode::Element(
VElement::new("div")
.class(day_cell_cls)
.on_event("click", move |_e: Box<dyn EventData>| {
*sd.write() = day;
if let Some(ref handler) = on_date_select {
handler.call((y, m, day));
}
})
.child(VNode::Element(
VElement::new("div")
.class(inner_class)
.child(VNode::Text(VText::new(&day.to_string()))),
)),
)
})
.collect();
let cy_prev = current_year.clone();
let cm_prev = current_month.clone();
let min_yr = props.min_year;
let prev_btn = VNode::Element(
VElement::new("button")
.class(nav_btn_class)
.attr("disabled", year <= min_yr && month == 1)
.on_event("click", move |_e: Box<dyn EventData>| {
let ny = if month == 1 { year - 1 } else { year };
let nm = if month == 1 { 12 } else { month - 1 };
if ny >= min_yr {
*cy_prev.write() = ny;
*cm_prev.write() = nm;
}
})
.child(VNode::Text(VText::new("‹"))),
);
let cy_prev2 = current_year.clone();
let cm_prev2 = current_month.clone();
let prev_btn2 = VNode::Element(
VElement::new("button")
.class(nav_btn_class)
.on_event("click", move |_e: Box<dyn EventData>| {
let ny = if month == 1 { year - 1 } else { year };
let nm = if month == 1 { 12 } else { month - 1 };
if ny >= min_yr {
*cy_prev2.write() = ny;
*cm_prev2.write() = nm;
}
})
.child(VNode::Text(VText::new("◀"))),
);
let cy_today = current_year.clone();
let cm_today = current_month.clone();
let today_btn = VNode::Element(
VElement::new("button")
.class(nav_btn_class)
.on_event("click", move |_e: Box<dyn EventData>| {
let (today_year, today_month) = get_current_date();
if today_year >= min_yr && today_year <= props.max_year {
*cy_today.write() = today_year;
*cm_today.write() = today_month;
}
})
.child(VNode::Text(VText::new("今天"))),
);
let cy_next = current_year.clone();
let cm_next = current_month.clone();
let max_yr = props.max_year;
let next_btn = VNode::Element(
VElement::new("button")
.class(nav_btn_class)
.on_event("click", move |_e: Box<dyn EventData>| {
let ny = if month == 12 { year + 1 } else { year };
let nm = if month == 12 { 1 } else { month + 1 };
if ny <= max_yr {
*cy_next.write() = ny;
*cm_next.write() = nm;
}
})
.child(VNode::Text(VText::new("▶"))),
);
let cy_next2 = current_year.clone();
let cm_next2 = current_month.clone();
let next_btn2 = VNode::Element(
VElement::new("button")
.class(nav_btn_class)
.attr("disabled", year >= max_yr && month == 12)
.on_event("click", move |_e: Box<dyn EventData>| {
let ny = if month == 12 { year + 1 } else { year };
let nm = if month == 12 { 1 } else { month + 1 };
if ny <= max_yr {
*cy_next2.write() = ny;
*cm_next2.write() = nm;
}
})
.child(VNode::Text(VText::new("›"))),
);
let title_text = format!("{}年 {}", year, MONTH_NAMES[(month - 1) as usize]);
let mut all_day_cells = empty_cells;
all_day_cells.extend(day_cells);
rsx! {
div { class: calendar_classes, style: props.style,
div { class: cal_class,
div { class: header_class,
div { class: nav_class,
{prev_btn}
{prev_btn2}
{today_btn}
{next_btn}
{next_btn2}
}
div { class: title_class, "{title_text}" }
}
div { class: weekdays_class, ..weekday_headers }
div { class: grid_class, ..all_day_cells }
}
}
}
}
impl StyledComponent for CalendarComponent {
fn styles() -> &'static str {
r#"
.hi-calendar {
display: inline-block;
padding: 1rem;
background-color: var(--hi-color-bg-container);
border: 1px solid var(--hi-color-border);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.hi-calendar-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding: 0.5rem;
}
.hi-calendar-nav {
display: flex;
gap: 0.25rem;
}
.hi-calendar-nav-button {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 32px;
height: 32px;
padding: 0 0.5rem;
background-color: var(--hi-color-bg-elevated);
border: 1px solid var(--hi-color-border);
border-radius: 4px;
color: var(--hi-color-text-primary);
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s ease;
}
.hi-calendar-nav-button:hover:not(:disabled) {
background-color: var(--hi-color-primary);
color: white;
border-color: var(--hi-color-primary);
box-shadow: 0 0 8px var(--hi-glow-button-primary);
}
.hi-calendar-nav-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.hi-calendar-title {
font-size: 1rem;
font-weight: 500;
color: var(--hi-color-text-primary);
}
.hi-calendar-weekdays {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.25rem;
margin-bottom: 0.5rem;
}
.hi-calendar-weekday {
text-align: center;
font-size: 0.75rem;
font-weight: 500;
color: var(--hi-color-text-secondary);
padding: 0.5rem 0;
}
.hi-calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 0.25rem;
}
.hi-calendar-day-cell {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.hi-calendar-day {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: 0.5rem;
border-radius: 4px;
font-size: 0.875rem;
color: var(--hi-color-text-primary);
transition: all 0.2s ease;
}
.hi-calendar-day:hover {
background-color: var(--hi-color-primary-bg);
color: var(--hi-color-primary);
}
.hi-calendar-day.hi-calendar-day-selected {
background-color: var(--hi-color-primary);
color: white;
box-shadow: 0 0 12px var(--hi-glow-button-primary);
}
.hi-calendar-day-today {
border: 1px solid var(--hi-color-primary);
}
.hi-calendar-day-disabled {
opacity: 0.3;
cursor: not-allowed;
}
"#
}
fn name() -> &'static str {
"calendar"
}
}