1use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
6
7use crate::{
8 geometry::Rect,
9 render::RenderCtx,
10 style::Style,
11 widget::{PropertyKey, PropertyValue, Widget},
12};
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub enum ContentIndicatorDirection {
17 Up,
18 Down,
19 Left,
20 Right,
21}
22
23#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct ContentIndicatorWidget {
26 pub direction: ContentIndicatorDirection,
27 pub visible: bool,
28 pub color: Rgb565,
29 pub pulse_progress: f32,
30}
31
32impl ContentIndicatorWidget {
33 pub const fn new(direction: ContentIndicatorDirection) -> Self {
34 Self {
35 direction,
36 visible: true,
37 color: Rgb565::new(31, 63, 31),
38 pulse_progress: 1.0,
39 }
40 }
41
42 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
43 where
44 D: DrawTarget<Color = Rgb565>,
45 C: crate::render::Compositor<D>,
46 {
47 if !self.visible || bounds.w == 0 || bounds.h == 0 {
48 return Ok(());
49 }
50
51 let cx = bounds.x + (bounds.w as i32 / 2);
52 let cy = bounds.y + (bounds.h as i32 / 2);
53 let s = (bounds.w.min(bounds.h) as i32 / 3).max(2);
54
55 match self.direction {
57 ContentIndicatorDirection::Up => {
58 ctx.draw_line(cx - s, cy + s / 2, cx, cy - s / 2, self.color)?;
59 ctx.draw_line(cx, cy - s / 2, cx + s, cy + s / 2, self.color)?;
60 }
61 ContentIndicatorDirection::Down => {
62 ctx.draw_line(cx - s, cy - s / 2, cx, cy + s / 2, self.color)?;
63 ctx.draw_line(cx, cy + s / 2, cx + s, cy - s / 2, self.color)?;
64 }
65 ContentIndicatorDirection::Left => {
66 ctx.draw_line(cx + s / 2, cy - s, cx - s / 2, cy, self.color)?;
67 ctx.draw_line(cx - s / 2, cy, cx + s / 2, cy + s, self.color)?;
68 }
69 ContentIndicatorDirection::Right => {
70 ctx.draw_line(cx - s / 2, cy - s, cx + s / 2, cy, self.color)?;
71 ctx.draw_line(cx + s / 2, cy, cx - s / 2, cy + s, self.color)?;
72 }
73 }
74 Ok(())
75 }
76}
77
78impl Widget for ContentIndicatorWidget {
79 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
80
81 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
82 match key {
83 PropertyKey::State => Some(PropertyValue::Bool(self.visible)),
84 PropertyKey::Progress => Some(PropertyValue::Float(self.pulse_progress)),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct CrumbsIndicatorWidget {
93 pub count: u8,
94 pub active_index: u8,
95 pub dot_radius: u8,
96 pub dot_spacing: u8,
97 pub active_color: Rgb565,
98 pub inactive_color: Rgb565,
99}
100
101impl CrumbsIndicatorWidget {
102 pub const fn new(count: u8, active_index: u8) -> Self {
103 Self {
104 count,
105 active_index,
106 dot_radius: 2,
107 dot_spacing: 6,
108 active_color: Rgb565::new(31, 63, 31),
109 inactive_color: Rgb565::new(10, 20, 10),
110 }
111 }
112
113 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
114 where
115 D: DrawTarget<Color = Rgb565>,
116 C: crate::render::Compositor<D>,
117 {
118 if self.count == 0 {
119 return Ok(());
120 }
121
122 let total_w =
123 (self.count as i32 - 1) * (self.dot_spacing as i32) + (self.dot_radius as i32 * 2);
124 let start_x = bounds.x + (bounds.w as i32 - total_w) / 2 + self.dot_radius as i32;
125 let cy = bounds.y + (bounds.h as i32 / 2);
126
127 for i in 0..self.count {
128 let cx = start_x + (i as i32 * self.dot_spacing as i32);
129 let is_active = i == self.active_index;
130 let color = if is_active {
131 self.active_color
132 } else {
133 self.inactive_color
134 };
135 let r = if is_active {
136 self.dot_radius as u32 + 1
137 } else {
138 self.dot_radius as u32
139 };
140 ctx.fill_circle(cx, cy, r, color)?;
141 }
142 Ok(())
143 }
144}
145
146impl Widget for CrumbsIndicatorWidget {
147 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
148
149 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
150 match key {
151 PropertyKey::Selected => Some(PropertyValue::Int(self.active_index as i32)),
152 _ => None,
153 }
154 }
155}
156
157#[derive(Clone, Copy, Debug, PartialEq)]
159pub struct SelectionWidget<'a, const MAX_CELLS: usize = 6> {
160 pub cell_texts: [&'a str; MAX_CELLS],
161 pub cell_count: usize,
162 pub selected_cell: usize,
163 pub is_active: bool,
164 pub bump_offset_y: i8,
165 pub slide_offset_x: i8,
166 pub active_bg_color: Rgb565,
167 pub active_text_color: Rgb565,
168 pub inactive_bg_color: Rgb565,
169 pub inactive_text_color: Rgb565,
170}
171
172impl<'a, const MAX_CELLS: usize> SelectionWidget<'a, MAX_CELLS> {
173 pub const fn new(cell_texts: [&'a str; MAX_CELLS], cell_count: usize) -> Self {
174 Self {
175 cell_texts,
176 cell_count,
177 selected_cell: 0,
178 is_active: true,
179 bump_offset_y: 0,
180 slide_offset_x: 0,
181 active_bg_color: Rgb565::new(31, 63, 31),
182 active_text_color: Rgb565::new(0, 0, 0),
183 inactive_bg_color: Rgb565::new(3, 6, 3),
184 inactive_text_color: Rgb565::new(31, 63, 31),
185 }
186 }
187
188 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
189 where
190 D: DrawTarget<Color = Rgb565>,
191 C: crate::render::Compositor<D>,
192 {
193 if self.cell_count == 0 {
194 return Ok(());
195 }
196
197 let cell_w = (bounds.w as i32 / self.cell_count as i32).max(1);
198 let cell_h = bounds.h as i32;
199
200 for i in 0..self.cell_count {
201 let cx = bounds.x + (i as i32 * cell_w);
202 let is_selected = i == self.selected_cell;
203
204 let cell_rect = Rect::new(cx, bounds.y, cell_w as u32, cell_h as u32);
205 let bg_color = if is_selected && self.is_active {
206 self.active_bg_color
207 } else {
208 self.inactive_bg_color
209 };
210 let text_color = if is_selected && self.is_active {
211 self.active_text_color
212 } else {
213 self.inactive_text_color
214 };
215
216 ctx.fill_rounded_rect(cell_rect, 2, bg_color)?;
217
218 let text = if i < self.cell_texts.len() {
219 self.cell_texts[i]
220 } else {
221 ""
222 };
223 let text_y = bounds.y
224 + (if is_selected {
225 self.bump_offset_y as i32
226 } else {
227 0
228 });
229 let text_x = cx + (cell_w / 2) - 4;
230
231 ctx.draw_text(text_x, text_y + (cell_h / 2) - 3, text, text_color)?;
232 }
233 Ok(())
234 }
235}
236
237impl<'a, const MAX_CELLS: usize> Widget for SelectionWidget<'a, MAX_CELLS> {
238 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
239
240 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
241 match key {
242 PropertyKey::Selected => Some(PropertyValue::Int(self.selected_cell as i32)),
243 PropertyKey::State => Some(PropertyValue::Bool(self.is_active)),
244 _ => None,
245 }
246 }
247}
248
249#[derive(Clone, Copy, Debug, PartialEq)]
251pub struct ActionBarWidget<'a> {
252 pub up_icon: Option<char>,
253 pub select_icon: Option<char>,
254 pub down_icon: Option<char>,
255 pub up_label: Option<&'a str>,
256 pub select_label: Option<&'a str>,
257 pub down_label: Option<&'a str>,
258 pub background_color: Rgb565,
259 pub icon_color: Rgb565,
260}
261
262impl<'a> Default for ActionBarWidget<'a> {
263 fn default() -> Self {
264 Self::new()
265 }
266}
267
268impl<'a> ActionBarWidget<'a> {
269 pub const fn new() -> Self {
270 Self {
271 up_icon: None,
272 select_icon: None,
273 down_icon: None,
274 up_label: None,
275 select_label: None,
276 down_label: None,
277 background_color: Rgb565::new(0, 0, 0),
278 icon_color: Rgb565::new(31, 63, 31),
279 }
280 }
281
282 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
283 where
284 D: DrawTarget<Color = Rgb565>,
285 C: crate::render::Compositor<D>,
286 {
287 ctx.fill_rounded_rect(bounds, 2, self.background_color)?;
288
289 let slot_h = bounds.h as i32 / 3;
290 let icon_x = bounds.x + (bounds.w as i32 / 2) - 3;
291
292 if let Some(lbl) = self.up_label {
294 ctx.draw_text(icon_x, bounds.y + (slot_h / 2) - 3, lbl, self.icon_color)?;
295 }
296
297 if let Some(lbl) = self.select_label {
299 ctx.draw_text(
300 icon_x,
301 bounds.y + slot_h + (slot_h / 2) - 3,
302 lbl,
303 self.icon_color,
304 )?;
305 }
306
307 if let Some(lbl) = self.down_label {
309 ctx.draw_text(
310 icon_x,
311 bounds.y + (slot_h * 2) + (slot_h / 2) - 3,
312 lbl,
313 self.icon_color,
314 )?;
315 }
316
317 Ok(())
318 }
319}
320
321impl<'a> Widget for ActionBarWidget<'a> {
322 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
323
324 fn get_property(&self, key: PropertyKey) -> Option<PropertyValue<'_>> {
325 match key {
326 PropertyKey::Text => self.select_label.map(PropertyValue::Str),
327 _ => None,
328 }
329 }
330}
331
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use crate::framebuffer::Framebuffer;
336
337 #[test]
338 fn test_content_indicator_render() {
339 let indicator = ContentIndicatorWidget::new(ContentIndicatorDirection::Down);
340 let mut fb = Framebuffer::<400>::new(20, 20);
341 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 20, 20));
342 assert!(indicator.render(&mut ctx, Rect::new(0, 0, 20, 20)).is_ok());
343 }
344
345 #[test]
346 fn test_crumbs_indicator_render() {
347 let crumbs = CrumbsIndicatorWidget::new(4, 1);
348 let mut fb = Framebuffer::<400>::new(20, 20);
349 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 20, 20));
350 assert!(crumbs.render(&mut ctx, Rect::new(0, 0, 20, 20)).is_ok());
351 }
352
353 #[test]
354 fn test_selection_widget_render() {
355 let sel = SelectionWidget::<3>::new(["12", "34", "56"], 3);
356 let mut fb = Framebuffer::<1200>::new(60, 20);
357 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 60, 20));
358 assert!(sel.render(&mut ctx, Rect::new(0, 0, 60, 20)).is_ok());
359 }
360}