libcosmic/src/widget/calendar.rs
2026-04-08 03:12:10 +02:00

266 lines
8.1 KiB
Rust

// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! A widget that displays an interactive calendar.
use crate::fl;
use crate::widget::{button, column, grid, icon, row, text};
use apply::Apply;
use iced::alignment::Vertical;
use iced_core::{Alignment, Length};
use jiff::{
ToSpan,
civil::{Date, Weekday},
};
/// A widget that displays an interactive calendar.
pub fn calendar<M>(
model: &CalendarModel,
on_select: impl Fn(Date) -> M + 'static,
on_prev: impl Fn() -> M + 'static,
on_next: impl Fn() -> M + 'static,
first_day_of_week: Weekday,
) -> Calendar<'_, M> {
Calendar {
model,
on_select: Box::new(on_select),
on_prev: Box::new(on_prev),
on_next: Box::new(on_next),
first_day_of_week,
}
}
pub fn set_day(date_selected: Date, day: i8) -> Date {
date_selected
.with()
.day(day)
.build()
.unwrap_or(date_selected)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct CalendarModel {
pub selected: Date,
pub visible: Date,
}
impl CalendarModel {
pub fn now() -> Self {
let now = jiff::Zoned::now().date();
CalendarModel {
selected: now,
visible: now,
}
}
#[inline]
pub fn new(selected: Date, visible: Date) -> Self {
CalendarModel { selected, visible }
}
pub fn show_prev_month(&mut self) {
self.visible = self.visible.checked_sub(1.month()).expect("valid date");
}
pub fn show_next_month(&mut self) {
self.visible = self.visible.checked_add(1.month()).expect("valid date");
}
#[inline]
pub fn set_prev_month(&mut self) {
self.show_prev_month();
self.selected = self.visible;
}
#[inline]
pub fn set_next_month(&mut self) {
self.show_next_month();
self.selected = self.visible;
}
#[inline]
pub fn set_selected_visible(&mut self, selected: Date) {
self.selected = selected;
self.visible = self.selected;
}
}
pub struct Calendar<'a, M> {
model: &'a CalendarModel,
on_select: Box<dyn Fn(Date) -> M>,
on_prev: Box<dyn Fn() -> M>,
on_next: Box<dyn Fn() -> M>,
first_day_of_week: Weekday,
}
impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
where
Message: Clone + 'static,
{
fn from(this: Calendar<'a, Message>) -> Self {
macro_rules! translate_month {
($month:expr, $year:expr) => {{
match $month {
1 => fl!("january", year = $year),
2 => fl!("february", year = $year),
3 => fl!("march", year = $year),
4 => fl!("april", year = $year),
5 => fl!("may", year = $year),
6 => fl!("june", year = $year),
7 => fl!("july", year = $year),
8 => fl!("august", year = $year),
9 => fl!("september", year = $year),
10 => fl!("october", year = $year),
11 => fl!("november", year = $year),
12 => fl!("december", year = $year),
_ => unreachable!(),
}
}};
}
macro_rules! translate_weekday {
($weekday:expr, short) => {{
match $weekday {
Weekday::Monday => fl!("mon"),
Weekday::Tuesday => fl!("tue"),
Weekday::Wednesday => fl!("wed"),
Weekday::Thursday => fl!("thu"),
Weekday::Friday => fl!("fri"),
Weekday::Saturday => fl!("sat"),
Weekday::Sunday => fl!("sun"),
}
}};
($weekday:expr, long) => {{
match $weekday {
Weekday::Monday => fl!("monday"),
Weekday::Tuesday => fl!("tuesday"),
Weekday::Wednesday => fl!("wednesday"),
Weekday::Thursday => fl!("thursday"),
Weekday::Friday => fl!("friday"),
Weekday::Saturday => fl!("saturday"),
Weekday::Sunday => fl!("sunday"),
}
}};
}
let date = text(translate_month!(
this.model.visible.month(),
this.model.visible.year()
))
.size(18);
let day = text::body(translate_weekday!(this.model.visible.weekday(), long));
let month_controls = row::with_capacity(2)
.spacing(8)
.push(
icon::from_name("go-previous-symbolic")
.apply(button::icon)
.on_press((this.on_prev)()),
)
.push(
icon::from_name("go-next-symbolic")
.apply(button::icon)
.on_press((this.on_next)()),
);
// Calendar
let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill);
let mut first_day_of_week = this.first_day_of_week;
for _ in 0..7 {
calendar_grid = calendar_grid.push(
text::caption(translate_weekday!(first_day_of_week, short))
.width(Length::Fixed(44.0))
.align_x(Alignment::Center),
);
first_day_of_week = first_day_of_week.next();
}
calendar_grid = calendar_grid.insert_row();
let first = get_calendar_first(
this.model.visible.year(),
this.model.visible.month(),
this.first_day_of_week,
);
let today = jiff::Zoned::now().date();
for i in 0..42 {
if i > 0 && i % 7 == 0 {
calendar_grid = calendar_grid.insert_row();
}
let date = first
.checked_add(i.days())
.expect("valid date in calendar range");
let is_currently_viewed_month =
date.first_of_month() == this.model.visible.first_of_month();
let is_currently_selected_month =
date.first_of_month() == this.model.selected.first_of_month();
let is_currently_selected_day =
date.day() == this.model.selected.day() && is_currently_selected_month;
let is_today = date == today;
calendar_grid = calendar_grid.push(date_button(
date,
is_currently_viewed_month,
is_currently_selected_day,
is_today,
&this.on_select,
));
}
let content_list = column::with_children([
row::with_children([
column([date.into(), day.into()]).into(),
crate::widget::space::horizontal()
.width(Length::Fill)
.into(),
month_controls.into(),
])
.align_y(Vertical::Center)
.padding([12, 20])
.into(),
calendar_grid.into(),
])
.width(360)
.padding([8, 0]);
Self::new(content_list)
}
}
fn date_button<Message: Clone + 'static>(
date: Date,
is_currently_viewed_month: bool,
is_currently_selected_day: bool,
is_today: bool,
on_select: &dyn Fn(Date) -> Message,
) -> crate::widget::Button<'static, Message> {
let style = if is_currently_selected_day {
button::ButtonClass::Suggested
} else if is_today {
button::ButtonClass::Standard
} else {
button::ButtonClass::Text
};
let button = button::custom(text(format!("{}", date.day())).center())
.class(style)
.height(Length::Fixed(44.0))
.width(Length::Fixed(44.0));
if is_currently_viewed_month {
button.on_press((on_select)(set_day(date, date.day())))
} else {
button
}
}
/// Gets the first date that will be visible on the calendar
#[must_use]
pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date {
let date = Date::new(year, month, 1).expect("valid date");
let num_days = date.weekday().since(from_weekday);
date.checked_sub(num_days.days()).expect("valid date")
}