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}