1use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
7
8use crate::{
9 geometry::Rect,
10 render::{RenderCtx, StrokeStyle},
11 round::UnobstructedArea,
12 style::Style,
13 widget::{PropertyKey, PropertyValue, Widget},
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub enum TimelineNodeState {
19 Past,
20 ActiveNow,
21 Upcoming,
22 Future,
23}
24
25#[derive(Clone, Copy, Debug, PartialEq)]
27pub struct TimelineNodeWidget {
28 pub state: TimelineNodeState,
29 pub node_radius: u8,
30 pub bar_width: u8,
31 pub has_top_bar: bool,
32 pub has_bottom_bar: bool,
33 pub active_color: Rgb565,
34 pub inactive_color: Rgb565,
35 pub line_color: Rgb565,
36}
37
38impl TimelineNodeWidget {
39 pub const fn new(state: TimelineNodeState) -> Self {
40 Self {
41 state,
42 node_radius: 4,
43 bar_width: 2,
44 has_top_bar: true,
45 has_bottom_bar: true,
46 active_color: Rgb565::new(31, 40, 0), inactive_color: Rgb565::new(10, 20, 10),
48 line_color: Rgb565::new(8, 16, 8),
49 }
50 }
51
52 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
53 where
54 D: DrawTarget<Color = Rgb565>,
55 C: crate::render::Compositor<D>,
56 {
57 let cx = bounds.x + (bounds.w as i32 / 2);
58 let cy = bounds.y + (bounds.h as i32 / 2);
59 let r = self.node_radius as u32;
60
61 if self.has_top_bar {
63 let stroke = StrokeStyle::new(self.line_color).with_width(self.bar_width);
64 ctx.draw_line_styled(cx, bounds.y, cx, cy - r as i32, stroke)?;
65 }
66
67 if self.has_bottom_bar {
69 let stroke = StrokeStyle::new(self.line_color).with_width(self.bar_width);
70 ctx.draw_line_styled(cx, cy + r as i32, cx, bounds.bottom(), stroke)?;
71 }
72
73 let (dot_color, dot_r) = match self.state {
75 TimelineNodeState::ActiveNow => (self.active_color, r + 1),
76 TimelineNodeState::Upcoming => (self.active_color, r),
77 TimelineNodeState::Past | TimelineNodeState::Future => (self.inactive_color, r),
78 };
79
80 ctx.fill_circle(cx, cy, dot_r, dot_color)?;
81 if matches!(self.state, TimelineNodeState::ActiveNow) {
82 ctx.stroke_circle(cx, cy, dot_r + 2, Rgb565::new(31, 63, 31))?;
83 }
84
85 Ok(())
86 }
87}
88
89impl Widget for TimelineNodeWidget {
90 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
91
92 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
93 match key {
94 PropertyKey::Selected => Some(PropertyValue::Int(self.state as i32)),
95 _ => None,
96 }
97 }
98}
99
100#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct PeekBannerWidget<'a> {
103 pub title: &'a str,
104 pub subtitle: Option<&'a str>,
105 pub icon_char: Option<char>,
106 pub height: u16,
107 pub is_expanded: bool,
108 pub background_color: Rgb565,
109 pub text_color: Rgb565,
110 pub accent_color: Rgb565,
111}
112
113impl<'a> PeekBannerWidget<'a> {
114 pub const fn new(title: &'a str) -> Self {
115 Self {
116 title,
117 subtitle: None,
118 icon_char: None,
119 height: 28,
120 is_expanded: false,
121 background_color: Rgb565::new(4, 8, 12),
122 text_color: Rgb565::new(31, 63, 31),
123 accent_color: Rgb565::new(0, 45, 25), }
125 }
126
127 pub fn apply_to_unobstructed_area(&self, area: &mut UnobstructedArea) {
129 let h = if self.is_expanded {
130 self.height * 2
131 } else {
132 self.height
133 };
134 area.inset_top = h;
135 }
136
137 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
138 where
139 D: DrawTarget<Color = Rgb565>,
140 C: crate::render::Compositor<D>,
141 {
142 ctx.fill_rounded_rect(bounds, 4, self.background_color)?;
144
145 let stripe = Rect::new(bounds.x, bounds.y, 4, bounds.h);
147 ctx.fill_rounded_rect(stripe, 2, self.accent_color)?;
148
149 let text_x = bounds.x + 10;
151 let text_y = bounds.y + (bounds.h as i32 / 2) - 4;
152 ctx.draw_text(text_x, text_y, self.title, self.text_color)?;
153
154 if let Some(sub) = self.subtitle {
155 if self.is_expanded {
156 ctx.draw_text(text_x, text_y + 12, sub, Rgb565::new(20, 40, 20))?;
157 }
158 }
159
160 Ok(())
161 }
162}
163
164impl<'a> Widget for PeekBannerWidget<'a> {
165 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
166
167 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
168 match key {
169 PropertyKey::Text => Some(PropertyValue::Str(self.title)),
170 PropertyKey::Expanded => Some(PropertyValue::Bool(self.is_expanded)),
171 _ => None,
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::framebuffer::Framebuffer;
180
181 #[test]
182 fn test_timeline_node_render() {
183 let node = TimelineNodeWidget::new(TimelineNodeState::ActiveNow);
184 let mut fb = Framebuffer::<400>::new(20, 20);
185 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 20, 20));
186 assert!(node.render(&mut ctx, Rect::new(0, 0, 20, 20)).is_ok());
187 }
188
189 #[test]
190 fn test_peek_banner_render() {
191 let peek = PeekBannerWidget::new("Meeting in 10m");
192 let mut fb = Framebuffer::<1200>::new(100, 12);
193 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 100, 12));
194 assert!(peek.render(&mut ctx, Rect::new(0, 0, 100, 12)).is_ok());
195 }
196}