Skip to main content

embedded_gui/
block.rs

1use embedded_graphics_core::{draw_target::DrawTarget, pixelcolor::Rgb565};
2
3use crate::{
4    geometry::{EdgeInsets, Rect},
5    render::{RenderCtx, TextAlign, TextStyle},
6    style::{Border, Style},
7};
8
9#[derive(Clone, Copy, Debug, PartialEq, Eq)]
10pub struct Block<'a> {
11    pub title: Option<&'a str>,
12    pub title_align: TextAlign,
13    pub border: Border,
14    pub style: Style,
15    pub padding: EdgeInsets,
16}
17
18impl<'a> Block<'a> {
19    pub const fn new() -> Self {
20        Self {
21            title: None,
22            title_align: TextAlign::Left,
23            border: Border::none(),
24            style: Style::new(),
25            padding: EdgeInsets::all(0),
26        }
27    }
28
29    pub const fn styled(style: Style) -> Self {
30        Self {
31            title: None,
32            title_align: TextAlign::Left,
33            border: style.border,
34            padding: style.padding,
35            style,
36        }
37    }
38
39    pub const fn title(mut self, title: &'a str) -> Self {
40        self.title = Some(title);
41        self
42    }
43
44    pub const fn title_align(mut self, align: TextAlign) -> Self {
45        self.title_align = align;
46        self
47    }
48
49    pub const fn border(mut self, border: Border) -> Self {
50        self.border = border;
51        self
52    }
53
54    pub const fn padding(mut self, padding: EdgeInsets) -> Self {
55        self.padding = padding;
56        self
57    }
58
59    pub fn inner(self, rect: Rect) -> Rect {
60        let border = self.border.width as i16;
61        rect.inset(EdgeInsets {
62            left: self.padding.left.saturating_add(border),
63            right: self.padding.right.saturating_add(border),
64            top: self.padding.top.saturating_add(border),
65            bottom: self.padding.bottom.saturating_add(border),
66        })
67    }
68
69    pub fn title_area(self, rect: Rect) -> Option<Rect> {
70        self.title.map(|_| {
71            Rect::new(
72                rect.x + self.border.width as i32 + self.padding.left.max(0) as i32,
73                rect.y,
74                rect.w
75                    .saturating_sub(self.border.width as u32 * 2)
76                    .saturating_sub(self.padding.left.max(0) as u32)
77                    .saturating_sub(self.padding.right.max(0) as u32),
78                self.style.font.line_height() + 1,
79            )
80        })
81    }
82
83    pub fn content_area(self, rect: Rect) -> Rect {
84        let inner = self.inner(rect);
85        if self.title.is_none() {
86            return inner;
87        }
88
89        let title_h = self.style.font.line_height() + 3;
90        Rect::new(
91            inner.x,
92            inner.y + title_h as i32,
93            inner.w,
94            inner.h.saturating_sub(title_h),
95        )
96    }
97
98    pub fn render<D>(self, rect: Rect, ctx: &mut RenderCtx<'_, D>) -> Result<(), D::Error>
99    where
100        D: DrawTarget<Color = Rgb565>,
101    {
102        if let Some(shadow) = self.style.shadow {
103            let spread = ctx.shadow_spread_for(shadow.spread);
104            let mut i = 0u8;
105            while i < spread {
106                let grow = i as i32;
107                let shadow_rect = Rect::new(
108                    rect.x + shadow.offset_x as i32 - grow,
109                    rect.y + shadow.offset_y as i32 - grow,
110                    rect.w.saturating_add((i as u32) * 2),
111                    rect.h.saturating_add((i as u32) * 2),
112                );
113                let fade = spread as u16;
114                let opacity = ((shadow.opacity as u16) * (fade - i as u16) / fade) as u8;
115                let radius = self.style.corner_radius.saturating_add(i);
116                ctx.fill_rounded_rect_alpha(shadow_rect, radius, shadow.color, opacity)?;
117                i += 1;
118            }
119        }
120
121        if let Some(gradient) = self.style.gradient {
122            ctx.fill_rounded_rect_gradient_alpha(
123                rect,
124                self.style.corner_radius,
125                gradient,
126                self.style.opacity,
127            )?;
128        } else if let Some(bg) = self.style.background {
129            ctx.fill_rounded_rect_alpha(rect, self.style.corner_radius, bg, self.style.opacity)?;
130        }
131        ctx.stroke_rounded_rect_alpha(
132            rect,
133            self.style.corner_radius,
134            self.border,
135            self.style.opacity,
136        )?;
137
138        if let Some(title) = self.title {
139            let title_rect = self.title_area(rect).unwrap_or(Rect::empty());
140            ctx.draw_text_in(
141                title_rect,
142                title,
143                TextStyle::new(self.style.accent)
144                    .with_font(self.style.font)
145                    .with_align(self.title_align)
146                    .with_opacity(self.style.opacity),
147            )?;
148        }
149
150        Ok(())
151    }
152}
153
154impl Default for Block<'_> {
155    fn default() -> Self {
156        Self::new()
157    }
158}