refactor(calendar): use jiff instead of chrono

This refactors the calendar widget to use `jiff` instead of `chrono`.
Also mostly matches the design of the widget to the time applet.
This commit is contained in:
Vukašin Vojinović 2026-02-17 20:32:21 +01:00 committed by Michael Murphy
parent b05f040e5f
commit 990e2e291b
5 changed files with 111 additions and 123 deletions

View file

@ -104,7 +104,7 @@ async-fs = { version = "2.2", optional = true }
async-std = { version = "1.13", optional = true } async-std = { version = "1.13", optional = true }
auto_enums = "0.8.7" auto_enums = "0.8.7"
cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true }
chrono = "0.4.43" jiff = "0.2"
cosmic-config = { path = "cosmic-config" } cosmic-config = { path = "cosmic-config" }
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true }
# Internationalization # Internationalization

View file

@ -1,12 +1,12 @@
[package] [package]
name = "calendar" name = "calendar"
version = "0.1.0" version = "1.0.0"
edition = "2021" edition = "2024"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
chrono = "0.4.42" jiff = "0.2"
[dependencies.libcosmic] [dependencies.libcosmic]
path = "../../" path = "../../"

View file

@ -3,10 +3,10 @@
//! Calendar widget example //! Calendar widget example
use chrono::NaiveDate;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::widget::calendar::CalendarModel; use cosmic::widget::calendar::CalendarModel;
use cosmic::{executor, iced, ApplicationExt, Element}; use cosmic::{ApplicationExt, Element, executor, iced};
use jiff::civil::{Date, Weekday};
/// Runs application with these settings /// Runs application with these settings
#[rustfmt::skip] #[rustfmt::skip]
@ -19,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
/// Messages that are used specifically by our [`App`]. /// Messages that are used specifically by our [`App`].
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum Message { pub enum Message {
DateSelected(NaiveDate), DateSelected(Date),
PrevMonth, PrevMonth,
NextMonth, NextMonth,
} }
@ -92,7 +92,7 @@ impl cosmic::Application for App {
|date| Message::DateSelected(date), |date| Message::DateSelected(date),
|| Message::PrevMonth, || Message::PrevMonth,
|| Message::NextMonth, || Message::NextMonth,
chrono::Weekday::Sun, Weekday::Sunday,
); );
content = content.push(calendar); content = content.push(calendar);

View file

@ -23,10 +23,17 @@ september = September { $year }
october = October { $year } october = October { $year }
november = November { $year } november = November { $year }
december = December { $year } december = December { $year }
monday = Mon monday = Monday
tuesday = Tue mon = Mon
wednesday = Wed tuesday = Tuesday
thursday = Thu tue = Tue
friday = Fri wednesday = Wednesday
saturday = Sat wed = Wed
sunday = Sun thursday = Thursday
thu = Thu
friday = Friday
fri = Fri
saturday = Saturday
sat = Sat
sunday = Sunday
sun = Sun

View file

