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: impl Into<Rect>) -> Rect {
60        let rect = rect.into();
61        let border = self.border.width as i16;
62        rect.inset(EdgeInsets {
63            left: self.padding.left.saturating_add(border),
64            right: self.padding.right.saturating_add(border),
65            top: self.padding.top.saturating_add(border),
66            bottom: self.padding.bottom.saturating_add(border),
67        })
68    }
69
70    pub fn title_area(self, rect: impl Into<Rect>) -> Option<Rect> {
71        let rect = rect.into();
72        self.title.map(|_| {
73            Rect::new(
74                rect.x + self.border.width as i32 + self.padding.left.max(0) as i32,
75                rect.y,
76                rect.w
77                    .saturating_sub(self.border.width as u32 * 2)
78                    .saturating_sub(self.padding.left.max(0) as u32)
79                    .saturating_sub(self.padding.right.max(0) as u32),
80                self.style.font.line_height() + 1,
81            )
82        })
83    }
84
85    pub fn content_area(self, rect: impl Into<Rect>) -> Rect {
86        let rect = rect.into();
87        let inner = self.inner(rect);
88        if self.title.is_none() {
89            return inner;
90        }
91
92        let title_h = self.style.font.line_height() + 3;
93        Rect::new(
94            inner.x,
95            inner.y + title_h as i32,
96            inner.w,
97            inner.h.saturating_sub(title_h),
98        )
99    }
100
101    pub fn render<D, C>(
102        self,
103        rect: impl Into<Rect>,
104        ctx: &mut RenderCtx<'_, D, C>,
105    ) -> Result<(), D::Error>
106    where
107        D: DrawTarget<Color = Rgb565>,
108        C: Compositor<D>,
109    {
110        let rect = rect.into();
111        if let Some(shadow) = self.style.shadow {
112            let spread = ctx.shadow_spread_for(shadow.spread);
113            let mut i = 0u8;
114            while i < spread {
115                let grow = i as i32;
116                let shadow_rect = Rect::new(
117                    rect.x + shadow.offset_x as i32 - grow,
118                    rect.y + shadow.offset_y as i32 - grow,
119                    rect.w.saturating_add((i as u32) * 2),
120                    rect.h.saturating_add((i as u32) * 2),
121                );
122                let fade = spread as u16;
123                let opacity = ((shadow.opacity as u16) * (fade - i as u16) / fade) as u8;
124                let radius = self.style.corner_radius.saturating_add(i);
125                ctx.fill_rounded_rect_alpha(shadow_rect, radius, shadow.color, opacity)?;
126                i += 1;
127            }
128        }
129
130        if let Some(gradient) = self.style.gradient {
131            ctx.fill_rounded_rect_gradient_alpha(
132                rect,
133                self.style.corner_radius,
134                gradient,
135                self.style.opacity,
136            )?;
137        } else if let Some(bg) = self.style.background {
138            ctx.fill_rounded_rect_alpha(rect, self.style.corner_radius, bg, self.style.opacity)?;
139        }
140        ctx.stroke_rounded_rect_alpha(
141            rect,
142            self.style.corner_radius,
143            self.border,
144            self.style.opacity,
145        )?;
146
147        if let Some(title) = self.title {
148            let title_rect = self.title_area(rect).unwrap_or(Rect::empty());
149            ctx.draw_text_in(
150                title_rect,
151                title,
152                TextStyle::new(self.style.accent)
153                    .with_font(self.style.font)
154                    .with_align(self.title_align)
155                    .with_opacity(self.style.opacity),
156            )?;
157        }
158
159        Ok(())
160    }
161}
162
163impl Default for Block<'_> {
164    fn default() -> Self {
165        Self::new()
166    }
167}