2024-03-14 16:16:51 +01:00
|
|
|
// Copyright 2024 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
|
2024-05-20 20:01:47 +02:00
|
|
|
//! A widget that displays an interactive calendar.
|
|
|
|
|
|
2024-03-14 16:48:51 +01:00
|
|
|
use std::cmp;
|
|
|
|
|
|
2024-11-03 19:16:37 +01:00
|
|
|
use crate::iced_core::{Alignment, Length, Padding};
|
2025-03-21 03:17:59 +01:00
|
|
|
use crate::widget::{Grid, button, column, grid, icon, row, text};
|
2025-09-03 17:35:37 +00:00
|
|
|
use apply::Apply;
|
2025-10-07 23:29:29 +02:00
|
|
|
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
|
|
|
|
|
use crate::fl;
|
2024-03-14 16:16:51 +01:00
|
|
|
|
2024-05-20 20:01:47 +02:00
|
|
|
/// A widget that displays an interactive calendar.
|
2024-03-14 16:16:51 +01:00
|
|
|
pub fn calendar<M>(
|
2025-01-16 05:30:21 +00:00
|
|
|
model: &CalendarModel,
|
2024-03-15 05:39:03 -07:00
|
|
|
on_select: impl Fn(NaiveDate) -> M + 'static,
|
2025-01-16 05:30:21 +00:00
|
|
|
on_prev: impl Fn() -> M + 'static,
|
|
|
|
|
on_next: impl Fn() -> M + 'static,
|
2025-05-19 13:06:09 -04:00
|
|
|
first_day_of_week: Weekday,
|
2025-10-05 12:27:32 +10:00
|
|
|
) -> Calendar<'_, M> {
|
2024-03-14 16:16:51 +01:00
|
|
|
Calendar {
|
2025-01-16 05:30:21 +00:00
|
|
|
model,
|
2024-03-14 16:16:51 +01:00
|
|
|
on_select: Box::new(on_select),
|
2025-01-16 05:30:21 +00:00
|
|
|
on_prev: Box::new(on_prev),
|
|
|
|
|
on_next: Box::new(on_next),
|
2025-05-19 13:06:09 -04:00
|
|
|
first_day_of_week,
|
2024-03-14 16:16:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-15 05:39:03 -07:00
|
|
|
pub fn set_day(date_selected: NaiveDate, day: u32) -> NaiveDate {
|
2024-03-14 16:48:51 +01:00
|
|
|
let current = date_selected.day();
|
|
|
|
|
|
|
|
|
|
let new_date = match current.cmp(&day) {
|
|
|
|
|
cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)),
|
|
|
|
|
|
|
|
|
|
cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)),
|
|
|
|
|
|
|
|
|
|
_ => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if let Some(new) = new_date {
|
2024-03-15 05:39:03 -07:00
|
|
|
new
|
|
|
|
|
} else {
|
|
|
|
|
date_selected
|
2024-03-14 16:48:51 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-19 11:58:57 +00:00
|
|
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
|
2025-01-16 05:30:21 +00:00
|
|
|
pub struct CalendarModel {
|
|
|
|
|
pub selected: NaiveDate,
|
2025-01-19 14:37:07 +00:00
|
|
|
pub visible: NaiveDate,
|
2024-03-14 16:48:51 +01:00
|
|
|
}
|
|
|
|
|
|
2025-01-16 05:30:21 +00:00
|
|
|
impl CalendarModel {
|
|
|
|
|
pub fn now() -> Self {
|
|
|
|
|
let now = Local::now();
|
|
|
|
|
let naive_now = NaiveDate::from(now.naive_local());
|
|
|
|
|
CalendarModel {
|
2025-03-14 11:56:21 -04:00
|
|
|
selected: naive_now,
|
2025-01-16 05:30:21 +00:00
|
|
|
visible: naive_now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 03:17:59 +01:00
|
|
|
#[inline]
|
2025-01-19 14:37:07 +00:00
|
|
|
pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self {
|
|
|
|
|
CalendarModel { selected, visible }
|
2025-01-16 05:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show_prev_month(&mut self) {
|
|
|
|
|
let prev_month_date = self
|
|
|
|
|
.visible
|
|
|
|
|
.checked_sub_months(Months::new(1))
|
|
|
|
|
.expect("valid naivedate");
|
|
|
|
|
|
2025-03-14 11:56:21 -04:00
|
|
|
self.visible = prev_month_date;
|
2025-01-16 05:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn show_next_month(&mut self) {
|
|
|
|
|
let next_month_date = self
|
|
|
|
|
.visible
|
|
|
|
|
.checked_add_months(Months::new(1))
|
|
|
|
|
.expect("valid naivedate");
|
|
|
|
|
|
2025-03-14 11:56:21 -04:00
|
|
|
self.visible = next_month_date;
|
2025-01-16 05:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-21 03:17:59 +01:00
|
|
|
#[inline]
|
2025-01-16 05:30:21 +00:00
|
|
|
pub fn set_prev_month(&mut self) {
|
|
|
|
|
self.show_prev_month();
|
2025-03-14 11:56:21 -04:00
|
|
|
self.selected = self.visible;
|
2025-01-16 05:30:21 +00:00
|
|
|
}
|
|
|
|
|
|
2025-03-21 03:17:59 +01:00
|
|
|
#[inline]
|
2025-01-16 05:30:21 +00:00
|
|
|
pub fn set_next_month(&mut self) {
|
|
|
|
|
self.show_next_month();
|
2025-03-14 11:56:21 -04:00
|
|
|
self.selected = self.visible;
|
2025-01-16 05:30:21 +00:00
|
|
|
}
|
2025-01-19 14:37:07 +00:00
|
|
|
|
2025-03-21 03:17:59 +01:00
|
|
|
#[inline]
|
2025-01-19 14:37:07 +00:00
|
|
|
pub fn set_selected_visible(&mut self, selected: NaiveDate) {
|
|
|
|
|
self.selected = selected;
|
2025-03-14 11:56:21 -04:00
|
|
|
self.visible = self.selected;
|
2025-01-19 14:37:07 +00:00
|
|
|
}
|
2024-03-14 16:48:51 +01:00
|
|
|
}
|
|
|
|
|
|
2024-03-14 16:16:51 +01:00
|
|
|
pub struct Calendar<'a, M> {
|
2025-01-16 05:30:21 +00:00
|
|
|
model: &'a CalendarModel,
|
2024-03-15 05:39:03 -07:00
|
|
|
on_select: Box<dyn Fn(NaiveDate) -> M>,
|
2025-01-16 05:30:21 +00:00
|
|
|
on_prev: Box<dyn Fn() -> M>,
|
|
|
|
|
on_next: Box<dyn Fn() -> M>,
|
2025-05-19 13:06:09 -04:00
|
|
|
first_day_of_week: Weekday,
|
2024-03-14 16:16:51 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl<'a, Message> From<Calendar<'a, Message>> for crate::Element<'a, Message>
|
|
|
|
|
where
|
|
|
|
|
Message: Clone + 'static,
|
|
|
|
|
{
|
|
|
|
|
fn from(this: Calendar<'a, Message>) -> Self {
|
2025-09-03 17:35:37 +00:00
|
|
|
macro_rules! icon {
|
|
|
|
|
($name:expr, $on_press:expr) => {{
|
|
|
|
|
#[cfg(target_os = "linux")]
|
2025-09-03 20:22:06 +02:00
|
|
|
let icon = { icon::from_name($name).apply(button::icon) };
|
2025-09-03 17:35:37 +00:00
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
|
|
|
let icon = {
|
2025-09-03 20:22:06 +02:00
|
|
|
icon::from_svg_bytes(include_bytes!(concat!("../../res/icons/", $name, ".svg")))
|
|
|
|
|
.symbolic(true)
|
|
|
|
|
.apply(button::icon)
|
2025-09-03 17:35:37 +00:00
|
|
|
};
|
2025-09-03 20:22:06 +02:00
|
|
|
icon.padding([0, 12]).on_press($on_press)
|
2025-09-03 17:35:37 +00:00
|
|
|
}};
|
|
|
|
|
}
|
2025-10-07 23:29:29 +02:00
|
|
|
macro_rules! translate_month {
|
|
|
|
|
($month:expr, $year:expr) => {{
|
|
|
|
|
match $month {
|
|
|
|
|
chrono::Month::January => fl!("january", year=$year),
|
|
|
|
|
chrono::Month::February => fl!("february", year=$year),
|
|
|
|
|
chrono::Month::March => fl!("march", year=$year),
|
|
|
|
|
chrono::Month::April => fl!("april", year=$year),
|
|
|
|
|
chrono::Month::May => fl!("may", year=$year),
|
|
|
|
|
chrono::Month::June => fl!("june", year=$year),
|
|
|
|
|
chrono::Month::July => fl!("july", year=$year),
|
|
|
|
|
chrono::Month::August => fl!("august", year=$year),
|
|
|
|
|
chrono::Month::September => fl!("september", year=$year),
|
|
|
|
|
chrono::Month::October => fl!("october", year=$year),
|
|
|
|
|
chrono::Month::November => fl!("november", year=$year),
|
|
|
|
|
chrono::Month::December => fl!("december", year=$year)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
}
|
|
|
|
|
macro_rules! translate_weekday {
|
|
|
|
|
($weekday:expr) => {{
|
|
|
|
|
match $weekday {
|
|
|
|
|
Weekday::Mon => fl!("monday"),
|
|
|
|
|
Weekday::Tue => fl!("tuesday"),
|
|
|
|
|
Weekday::Wed => fl!("wednesday"),
|
|
|
|
|
Weekday::Thu => fl!("thursday"),
|
|
|
|
|
Weekday::Fri => fl!("friday"),
|
|
|
|
|
Weekday::Sat => fl!("saturday"),
|
|
|
|
|
Weekday::Sun => fl!("sunday")
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let date = text(translate_month!(
|
|
|
|
|
Month::try_from(this.model.visible.month() as u8)
|
|
|
|
|
.expect("Previously valid month is suddenly invalid"),
|
|
|
|
|
this.model.visible.year())).size(18);
|
2024-03-14 16:16:51 +01:00
|
|
|
|
|
|
|
|
let month_controls = row::with_capacity(2)
|
2025-09-03 17:35:37 +00:00
|
|
|
.push(icon!("go-previous-symbolic", (this.on_prev)()))
|
|
|
|
|
.push(icon!("go-next-symbolic", (this.on_next)()));
|
2024-03-14 16:16:51 +01:00
|
|
|
|
|
|
|
|
// Calender
|
|
|
|
|
let mut calendar_grid: Grid<'_, Message> =
|
|
|
|
|
grid().padding([0, 12].into()).width(Length::Fill);
|
|
|
|
|
|
2025-05-16 17:02:16 +02:00
|
|
|
let mut first_day_of_week = this.first_day_of_week;
|
2024-03-14 16:16:51 +01:00
|
|
|
for _ in 0..7 {
|
|
|
|
|
calendar_grid = calendar_grid.push(
|
2025-10-07 23:29:29 +02:00
|
|
|
text(translate_weekday!(first_day_of_week))
|
2024-03-14 16:16:51 +01:00
|
|
|
.size(12)
|
|
|
|
|
.width(Length::Fixed(36.0))
|
2024-11-03 19:16:37 +01:00
|
|
|
.align_x(Alignment::Center),
|
2024-03-14 16:16:51 +01:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
first_day_of_week = first_day_of_week.succ();
|
|
|
|
|
}
|
|
|
|
|
calendar_grid = calendar_grid.insert_row();
|
|
|
|
|
|
|
|
|
|
let monday = get_calender_first(
|
2025-01-16 05:30:21 +00:00
|
|
|
this.model.visible.year(),
|
|
|
|
|
this.model.visible.month(),
|
2024-03-14 16:16:51 +01:00
|
|
|
first_day_of_week,
|
|
|
|
|
);
|
|
|
|
|
let mut day_iter = monday.iter_days();
|
|
|
|
|
for i in 0..42 {
|
|
|
|
|
if i > 0 && i % 7 == 0 {
|
|
|
|
|
calendar_grid = calendar_grid.insert_row();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let date = day_iter.next().unwrap();
|
2025-01-16 05:30:21 +00:00
|
|
|
let is_currently_viewed_month = date.month() == this.model.visible.month()
|
|
|
|
|
&& date.year_ce() == this.model.visible.year_ce();
|
|
|
|
|
let is_currently_selected_month = date.month() == this.model.selected.month()
|
|
|
|
|
&& date.year_ce() == this.model.selected.year_ce();
|
|
|
|
|
let is_currently_selected_day =
|
|
|
|
|
date.day() == this.model.selected.day() && is_currently_selected_month;
|
|
|
|
|
|
|
|
|
|
calendar_grid = calendar_grid.push(date_button(
|
|
|
|
|
date,
|
|
|
|
|
is_currently_viewed_month,
|
|
|
|
|
is_currently_selected_day,
|
|
|
|
|
&this.on_select,
|
|
|
|
|
));
|
2024-03-14 16:16:51 +01:00
|
|
|
}
|
|
|
|
|
|
2025-10-11 13:36:35 +10:00
|
|
|
let content_list = column::with_children([
|
|
|
|
|
row::with_children([
|
2025-01-16 05:30:21 +00:00
|
|
|
date.into(),
|
2024-03-14 16:16:51 +01:00
|
|
|
crate::widget::Space::with_width(Length::Fill).into(),
|
|
|
|
|
month_controls.into(),
|
|
|
|
|
])
|
|
|
|
|
.padding([12, 20])
|
|
|
|
|
.into(),
|
|
|
|
|
calendar_grid.into(),
|
|
|
|
|
padded_control(crate::widget::divider::horizontal::default()).into(),
|
|
|
|
|
])
|
2024-03-14 16:48:51 +01:00
|
|
|
.width(315)
|
2024-03-14 16:16:51 +01:00
|
|
|
.padding([8, 0]);
|
|
|
|
|
|
|
|
|
|
Self::new(content_list)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-23 12:40:10 -04:00
|
|
|
fn date_button<Message: Clone + 'static>(
|
2024-03-15 05:39:03 -07:00
|
|
|
date: NaiveDate,
|
2025-01-16 05:30:21 +00:00
|
|
|
is_currently_viewed_month: bool,
|
|
|
|
|
is_currently_selected_day: bool,
|
2024-03-15 05:39:03 -07:00
|
|
|
on_select: &dyn Fn(NaiveDate) -> Message,
|
2024-05-17 19:08:43 +02:00
|
|
|
) -> crate::widget::Button<'static, Message> {
|
2025-01-16 05:30:21 +00:00
|
|
|
let style = if is_currently_selected_day {
|
2024-10-16 20:36:46 -04:00
|
|
|
button::ButtonClass::Suggested
|
2024-03-14 16:16:51 +01:00
|
|
|
} else {
|
2024-10-16 20:36:46 -04:00
|
|
|
button::ButtonClass::Text
|
2024-03-14 16:16:51 +01:00
|
|
|
};
|
|
|
|
|
|
2024-11-03 19:16:37 +01:00
|
|
|
let button = button::custom(text(format!("{}", date.day())).center())
|
|
|
|
|
.class(style)
|
|
|
|
|
.height(Length::Fixed(36.0))
|
|
|
|
|
.width(Length::Fixed(36.0));
|
2024-03-14 16:16:51 +01:00
|
|
|
|
2025-01-16 05:30:21 +00:00
|
|
|
if is_currently_viewed_month {
|
2024-03-15 05:39:03 -07:00
|
|
|
button.on_press((on_select)(set_day(date, date.day())))
|
2024-03-14 16:16:51 +01:00
|
|
|
} else {
|
|
|
|
|
button
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Gets the first date that will be visible on the calender
|
|
|
|
|
#[must_use]
|
|
|
|
|
pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate {
|
|
|
|
|
let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
|
|
|
|
|
let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from
|
|
|
|
|
date.checked_sub_days(Days::new(num_days as u64)).unwrap()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Refactor to use same function from applet module.
|
|
|
|
|
fn padded_control<'a, Message>(
|
|
|
|
|
content: impl Into<crate::Element<'a, Message>>,
|
|
|
|
|
) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> {
|
|
|
|
|
crate::widget::container(content)
|
|
|
|
|
.padding(menu_control_padding())
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-21 03:17:59 +01:00
|
|
|
#[inline]
|
2024-03-14 16:16:51 +01:00
|
|
|
fn menu_control_padding() -> Padding {
|
2024-08-02 20:00:16 +02:00
|
|
|
let guard = crate::theme::THEME.lock().unwrap();
|
|
|
|
|
let cosmic = guard.cosmic();
|
|
|
|
|
[cosmic.space_xxs(), cosmic.space_m()].into()
|
2024-03-14 16:16:51 +01:00
|
|
|
}
|