1use super::{Widget, button::Button, column::Column, element::Element, row::Row, text::Text};
30use alloc::{string::String, vec::Vec};
31use core::marker::PhantomData;
32use embedded_graphics::{pixelcolor::PixelColor, prelude::*, primitives::Rectangle};
33use zest_core::{Constraints, Horizontal, Length, RenderError, Renderer, TouchPhase, Vertical};
34use zest_theme::Theme;
35
36const CARD_W: u32 = 260;
38const CARD_PAD: u32 = 12;
40const BUTTON_H: u32 = 36;
42
43pub struct MessageBox<'a, C: PixelColor, M: Clone> {
47 title: String,
48 body: String,
49 buttons: Vec<(String, M)>,
50 on_dismiss: Option<M>,
51 width: Length,
52 height: Length,
53 stack: Option<Element<'a, C, M>>,
55}
56
57impl<'a, C: PixelColor + 'a, M: Clone + 'a> MessageBox<'a, C, M> {
58 pub fn new() -> Self {
62 Self {
63 title: String::new(),
64 body: String::new(),
65 buttons: Vec::new(),
66 on_dismiss: None,
67 width: Length::Fill,
68 height: Length::Fill,
69 stack: None,
70 }
71 }
72
73 #[must_use]
75 pub fn title(mut self, title: impl Into<String>) -> Self {
76 self.title = title.into();
77 self
78 }
79
80 #[must_use]
82 pub fn body(mut self, body: impl Into<String>) -> Self {
83 self.body = body.into();
84 self
85 }
86
87 #[must_use]
91 pub fn button(mut self, label: impl Into<String>, message: M) -> Self {
92 self.buttons.push((label.into(), message));
93 self
94 }
95
96 #[must_use]
100 pub fn on_dismiss(mut self, message: M) -> Self {
101 self.on_dismiss = Some(message);
102 self
103 }
104
105 #[must_use]
108 pub fn width(mut self, width: impl Into<Length>) -> Self {
109 self.width = width.into();
110 self
111 }
112
113 #[must_use]
116 pub fn height(mut self, height: impl Into<Length>) -> Self {
117 self.height = height.into();
118 self
119 }
120
121 fn build(&mut self) -> Element<'a, C, M> {
124 let mut card = Column::new()
126 .spacing(8)
127 .width(Length::Fill)
128 .height(Length::Shrink);
129
130 card = card.push(
131 Text::new(self.title.clone())
132 .align_x(Horizontal::Center)
133 .height(Length::Shrink),
134 );
135 card = card.push(
136 Text::new(self.body.clone())
137 .align_x(Horizontal::Center)
138 .height(Length::Shrink),
139 );
140
141 let mut button_row = Row::new()
142 .spacing(8)
143 .width(Length::Fill)
144 .height(Length::Fixed(BUTTON_H));
145 for (label, msg) in core::mem::take(&mut self.buttons) {
146 button_row = button_row.push(Button::new(label).on_press(msg));
147 }
148 card = card.push(button_row);
149
150 let panel = MessageCard {
154 rect: Rectangle::zero(),
155 inner: Element::new(card),
156 card_width: Length::Fixed(CARD_W),
157 card_height: Length::Shrink,
158 _color: PhantomData,
159 };
160
161 let scrim = Scrim {
163 rect: Rectangle::zero(),
164 on_dismiss: self.on_dismiss.take(),
165 _color: PhantomData,
166 };
167
168 let stack = super::stack::Stack::new()
169 .width(self.width)
170 .height(self.height)
171 .push_aligned(scrim, Horizontal::Left, Vertical::Top)
172 .push_aligned(panel, Horizontal::Center, Vertical::Center);
173
174 Element::new(stack)
175 }
176
177 fn ensure_built(&mut self) {
178 if self.stack.is_none() {
179 self.stack = Some(self.build());
180 }
181 }
182}
183
184impl<'a, C: PixelColor + 'a, M: Clone + 'a> Default for MessageBox<'a, C, M> {
185 fn default() -> Self {
186 Self::new()
187 }
188}
189
190impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for MessageBox<'a, C, M> {
191 fn measure(&mut self, constraints: Constraints) -> Size {
192 self.ensure_built();
193 self.stack
194 .as_mut()
195 .map_or(Size::zero(), |s| s.measure(constraints))
196 }
197
198 fn preferred_size(&self) -> (Length, Length) {
199 (self.width, self.height)
200 }
201
202 fn arrange(&mut self, rect: Rectangle) {
203 self.ensure_built();
204 if let Some(stack) = self.stack.as_mut() {
205 stack.arrange(rect);
206 }
207 }
208
209 fn rect(&self) -> Rectangle {
210 self.stack.as_ref().map_or(Rectangle::zero(), |s| s.rect())
211 }
212
213 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
214 self.stack
215 .as_mut()
216 .and_then(|s| s.handle_touch(point, phase))
217 }
218
219 fn mark_pressed(&mut self, point: Point) {
220 if let Some(stack) = self.stack.as_mut() {
221 stack.mark_pressed(point);
222 }
223 }
224
225 fn draw<'t>(
226 &self,
227 renderer: &mut dyn Renderer<C>,
228 theme: &Theme<'t, C>,
229 ) -> Result<(), RenderError> {
230 if let Some(stack) = &self.stack {
231 stack.draw(renderer, theme)?;
232 }
233 Ok(())
234 }
235}
236
237struct Scrim<C: PixelColor, M: Clone> {
243 rect: Rectangle,
244 on_dismiss: Option<M>,
245 _color: PhantomData<C>,
246}
247
248impl<C: PixelColor, M: Clone> Scrim<C, M> {
249 fn hit_test(&self, point: Point) -> bool {
250 let tl = self.rect.top_left;
251 let br = tl + Point::new(self.rect.size.width as i32, self.rect.size.height as i32);
252 point.x >= tl.x && point.x < br.x && point.y >= tl.y && point.y < br.y
253 }
254}
255
256impl<C: PixelColor, M: Clone> Widget<C, M> for Scrim<C, M> {
257 fn measure(&mut self, constraints: Constraints) -> Size {
258 constraints.max
259 }
260
261 fn preferred_size(&self) -> (Length, Length) {
262 (Length::Fill, Length::Fill)
263 }
264
265 fn arrange(&mut self, rect: Rectangle) {
266 self.rect = rect;
267 }
268
269 fn rect(&self) -> Rectangle {
270 self.rect
271 }
272
273 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
274 if !self.hit_test(point) {
276 return None;
277 }
278 match phase {
279 TouchPhase::Up => self.on_dismiss.clone(),
280 _ => None,
281 }
282 }
283
284 fn draw<'t>(
285 &self,
286 renderer: &mut dyn Renderer<C>,
287 theme: &Theme<'t, C>,
288 ) -> Result<(), RenderError> {
289 renderer.fill_rect(self.rect, theme.palette.neutral_10)?;
291 Ok(())
292 }
293}
294
295struct MessageCard<'a, C: PixelColor, M: Clone> {
300 rect: Rectangle,
301 inner: Element<'a, C, M>,
302 card_width: Length,
303 card_height: Length,
304 _color: PhantomData<C>,
305}
306
307impl<'a, C: PixelColor + 'a, M: Clone + 'a> Widget<C, M> for MessageCard<'a, C, M> {
308 fn measure(&mut self, constraints: Constraints) -> Size {
309 let pad2 = CARD_PAD * 2;
310 let inner_c = constraints.shrink(pad2, pad2);
311 let inner = self.inner.measure(inner_c);
312 let w = self.card_width.resolve(CARD_W, constraints.max.width);
313 let h = self
314 .card_height
315 .resolve(inner.height + pad2, constraints.max.height);
316 constraints.clamp(Size::new(w, h))
317 }
318
319 fn preferred_size(&self) -> (Length, Length) {
320 (self.card_width, self.card_height)
321 }
322
323 fn arrange(&mut self, rect: Rectangle) {
324 self.rect = rect;
325 let pad = CARD_PAD as i32;
326 let inner_rect = Rectangle::new(
327 rect.top_left + Point::new(pad, pad),
328 Size::new(
329 rect.size.width.saturating_sub(CARD_PAD * 2),
330 rect.size.height.saturating_sub(CARD_PAD * 2),
331 ),
332 );
333 self.inner.arrange(inner_rect);
334 }
335
336 fn rect(&self) -> Rectangle {
337 self.rect
338 }
339
340 fn handle_touch(&mut self, point: Point, phase: TouchPhase) -> Option<M> {
341 self.inner.handle_touch(point, phase)
342 }
343
344 fn mark_pressed(&mut self, point: Point) {
345 self.inner.mark_pressed(point);
346 }
347
348 fn draw<'t>(
349 &self,
350 renderer: &mut dyn Renderer<C>,
351 theme: &Theme<'t, C>,
352 ) -> Result<(), RenderError> {
353 renderer.fill_rect(self.rect, theme.primary.base)?;
354 renderer.stroke_rect(self.rect, theme.background.divider)?;
355 self.inner.draw(renderer, theme)?;
356 Ok(())
357 }
358}