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 top_left: Point, pub size: Size, pub fg_color: C, pub bg_color: C, pub border: u32, 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.border / 2 + 1, self.border / 2 + 1), ); date_rect .into_styled(PrimitiveStyle::with_fill(self.bg_color)) .draw(target)?; date_rect .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.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.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.fg_color, 2)) .draw(target)?; } Ok(()) } } pub struct CalendarMonth { pub top_left: Point, pub size: Size, pub border: u32, pub fg_color: C, pub bg_color: C, 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.border / 2 + 1, self.border / 2 + 1), ); rect .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.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.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 border: u32, pub fg_color: C, pub bg_color: C, // If hi_color is None, the fg and bg colors are inverted instead. pub hi_color: Option, } 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.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 { border, top_left: self.top_left, size: Size::new(month_width, height), fg_color: self.fg_color, bg_color: self.bg_color, 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 (mut fg_color, mut bg_color, mut border) = match is_weekend ^ is_today { false => (self.fg_color, self.bg_color, border), true => (self.bg_color, self.fg_color, 0), }; if is_today && self.hi_color.is_some() { (fg_color, bg_color, border) = (self.bg_color, self.hi_color.unwrap(), 0); } let calendar_day = CalendarDay { border, top_left: self.top_left + Point::new((width * x) as i32, (height * y) as i32), size: Size::new(width, height), fg_color, bg_color, day_of_month: i - empty_squares + 1, has_event: i - empty_squares + 1 == date_now.day(), }; calendar_day.draw(target)?; } Ok(()) } } pub struct ScrollingWeek { day: chrono::NaiveDate, } impl ScrollingWeek { pub fn succ(&self) -> ScrollingWeek { let start_of_next_month = self .day .with_month(self.day.month() % 11 + 1) .unwrap() .with_day(1) .unwrap(); let days_until_next_month = start_of_next_month.num_days_from_ce() - self.day.num_days_from_ce(); let day_of_week = self.day.weekday().number_from_monday() as i32; let days_to_skip = if days_until_next_month < 7 { days_until_next_month } else { 8 - day_of_week }; ScrollingWeek { day: self .day .checked_add_signed(chrono::Duration::days(days_to_skip as i64)) .unwrap(), } } pub fn month(&self) -> chrono::Month { chrono::Month::from_u32(self.day.month()).unwrap() } pub fn week(&self) -> u32 { self.day.iso_week().week() } pub fn year(&self) -> i32 { self.day.year() } } pub struct ScrollingCalendar { pub top_left: Point, pub size: Size, pub style: ComponentStyle, pub day: chrono::NaiveDate, pub weeks: u32, } impl Drawable for ScrollingCalendar where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { let height = self.size.height / (1 + self.weeks * 2); let mut week = ScrollingWeek { day: chrono::NaiveDate::from_isoywd( self.day.year(), self.day.iso_week().week(), chrono::Weekday::Mon, ), }; for i in 0..self.weeks * 2 { week = week.succ(); let calendar_week = CalendarWeek { top_left: Point::new(self.top_left.x, self.top_left.y + (height * i) as i32), size: Size::new(self.size.width, height), style: self.style.clone(), year: week.year(), month: week.month(), week: week.week(), }; 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, } impl Drawable for CalendarWeek where C: PixelColor + From, { type Color = C; type Output = (); fn draw(&self, target: &mut D) -> Result where D: DrawTarget, { 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 width = self.size.width / 7; let mut weekday = first_day.weekday(); for i 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 { 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 { top_left: self.top_left + Point::new((width * weekday.num_days_from_monday()) as i32, 0), size: Size::new(width, self.size.height), border: self.style.border, fg_color, bg_color, day_of_month: day.day(), has_event: true, }; calendar_day.draw(target)?; weekday = weekday.succ(); } Ok(()) } }