commit 1b5c4c179586b74501466e3ee21dd8d7171da691 Author: Ilya Date: Tue Mar 29 23:16:37 2022 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..b983c3d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "embedded-components" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +embedded-graphics = "*" +embedded-graphics-simulator = "*" +chrono = "*" +embedded-layout = "*" +num-traits = "*" diff --git a/examples/test.rs b/examples/test.rs new file mode 100644 index 0000000..092c03a --- /dev/null +++ b/examples/test.rs @@ -0,0 +1,106 @@ +use embedded_components::{Calendar, ComponentStyle, Gauge, ScrollingCalendar}; +use embedded_graphics::{ + pixelcolor::{raw::RawU2, BinaryColor, Rgb888}, + prelude::*, +}; +use embedded_graphics_simulator::{ + BinaryColorTheme, OutputSettingsBuilder, SimulatorDisplay, Window, +}; + +#[derive(Copy, Clone, PartialEq, Eq)] +enum TriColor { + White, + Black, + Red, +} + +impl PixelColor for TriColor { + type Raw = RawU2; +} + +impl From for TriColor { + fn from(value: BinaryColor) -> TriColor { + match value { + BinaryColor::On => TriColor::Black, + BinaryColor::Off => TriColor::White, + } + } +} + +impl Into for TriColor { + fn into(self) -> Rgb888 { + match self { + TriColor::Black => Rgb888::new(0, 0, 0), + TriColor::White => Rgb888::new(255, 255, 255), + TriColor::Red => Rgb888::new(180, 0, 0), + } + } +} + +impl From for TriColor { + fn from(value: Rgb888) -> TriColor { + match value { + _ => TriColor::White, + } + } +} + +fn main() -> Result<(), core::convert::Infallible> { + //let mut display = SimulatorDisplay::::new(Size::new(212, 104)); + //let mut display = SimulatorDisplay::::new(Size::new(800, 480)); + let mut display = SimulatorDisplay::::new(Size::new(800, 480)); + + let fg_color = TriColor::Black; + let bg_color = TriColor::White; + let hi_color = Some(TriColor::Red); + + let gauge = Gauge { + top_left: Point::new(300, 300), + bezel: 1, + border: 1, + size: Size::new(200, 20), + ratio: 0.5, + fg_color, + bg_color, + text: "test test test test test", + }; + gauge.draw(&mut display)?; + + let calendar = Calendar { + top_left: Point::new(10, 10), + size: Size::new(196, 196), + border: 1, + fg_color, + bg_color, + hi_color, + }; + calendar.draw(&mut display)?; + + let calendar_style = ComponentStyle { + border: 1, + bezel: 1, + fg_color, + bg_color, + hi_color: TriColor::Red, + }; + + let calendar_week = ScrollingCalendar { + top_left: Point::new(400, 10), + size: Size::new(396, 396), + style: calendar_style, + day: chrono::Local::now() + .naive_local() + .date() + .checked_add_signed(chrono::Duration::days(28 * 4 * 0 - 14)) + .unwrap(), + weeks: 5, + }; + calendar_week.draw(&mut display)?; + + let output_settings = OutputSettingsBuilder::new() + .theme(BinaryColorTheme::Default) + .build(); + Window::new("Hello World", &output_settings).show_static(&display); + + Ok(()) +} diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..b196eaa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +tab_spaces = 2 diff --git a/src/bullet_counter.rs b/src/bullet_counter.rs new file mode 100644 index 0000000..2591744 --- /dev/null +++ b/src/bullet_counter.rs @@ -0,0 +1,27 @@ +use embedded_graphics::{ + pixelcolor::{BinaryColor, PixelColor}, + prelude::*, + primitives::{Circle, Rectangle}, +}; +use embedded_layout::prelude::*; + +pub struct BulletCounter { + pub top_left: Point, + pub size: Size, // Available space + pub color: C, +} + +impl Drawable for BulletCounter +where + C: PixelColor + From, +{ + type Color = C; + type Output = (); + + fn draw(&self, target: &mut D) -> Result + where + D: DrawTarget, + { + Ok(()) + } +} diff --git a/src/calendar.rs b/src/calendar.rs new file mode 100644 index 0000000..4e1a1a7 --- /dev/null +++ b/src/calendar.rs @@ -0,0 +1,386 @@ +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, + ) + .checked_add_signed(chrono::Duration::days(-7 * self.weeks as i64)) + .unwrap(), + }; + + 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 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 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, + 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 { + 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: day.num_days_from_ce() == today.num_days_from_ce(), + }; + calendar_day.draw(target)?; + + weekday = weekday.succ(); + } + + Ok(()) + } +} diff --git a/src/gauge.rs b/src/gauge.rs new file mode 100644 index 0000000..add9350 --- /dev/null +++ b/src/gauge.rs @@ -0,0 +1,77 @@ +use embedded_graphics::{ + mono_font::{ascii::FONT_6X9, MonoTextStyle}, + pixelcolor::{BinaryColor, PixelColor}, + prelude::*, + primitives::{PrimitiveStyle, Rectangle}, + text::{Baseline, Text}, +}; + +pub struct Gauge<'a, C> { + pub top_left: Point, + pub bezel: u32, + pub border: u32, + pub size: Size, + pub ratio: f32, + pub fg_color: C, + pub bg_color: C, + pub text: &'a str, +} + +impl Drawable for Gauge<'_, C> +where + C: PixelColor + From, +{ + type Color = C; + type Output = (); + + fn draw(&self, target: &mut D) -> Result + where + D: DrawTarget, + { + let margin = self.border + self.bezel; + + Rectangle::new(self.top_left, self.size) + .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.border)) + .draw(target)?; + + let inner_size = self.size - Size::new(margin * 2, margin * 2); + let inner_filled = Size::new( + (inner_size.width as f32 * self.ratio).ceil() as u32, + inner_size.height, + ); + let inner_unfilled = Size::new(inner_size.width - inner_filled.width, inner_size.height); + + let filled_rect = Rectangle::new( + self.top_left + Point::new(margin as i32, margin as i32), + inner_filled, + ); + let unfilled_rect = Rectangle::new( + self.top_left + Point::new(margin as i32 + inner_filled.width as i32, margin as i32), + inner_unfilled, + ); + + // Added stroke as well because sometimes it showed a 1px bezel otherwise + // This is probably because the outer border stroke extends border/2 out/inwards + filled_rect + .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.border)) + .draw(target)?; + filled_rect + .into_styled(PrimitiveStyle::with_fill(self.fg_color)) + .draw(target)?; + + let filled_style = MonoTextStyle::new(&FONT_6X9, self.bg_color); + let unfilled_style = MonoTextStyle::new(&FONT_6X9, self.fg_color); + + let mut text = + Text::with_baseline(self.text, self.top_left, filled_style, Baseline::Middle); + let text_box = text.bounding_box(); + text.position = self.top_left + Point::new(margin as i32, margin as i32) + inner_size / 2 + - Point::new((text_box.size.width as f32 / 2.) as i32, 0); + + text.draw(&mut target.clipped(&filled_rect))?; + let text = Text::with_baseline(self.text, text.position, unfilled_style, Baseline::Middle); + text.draw(&mut target.clipped(&unfilled_rect))?; + + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f61eb47 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +mod bullet_counter; +mod calendar; +mod gauge; +mod style; + +pub use bullet_counter::BulletCounter; +pub use calendar::{Calendar, ScrollingCalendar}; +pub use gauge::Gauge; +pub use style::ComponentStyle; diff --git a/src/list_item.rs b/src/list_item.rs new file mode 100644 index 0000000..9bdcd0f --- /dev/null +++ b/src/list_item.rs @@ -0,0 +1,78 @@ +use embedded_graphics::{ + pixelcolor::{BinaryColor, PixelColor}, + prelude::*, + primitives::{Line, PrimitiveStyle, Rectangle}, + text::Text, +}; +use embedded_layout::prelude::*; + +pub struct ListItemData { + pub text: &'a str, + pub icon: &'a str, +} + +pub struct List<'a, C> { + pub top_left: Point, + pub border: u32, + pub size: Size, + pub fg_color: C, + pub bg_color: C, + pub list_items: Vec, +} + +impl Drawable for ListItem<'_, C> +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); + rect + .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.border)) + .draw(target)?; + rect + .into_styled(PrimitiveStyle::with_fill(self.fg_color)) + .draw(target)?; + + // TODO: draw list items + + Ok(()) + } +} + +pub struct ListItem<'a, C> { + pub top_left: Point, + pub border: u32, + pub size: Size, + pub fg_color: C, + pub bg_color: C, + pub text: &'a str, +} + +impl Drawable for ListItem<'_, C> +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); + rect + .into_styled(PrimitiveStyle::with_stroke(self.fg_color, self.border)) + .draw(target)?; + rect + .into_styled(PrimitiveStyle::with_fill(self.fg_color)) + .draw(target)?; + + Ok(()) + } +} diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..80d8c01 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,8 @@ +#[derive(Clone)] +pub struct ComponentStyle { + pub fg_color: C, + pub bg_color: C, + pub hi_color: C, + pub border: u32, + pub bezel: u32, +}