use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
use crate::{
geometry::Rect,
render::{RenderCtx, StrokeStyle},
round::UnobstructedArea,
style::Style,
widget::{PropertyKey, PropertyValue, Widget},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TimelineNodeState {
Past,
ActiveNow,
Upcoming,
Future,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TimelineNodeWidget {
pub state: TimelineNodeState,
pub node_radius: u8,
pub bar_width: u8,
pub has_top_bar: bool,
pub has_bottom_bar: bool,
pub active_color: Rgb565,
pub inactive_color: Rgb565,
pub line_color: Rgb565,
}
impl TimelineNodeWidget {
pub const fn new(state: TimelineNodeState) -> Self {
Self {
state,
node_radius: 4,
bar_width: 2,
has_top_bar: true,
has_bottom_bar: true,
active_color: Rgb565::new(31, 40, 0), inactive_color: Rgb565::new(10, 20, 10),
line_color: Rgb565::new(8, 16, 8),
}
}
pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
C: crate::render::Compositor<D>,
{
let cx = bounds.x + (bounds.w as i32 / 2);
let cy = bounds.y + (bounds.h as i32 / 2);
let r = self.node_radius as u32;
if self.has_top_bar {
let stroke = StrokeStyle::new(self.line_color).with_width(self.bar_width);
ctx.draw_line_styled(cx, bounds.y, cx, cy - r as i32, stroke)?;
}
if self.has_bottom_bar {
let stroke = StrokeStyle::new(self.line_color).with_width(self.bar_width);
ctx.draw_line_styled(cx, cy + r as i32, cx, bounds.bottom(), stroke)?;
}
let (dot_color, dot_r) = match self.state {
TimelineNodeState::ActiveNow => (self.active_color, r + 1),
TimelineNodeState::Upcoming => (self.active_color, r),
TimelineNodeState::Past | TimelineNodeState::Future => (self.inactive_color, r),
};
ctx.fill_circle(cx, cy, dot_r, dot_color)?;
if matches!(self.state, TimelineNodeState::ActiveNow) {
ctx.stroke_circle(cx, cy, dot_r + 2, Rgb565::new(31, 63, 31))?;
}
Ok(())
}
}
impl Widget for TimelineNodeWidget {
fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
match key {
PropertyKey::Selected => Some(PropertyValue::Int(self.state as i32)),
_ => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct PeekBannerWidget<'a> {
pub title: &'a str,
pub subtitle: Option<&'a str>,
pub icon_char: Option<char>,
pub height: u16,
pub is_expanded: bool,
pub background_color: Rgb565,
pub text_color: Rgb565,
pub accent_color: Rgb565,
}
impl<'a> PeekBannerWidget<'a> {
pub const fn new(title: &'a str) -> Self {
Self {
title,
subtitle: None,
icon_char: None,
height: 28,
is_expanded: false,
background_color: Rgb565::new(4, 8, 12),
text_color: Rgb565::new(31, 63, 31),
accent_color: Rgb565::new(0, 45, 25), }
}
pub fn apply_to_unobstructed_area(&self, area: &mut UnobstructedArea) {
let h = if self.is_expanded {
self.height * 2
} else {
self.height
};
area.inset_top = h;
}
pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
where
D: DrawTarget<Color = Rgb565>,
C: crate::render::Compositor<D>,
{
ctx.fill_rounded_rect(bounds, 4, self.background_color)?;
let stripe = Rect::new(bounds.x, bounds.y, 4, bounds.h);
ctx.fill_rounded_rect(stripe, 2, self.accent_color)?;
let text_x = bounds.x + 10;
let text_y = bounds.y + (bounds.h as i32 / 2) - 4;
ctx.draw_text(text_x, text_y, self.title, self.text_color)?;
if let Some(sub) = self.subtitle {
if self.is_expanded {
ctx.draw_text(text_x, text_y + 12, sub, Rgb565::new(20, 40, 20))?;
}
}
Ok(())
}
}
impl<'a> Widget for PeekBannerWidget<'a> {
fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
match key {
PropertyKey::Text => Some(PropertyValue::Str(self.title)),
PropertyKey::Expanded => Some(PropertyValue::Bool(self.is_expanded)),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framebuffer::Framebuffer;
#[test]
fn test_timeline_node_render() {
let node = TimelineNodeWidget::new(TimelineNodeState::ActiveNow);
let mut fb = Framebuffer::<400>::new(20, 20);
let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 20, 20));
assert!(node.render(&mut ctx, Rect::new(0, 0, 20, 20)).is_ok());
}
#[test]
fn test_peek_banner_render() {
let peek = PeekBannerWidget::new("Meeting in 10m");
let mut fb = Framebuffer::<1200>::new(100, 12);
let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 100, 12));
assert!(peek.render(&mut ctx, Rect::new(0, 0, 100, 12)).is_ok());
}
}