You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

485 lines
12 KiB

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<C> {
pub style: ComponentStyle<C>,
pub top_left: Point,
pub size: Size,
pub day_of_month: u32,
pub has_event: bool,
}
impl<C> Drawable for CalendarDay<C>
where
C: PixelColor + From<BinaryColor>,
{
type Color = C;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = C>,
{
// 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<C> {
pub top_left: Point,
pub size: Size,
pub style: ComponentStyle<C>,
pub month: chrono::Month,
}
impl<C> Drawable for CalendarMonth<C>
where
C: PixelColor + From<BinaryColor>,
{
type Color = C;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = C>,
{
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<C> {
pub top_left: Point,
pub size: Size,
pub style: ComponentStyle<C>,
}
impl<C> Drawable for Calendar<C>
where
C: PixelColor + From<BinaryColor>,
{
type Color = C;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = C>,
{
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<chrono::NaiveDate> {
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<chrono::NaiveDate> {
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<C> {
pub style: ComponentStyle<C>,
pub day: chrono::NaiveDate,
pub prev_weeks: u32,
pub next_weeks: u32,
}
impl<C> ScrollingCalendar<C> {
pub fn new(style: ComponentStyle<C>, prev_weeks: u32, next_weeks: u32) -> Self {
Self {
style,
day: chrono::Local::now().naive_local().date(),
prev_weeks,
next_weeks,
}
}
}
impl<C> Drawable for ScrollingCalendar<C>
where
C: PixelColor + From<BinaryColor>,
{
type Color = C;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = C>,
{
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<C> {
pub top_left: Point,
pub size: Size,
pub style: ComponentStyle<C>,
pub year: i32,
pub month: chrono::Month,
pub week: u32,
pub scroll_week: ScrollingWeek,
}
impl<C> Drawable for CalendarWeek<C>
where
C: PixelColor + From<BinaryColor>,
{
type Color = C;
type Output = ();
fn draw<D>(&self, target: &mut D) -> Result<Self::Output, D::Error>
where
D: DrawTarget<Color = C>,
{
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(())
}
}