380 lines
9.6 KiB
Rust
380 lines
9.6 KiB
Rust
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 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<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.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<C> {
|
|
pub top_left: Point,
|
|
pub size: Size,
|
|
pub border: u32,
|
|
pub fg_color: C,
|
|
pub bg_color: 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.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<C> {
|
|
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<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.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<C> {
|
|
pub top_left: Point,
|
|
pub size: Size,
|
|
pub style: ComponentStyle<C>,
|
|
pub day: chrono::NaiveDate,
|
|
pub weeks: u32,
|
|
}
|
|
|
|
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 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<C> {
|
|
pub top_left: Point,
|
|
pub size: Size,
|
|
pub style: ComponentStyle<C>,
|
|
pub year: i32,
|
|
pub month: chrono::Month,
|
|
pub week: u32,
|
|
}
|
|
|
|
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 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(())
|
|
}
|
|
}
|