use crate::ComponentStyle; use chrono::{Datelike, Month}; use embedded_graphics::{ mono_font::{ ascii::{FONT_10X20, FONT_4X6, FONT_6X9, FONT_8X13}, MonoFont, MonoTextStyle, }, pixelcolor::{BinaryColor, PixelColor}, prelude::*, primitives::{Line, PrimitiveStyle, Rectangle}, text::Text, }; use embedded_layout::prelude::*; use num_traits::cast::FromPrimitive; fn font(height: u32) -> MonoFont<'static> { if height < 8 { return FONT_4X6; } else if height < 10 { return FONT_6X9; } else if height < 14 { return FONT_8X13; } else { return FONT_10X20; } } pub struct CalendarDay { pub style: ComponentStyle, pub top_left: Point, pub size: Size, pub day_of_month: u32, pub has_event: bool, } impl Drawable for CalendarDay where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { // Adding based on border so that the borders overlap let date_rect = Rectangle::new( self.top_left, self.size + Size::new(self.style.border / 2 + 1, self.style.border / 2 + 1), ); date_rect .into_styled(PrimitiveStyle::with_fill(self.style.bg_color)) .draw(target)?; date_rect .into_styled(PrimitiveStyle::with_stroke( self.style.border_color, self.style.border, )) .draw(target)?; let text = format!("{}", self.day_of_month); let date_font = font(self.size.height / 2); let text_style = MonoTextStyle::new(&date_font, self.style.fg_color); Text::new(&text, self.top_left, text_style) .align_to(&date_rect, horizontal::Center, vertical::Center) .draw(target)?; if self.has_event { let event_padding: i32 = 2 + self.size.height as i32 / 10; Line::new( self.top_left + Point::new(event_padding, self.size.height as i32 - event_padding), self.top_left + Point::new( self.size.width as i32 - event_padding, self.size.height as i32 - event_padding, ), ) .into_styled(PrimitiveStyle::with_stroke(self.style.fg_color, 2)) .draw(target)?; } Ok(()) } } pub struct CalendarMonth { pub top_left: Point, pub size: Size, pub style: ComponentStyle, pub month: chrono::Month, } impl Drawable for CalendarMonth where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { let rect = Rectangle::new( self.top_left, self.size + Size::new(self.style.border / 2 + 1, self.style.border / 2 + 1), ); rect .into_styled(PrimitiveStyle::with_stroke( self.style.fg_color, self.style.border, )) .draw(target)?; let month_font = font((self.size.height as f32 * 0.8 - 2.) as u32); let text_style = MonoTextStyle::new(&month_font, self.style.fg_color); Text::new(self.month.name(), self.top_left, text_style) .align_to(&rect, horizontal::Center, vertical::Center) .draw(target)?; Ok(()) } } pub struct Calendar { pub top_left: Point, pub size: Size, pub style: ComponentStyle, } impl Drawable for Calendar where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { let border = self.style.border; let date_now = chrono::offset::Local::now().date(); let first_day = date_now.with_day(1).unwrap(); let last_day = date_now .with_day(1) .unwrap() .with_month(date_now.month() % 11 + 1) .unwrap() .pred(); let empty_squares = first_day.weekday().num_days_from_monday(); let squares_needed = empty_squares + last_day.day(); let month_needs_separate_row = empty_squares < 4; let mut rows_needed = squares_needed / 7 + 1; if month_needs_separate_row { rows_needed += 1; } let width = self.size.width / 7; let height = self.size.height / rows_needed; let month_width = if month_needs_separate_row { width * 7 } else { width * empty_squares }; let calendar_month = CalendarMonth { style: self.style.clone(), top_left: self.top_left, size: Size::new(month_width, height), month: Month::from_u32(date_now.month()).unwrap(), }; calendar_month.draw(target)?; for i in empty_squares..squares_needed { let x = i % 7; let mut y = i / 7; if month_needs_separate_row { y = y + 1; } let day_of_month = i - empty_squares + 1; let is_weekend = date_now .with_day(day_of_month) .unwrap() .weekday() .num_days_from_monday() >= 5; let is_today = day_of_month == date_now.day(); let style = self.style.clone(); let mut style = match is_weekend ^ is_today { false => style.with_border(border), true => style.invert().with_border(0), }; if is_today && self.style.hi_color != self.style.bg_color { style = ComponentStyle { fg_color: style.hi_color, bg_color: style.fg_color, ..style } } let calendar_day = CalendarDay { style, top_left: self.top_left + Point::new((width * x) as i32, (height * y) as i32), size: Size::new(width, height), day_of_month: i - empty_squares + 1, has_event: i - empty_squares + 1 == date_now.day(), }; calendar_day.draw(target)?; } Ok(()) } } #[derive(Clone)] pub struct ScrollingWeek { year: i32, week: u32, start_of_month: bool, } impl ScrollingWeek { fn monday(&self) -> chrono::NaiveDate { chrono::NaiveDate::from_isoywd(self.year, self.week, chrono::Weekday::Mon) } fn first_day_of_week(&self) -> Option { let monday = self.monday(); let next_monday = self .monday() .checked_add_signed(chrono::Duration::days(7)) .unwrap(); if !self.start_of_month || monday.month() == next_monday.month() { return Some(monday); } if !self.start_of_month && next_monday.day() == 1 { return None; } return next_monday.with_day(1); } fn last_day_of_week(&self) -> Option { return self .succ() .first_day_of_week()? .checked_sub_signed(chrono::Duration::days(1)); } pub fn from_date(date: chrono::NaiveDate) -> ScrollingWeek { let week = date.iso_week().week(); ScrollingWeek { year: date.year(), week, start_of_month: date.day() < 7, } } pub fn from_week(week: chrono::IsoWeek) -> ScrollingWeek { let now = chrono::Local::today().naive_local(); let monday = chrono::NaiveDate::from_isoywd(now.year(), week.week(), chrono::Weekday::Mon); ScrollingWeek { year: now.year(), week: week.week(), start_of_month: monday.day() < 7, } } pub fn succ(&self) -> ScrollingWeek { let monday = self.monday(); let next_monday = monday .checked_add_signed(chrono::Duration::days(7)) .unwrap(); match self.start_of_month || monday.month() == next_monday.month() { false => ScrollingWeek { start_of_month: true, ..*self }, true => ScrollingWeek { year: next_monday.year(), week: next_monday.iso_week().week(), start_of_month: false, }, } } pub fn month(&self) -> chrono::Month { chrono::Month::from_u32(self.monday().month()).unwrap() } pub fn week(&self) -> u32 { self.monday().iso_week().week() } pub fn year(&self) -> i32 { self.monday().year() } } pub struct ScrollingCalendar { pub style: ComponentStyle, pub day: chrono::NaiveDate, pub prev_weeks: u32, pub next_weeks: u32, } impl ScrollingCalendar { pub fn new(style: ComponentStyle, prev_weeks: u32, next_weeks: u32) -> Self { Self { style, day: chrono::Local::now().naive_local().date(), prev_weeks, next_weeks, } } } impl Drawable for ScrollingCalendar where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { let top_left = target.bounding_box().top_left; let size = target.bounding_box().size; let total_weeks = self.prev_weeks + 1 + self.next_weeks; let height = size.height / total_weeks; let mut week = ScrollingWeek::from_date( self .day .checked_sub_signed(chrono::Duration::days(self.prev_weeks as i64 * 7)) .unwrap(), ); for i in 0..total_weeks { week = week.succ(); let calendar_week = CalendarWeek { top_left: Point::new(top_left.x, top_left.y + (height * i) as i32), size: Size::new(size.width, height), style: self.style.clone(), year: week.year(), month: week.month(), week: week.week(), scroll_week: week.clone(), }; calendar_week.draw(target)?; } Ok(()) } } pub struct CalendarWeek { pub top_left: Point, pub size: Size, pub style: ComponentStyle, pub year: i32, pub month: chrono::Month, pub week: u32, pub scroll_week: ScrollingWeek, } impl Drawable for CalendarWeek where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { let today = chrono::Utc::now().date(); let monday = chrono::NaiveDate::from_isoywd(self.year, self.week, chrono::Weekday::Mon); let sunday = chrono::NaiveDate::from_isoywd(self.year, self.week, chrono::Weekday::Sun); let week_starts_in_prev_month = monday.month() != self.month.number_from_month(); let week_ends_in_next_month = sunday.month() != self.month.number_from_month(); let first_day = match week_starts_in_prev_month { true => sunday.with_day(1).unwrap(), false => monday, }; let last_day = match week_ends_in_next_month { true => sunday.with_day(1).unwrap().pred(), false => sunday, }; let first_day = self.scroll_week.first_day_of_week(); let last_day = self.scroll_week.last_day_of_week(); if first_day.is_none() || last_day.is_none() { return Ok(()); } let first_day = first_day.unwrap(); let last_day = last_day.unwrap(); let width = self.size.width / 7; if first_day.weekday().number_from_monday() > 4 { let month = CalendarMonth { style: self.style.clone(), top_left: self.top_left, size: Size::new( width * (first_day.weekday().number_from_monday() - 1), self.size.height, ), month: chrono::Month::from_u32(first_day.month()).unwrap(), }; month.draw(target)?; } else if last_day.weekday().number_from_monday() < 4 { let month = CalendarMonth { style: self.style.clone(), top_left: self.top_left + Point::new((width * last_day.weekday().number_from_monday()) as i32, 0), size: Size::new( width * (7 - last_day.weekday().number_from_monday()), self.size.height, ), month: chrono::Month::from_u32( last_day .checked_add_signed(chrono::Duration::days(1)) .unwrap() .month(), ) .unwrap(), }; month.draw(target)?; } let mut weekday = first_day.weekday(); for _ in first_day.weekday().number_from_monday()..(last_day.weekday().number_from_monday() + 1) { let day = chrono::NaiveDate::from_isoywd(self.year, self.week, weekday); let (fg_color, bg_color) = match ( weekday.num_days_from_monday() < 5, day.num_days_from_ce() == today.num_days_from_ce(), ) { (_, true) => (self.style.bg_color, self.style.hi_color), (true, _) => (self.style.fg_color, self.style.bg_color), (false, _) => (self.style.bg_color, self.style.fg_color), }; let style = ComponentStyle { fg_color, bg_color, ..self.style }; let calendar_day = CalendarDay { style, top_left: self.top_left + Point::new((width * weekday.num_days_from_monday()) as i32, 0), size: Size::new(width, self.size.height), day_of_month: day.day(), has_event: day.num_days_from_ce() == today.num_days_from_ce(), }; calendar_day.draw(target)?; weekday = weekday.succ(); } Ok(()) } }