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::{Compositor, 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, C>(self, rect: Rect, ctx: &mut RenderCtx<'_, D, C>) -> Result<(), D::Error>
99    where
100        D: DrawTarget<Color = Rgb565>,
101        C: Compositor<D>,
102    {
103        if let Some(shadow) = self.style.shadow {
104            let spread = ctx.shadow_spread_for(shadow.spread);
105            let mut i = 0u8;
106            while i < spread {
107                let grow = i as i32;
108                let shadow_rect = Rect::new(
109                    rect.x + shadow.offset_x as i32 - grow,
110                    rect.y + shadow.offset_y as i32 - grow,
111                    rect.w.saturating_add((i as u32) * 2),
112                    rect.h.saturating_add((i as u32) * 2),
113                );
114                let fade = spread as u16;
115                let opacity = ((shadow.opacity as u16) * (fade - i as u16) / fade) as u8;
116                let radius = self.style.corner_radius.saturating_add(i);
117                ctx.fill_rounded_rect_alpha(shadow_rect, radius, shadow.color, opacity)?;
118                i += 1;
119            }
120        }
121
122        if let Some(gradient) = self.style.gradient {
123            ctx.fill_rounded_rect_gradient_alpha(
124                rect,
125                self.style.corner_radius,
126                gradient,
127                self.style.opacity,
128            )?;
129        } else if let Some(bg) = self.style.background {
130            ctx.fill_rounded_rect_alpha(rect, self.style.corner_radius, bg, self.style.opacity)?;
131        }
132        ctx.stroke_rounded_rect_alpha(
133            rect,
134            self.style.corner_radius,
135            self.border,
136            self.style.opacity,
137        )?;
138
139        if let Some(title) = self.title {
140            let title_rect = self.title_area(rect).unwrap_or(Rect::empty());
141            ctx.draw_text_in(
142                title_rect,
143                title,
144                TextStyle::new(self.style.accent)
145                    .with_font(self.style.font)
146                    .with_align(self.title_align)
147                    .with_opacity(self.style.opacity),
148            )?;
149        }
150
151        Ok(())
152    }
153}
154
155impl Default for Block<'_> {
156    fn default() -> Self {
157        Self::new()
158    }
159}