@ -3,19 +3,20 @@
//! A widget that displays an interactive calendar. //! A widget that displays an interactive calendar.
use std::cmp;
use crate::fl; use crate::fl;
use crate::iced_core::{Alignment, Length, Padding}; use crate::iced_core::{Alignment, Length};
use crate::widget::{Grid, button, column, grid, icon, row, text}; use crate::widget::{button, column, grid, icon, row, text};
use apply::Apply; use apply::Apply;
use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday};
use iced::alignment::Vertical; use iced::alignment::Vertical;
use jiff::{
ToSpan,
civil::{Date, Weekday},
};
/// A widget that displays an interactive calendar. /// A widget that displays an interactive calendar.
pub fn calendar<M>( pub fn calendar<M>(
model: &CalendarModel, model: &CalendarModel,
on_select: impl Fn(NaiveDate) -> M + 'static, on_select: impl Fn(Date) -> M + 'static,
on_prev: impl Fn() -> M + 'static, on_prev: impl Fn() -> M + 'static,
on_next: impl Fn() -> M + 'static, on_next: impl Fn() -> M + 'static,
first_day_of_week: Weekday, first_day_of_week: Weekday,
@ -29,61 +30,40 @@ pub fn calendar<M>(
} }
} }
pub fn set_day(date_selected: NaiveDate, day: u32) -> NaiveDate { pub fn set_day(date_selected: Date, day: i8) -> Date {
let current = date_selected.day(); date_selected
.with()
let new_date = match current.cmp(&day) { .day(day)
cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)), .build()
.unwrap_or(date_selected)
cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)),
_ => None,
};
if let Some(new) = new_date {
new
} else {
date_selected
}
} }
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
pub struct CalendarModel { pub struct CalendarModel {
pub selected: NaiveDate, pub selected: Date,
pub visible: NaiveDate, pub visible: Date,
} }
impl CalendarModel { impl CalendarModel {
pub fn now() -> Self { pub fn now() -> Self {
let now = Local::now(); let now = jiff::Zoned::now().date();
let naive_now = NaiveDate::from(now.naive_local());
CalendarModel { CalendarModel {
selected: naive_now, selected: now,
visible: naive_now, visible: now,
} }
} }
#[inline] #[inline]
pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self { pub fn new(selected: Date, visible: Date) -> Self {
CalendarModel { selected, visible } CalendarModel { selected, visible }
} }
pub fn show_prev_month(&mut self) { pub fn show_prev_month(&mut self) {
let prev_month_date = self self.visible = self.visible.checked_sub(1.month()).expect("valid date");
.visible
.checked_sub_months(Months::new(1))
.expect("valid naivedate");
self.visible = prev_month_date;
} }
pub fn show_next_month(&mut self) { pub fn show_next_month(&mut self) {
let next_month_date = self self.visible = self.visible.checked_add(1.month()).expect("valid date");
.visible
.checked_add_months(Months::new(1))
.expect("valid naivedate");
self.visible = next_month_date;
} }
#[inline] #[inline]
@ -99,7 +79,7 @@ impl CalendarModel {
} }
#[inline] #[inline]
pub fn set_selected_visible(&mut self, selected: NaiveDate) { pub fn set_selected_visible(&mut self, selected: Date) {
self.selected = selected; self.selected = selected;
self.visible = self.selected; self.visible = self.selected;
} }
@ -107,7 +87,7 @@ impl CalendarModel {
pub struct Calendar<'a, M> { pub struct Calendar<'a, M> {
model: &'a CalendarModel, model: &'a CalendarModel,
on_select: Box<dyn Fn(NaiveDate) -> M>, on_select: Box<dyn Fn(Date) -> M>,
on_prev: Box<dyn Fn() -> M>, on_prev: Box<dyn Fn() -> M>,
on_next: Box<dyn Fn() -> M>, on_next: Box<dyn Fn() -> M>,
first_day_of_week: Weekday, first_day_of_week: Weekday,
@ -121,45 +101,57 @@ where
macro_rules! translate_month { macro_rules! translate_month {
($month:expr, $year:expr) => {{ ($month:expr, $year:expr) => {{
match $month { match $month {
chrono::Month::January => fl!("january", year = $year), 1 => fl!("january", year = $year),
chrono::Month::February => fl!("february", year = $year), 2 => fl!("february", year = $year),
chrono::Month::March => fl!("march", year = $year), 3 => fl!("march", year = $year),
chrono::Month::April => fl!("april", year = $year), 4 => fl!("april", year = $year),
chrono::Month::May => fl!("may", year = $year), 5 => fl!("may", year = $year),
chrono::Month::June => fl!("june", year = $year), 6 => fl!("june", year = $year),
chrono::Month::July => fl!("july", year = $year), 7 => fl!("july", year = $year),
chrono::Month::August => fl!("august", year = $year), 8 => fl!("august", year = $year),
chrono::Month::September => fl!("september", year = $year), 9 => fl!("september", year = $year),
chrono::Month::October => fl!("october", year = $year), 10 => fl!("october", year = $year),
chrono::Month::November => fl!("november", year = $year), 11 => fl!("november", year = $year),
chrono::Month::December => fl!("december", year = $year), 12 => fl!("december", year = $year),
_ => unreachable!(),
} }
}}; }};
} }
macro_rules! translate_weekday { macro_rules! translate_weekday {
($weekday:expr) => {{ ($weekday:expr, short) => {{
match $weekday { match $weekday {
Weekday::Mon => fl!("monday"), Weekday::Monday => fl!("mon"),
Weekday::Tue => fl!("tuesday"), Weekday::Tuesday => fl!("tue"),
Weekday::Wed => fl!("wednesday"), Weekday::Wednesday => fl!("wed"),
Weekday::Thu => fl!("thursday"), Weekday::Thursday => fl!("thu"),
Weekday::Fri => fl!("friday"), Weekday::Friday => fl!("fri"),
Weekday::Sat => fl!("saturday"), Weekday::Saturday => fl!("sat"),
Weekday::Sun => fl!("sunday"), 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!( let date = text(translate_month!(
Month::try_from(this.model.visible.month() as u8) this.model.visible.month(),
.expect("Previously valid month is suddenly invalid"),
this.model.visible.year() this.model.visible.year()
)) ))
.size(18); .size(18);
let day = text::body(translate_weekday!(this.model.visible.weekday())); let day = text::body(translate_weekday!(this.model.visible.weekday(), long));
let month_controls = row::with_capacity(2) let month_controls = row::with_capacity(2)
.spacing(8)
.push( .push(
icon::from_name("go-previous-symbolic") icon::from_name("go-previous-symbolic")
.apply(button::icon) .apply(button::icon)
@ -171,46 +163,49 @@ where
.on_press((this.on_next)()), .on_press((this.on_next)()),
); );
// Calender // Calendar
let mut calendar_grid: Grid<'_, Message> = let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill);
grid().padding([0, 12].into()).width(Length::Fill);
let mut first_day_of_week = this.first_day_of_week; let mut first_day_of_week = this.first_day_of_week;
for _ in 0..7 { for _ in 0..7 {
calendar_grid = calendar_grid.push( calendar_grid = calendar_grid.push(
text(translate_weekday!(first_day_of_week)) text::caption(translate_weekday!(first_day_of_week, short))
.size(12) .width(Length::Fixed(44.0))
.width(Length::Fixed(36.0))
.align_x(Alignment::Center), .align_x(Alignment::Center),
); );
first_day_of_week = first_day_of_week.succ(); first_day_of_week = first_day_of_week.next();
} }
calendar_grid = calendar_grid.insert_row(); calendar_grid = calendar_grid.insert_row();
let monday = get_calender_first( let first = get_calendar_first(
this.model.visible.year(), this.model.visible.year(),
this.model.visible.month(), this.model.visible.month(),
first_day_of_week, this.first_day_of_week,
); );
let mut day_iter = monday.iter_days();
let today = jiff::Zoned::now().date();
for i in 0..42 { for i in 0..42 {
if i > 0 && i % 7 == 0 { if i > 0 && i % 7 == 0 {
calendar_grid = calendar_grid.insert_row(); calendar_grid = calendar_grid.insert_row();
} }
let date = day_iter.next().unwrap(); let date = first
let is_currently_viewed_month = date.month() == this.model.visible.month() .checked_add(i.days())
&& date.year_ce() == this.model.visible.year_ce(); .expect("valid date in calendar range");
let is_currently_selected_month = date.month() == this.model.selected.month() let is_currently_viewed_month =
&& date.year_ce() == this.model.selected.year_ce(); 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 = let is_currently_selected_day =
date.day() == this.model.selected.day() && is_currently_selected_month; date.day() == this.model.selected.day() && is_currently_selected_month;
let is_today = date == today;
calendar_grid = calendar_grid.push(date_button( calendar_grid = calendar_grid.push(date_button(
date, date,
is_currently_viewed_month, is_currently_viewed_month,
is_currently_selected_day, is_currently_selected_day,
is_today,
&this.on_select, &this.on_select,
)); ));
} }
@ -225,9 +220,8 @@ where
.padding([12, 20]) .padding([12, 20])
.into(), .into(),
calendar_grid.into(), calendar_grid.into(),
padded_control(crate::widget::divider::horizontal::default()).into(),
]) ])
.width(315) .width(360)
.padding([8, 0]); .padding([8, 0]);
Self::new(content_list) Self::new(content_list)
@ -235,21 +229,24 @@ where
} }
fn date_button<Message: Clone + 'static>( fn date_button<Message: Clone + 'static>(
date: NaiveDate, date: Date,
is_currently_viewed_month: bool, is_currently_viewed_month: bool,
is_currently_selected_day: bool, is_currently_selected_day: bool,
on_select: &dyn Fn(NaiveDate) -> Message, is_today: bool,
on_select: &dyn Fn(Date) -> Message,
) -> crate::widget::Button<'static, Message> { ) -> crate::widget::Button<'static, Message> {
let style = if is_currently_selected_day { let style = if is_currently_selected_day {
button::ButtonClass::Suggested button::ButtonClass::Suggested
} else if is_today {
button::ButtonClass::Standard
} else { } else {
button::ButtonClass::Text button::ButtonClass::Text
}; };
let button = button::custom(text(format!("{}", date.day())).center()) let button = button::custom(text(format!("{}", date.day())).center())
.class(style) .class(style)
.height(Length::Fixed(36.0)) .height(Length::Fixed(44.0))
.width(Length::Fixed(36.0)); .width(Length::Fixed(44.0));
if is_currently_viewed_month { if is_currently_viewed_month {
button.on_press((on_select)(set_day(date, date.day()))) button.on_press((on_select)(set_day(date, date.day())))
@ -258,26 +255,10 @@ fn date_button<Message: Clone + 'static>(
} }
} }
/// Gets the first date that will be visible on the calender /// Gets the first date that will be visible on the calendar
#[must_use] #[must_use]
pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate { pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date {
let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); let date = Date::new(year, month, 1).expect("valid date");
let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from let num_days = date.weekday().since(from_weekday);
date.checked_sub_days(Days::new(num_days as u64)).unwrap() date.checked_sub(num_days.days()).expect("valid date")
}
// 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)
}
#[inline]
fn menu_control_padding() -> Padding {
let guard = crate::theme::THEME.lock().unwrap();
let cosmic = guard.cosmic();
[cosmic.space_xxs(), cosmic.space_m()].into()
} }