embedded_gui/widgets/
rich_text_node.rs1use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
7use heapless::Vec;
8
9use crate::{
10 geometry::Rect,
11 render::RenderCtx,
12 style::Style,
13 widget::{PropertyKey, PropertyValue, Widget},
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct RichTextError;
19
20#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub struct TextSpan<'a> {
23 pub text: &'a str,
24 pub color: Rgb565,
25 pub background: Option<Rgb565>,
26}
27
28impl<'a> TextSpan<'a> {
29 pub const fn plain(text: &'a str, color: Rgb565) -> Self {
30 Self {
31 text,
32 color,
33 background: None,
34 }
35 }
36
37 pub const fn badge(text: &'a str, color: Rgb565, bg: Rgb565) -> Self {
38 Self {
39 text,
40 color,
41 background: Some(bg),
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq)]
48pub struct RichTextNodeWidget<'a, const MAX_SPANS: usize = 6> {
49 pub spans: Vec<TextSpan<'a>, MAX_SPANS>,
50 pub line_spacing: u8,
51}
52
53impl<'a, const MAX_SPANS: usize> RichTextNodeWidget<'a, MAX_SPANS> {
54 pub const fn new() -> Self {
55 Self {
56 spans: Vec::new(),
57 line_spacing: 4,
58 }
59 }
60
61 pub fn push_span(&mut self, span: TextSpan<'a>) -> Result<(), RichTextError> {
62 self.spans.push(span).map_err(|_| RichTextError)
63 }
64
65 pub fn render<D, C>(&self, ctx: &mut RenderCtx<'_, D, C>, bounds: Rect) -> Result<(), D::Error>
66 where
67 D: DrawTarget<Color = Rgb565>,
68 C: crate::render::Compositor<D>,
69 {
70 let mut cursor_x = bounds.x;
71 let mut cursor_y = bounds.y;
72 let font_w = 6;
73 let font_h = 10;
74
75 for span in &self.spans {
76 let span_len = span.text.len() as i32;
77 let span_px_w = span_len * font_w;
78
79 if cursor_x > bounds.x && cursor_x + span_px_w > bounds.right() {
81 cursor_x = bounds.x;
82 cursor_y += font_h + self.line_spacing as i32;
83 }
84
85 if let Some(bg) = span.background {
86 let badge_rect = Rect::new(
87 cursor_x - 2,
88 cursor_y - 1,
89 span_px_w as u32 + 4,
90 font_h as u32 + 2,
91 );
92 ctx.fill_rounded_rect(badge_rect, 2, bg)?;
93 }
94
95 ctx.draw_text(cursor_x, cursor_y, span.text, span.color)?;
96 cursor_x += span_px_w + 6;
97 }
98
99 Ok(())
100 }
101}
102
103impl<'a, const MAX_SPANS: usize> Default for RichTextNodeWidget<'a, MAX_SPANS> {
104 fn default() -> Self {
105 Self::new()
106 }
107}
108
109impl<'a, const MAX_SPANS: usize> Widget for RichTextNodeWidget<'a, MAX_SPANS> {
110 fn render_widget_bounds(&self, _bounds: Rect, _style: &Style) {}
111
112 fn get_property(&self, _key: PropertyKey) -> Option<PropertyValue<'_>> {
113 None
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use crate::framebuffer::Framebuffer;
121
122 #[test]
123 fn test_rich_text_node_render() {
124 let mut node = RichTextNodeWidget::<4>::new();
125 assert!(
126 node.push_span(TextSpan::badge(
127 "ALERT",
128 Rgb565::new(31, 63, 31),
129 Rgb565::new(31, 0, 0),
130 ))
131 .is_ok()
132 );
133 assert!(
134 node.push_span(TextSpan::plain(
135 "Sensor reading nominal",
136 Rgb565::new(31, 63, 31),
137 ))
138 .is_ok()
139 );
140
141 let mut fb = Framebuffer::<24000>::new(200, 40);
142 let mut ctx = RenderCtx::new(&mut fb, Rect::new(0, 0, 200, 40));
143 assert!(node.render(&mut ctx, Rect::new(0, 0, 200, 40)).is_ok());
144 }
145}