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}