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
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(()) |
|
} |
|
}
|
|
|