use core::fmt::Debug;
use embedded_graphics_core::{
draw_target::DrawTarget,
pixelcolor::{Rgb565, WebColors},
};
use heapless::{String, Vec};
use crate::{
geometry::Rect,
render::{Compositor, RenderCtx},
style::Border,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DialogError {
RenderError,
CapacityExceeded,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum DialogType {
#[default]
Info,
Warning,
Error,
Success,
Question,
}
#[derive(Clone, Debug)]
pub struct DialogAction {
pub label: String<16>,
pub action_id: u16,
pub is_destructive: bool,
}
impl DialogAction {
pub fn new(label: &str, action_id: u16) -> Self {
let mut text = String::new();
let _ = text.push_str(label);
Self {
label: text,
action_id,
is_destructive: false,
}
}
pub fn destructive(label: &str, action_id: u16) -> Self {
let mut text = String::new();
let _ = text.push_str(label);
Self {
label: text,
action_id,
is_destructive: true,
}
}
}
#[derive(Clone, Debug)]
pub struct ActionableDialogWidget<const MAX_ACTIONS: usize = 3> {
pub dialog_type: DialogType,
pub title: String<24>,
pub message: String<64>,
pub actions: Vec<DialogAction, MAX_ACTIONS>,
pub selected_action: usize,
pub background_color: Rgb565,
pub border_color: Rgb565,
}
impl<const MAX_ACTIONS: usize> ActionableDialogWidget<MAX_ACTIONS> {
pub fn new(title: &str, message: &str, dialog_type: DialogType) -> Self {
let mut title_str = String::new();
let _ = title_str.push_str(title);
let mut msg_str = String::new();
let _ = msg_str.push_str(message);
let border_color = match dialog_type {
DialogType::Info => Rgb565::CSS_CYAN,
DialogType::Warning => Rgb565::CSS_ORANGE,
DialogType::Error => Rgb565::CSS_RED,
DialogType::Success => Rgb565::CSS_GREEN,
DialogType::Question => Rgb565::CSS_GOLD,
};
Self {
dialog_type,
title: title_str,
message: msg_str,
actions: Vec::new(),
selected_action: 0,
background_color: Rgb565::new(4, 8, 14),
border_color,
}
}
pub fn add_action(&mut self, action: DialogAction) -> Result<(), DialogError> {
self.actions
.push(action)
.map_err(|_| DialogError::CapacityExceeded)
}
pub fn select_next(&mut self) {
if !self.actions.is_empty() {
self.selected_action = (self.selected_action + 1) % self.actions.len();
}
}
pub fn select_prev(&mut self) {
if !self.actions.is_empty() {
self.selected_action = if self.selected_action == 0 {
self.actions.len() - 1
} else {
self.selected_action - 1
};
}
}
pub fn current_action_id(&self) -> Option<u16> {
self.actions.get(self.selected_action).map(|a| a.action_id)
}
pub fn render<D, C>(
&self,
ctx: &mut RenderCtx<'_, D, C>,
bounds: Rect,
) -> Result<(), DialogError>
where
D: DrawTarget<Color = Rgb565>,
C: Compositor<D>,
{
if bounds.is_empty() {
return Ok(());
}
ctx.fill_rounded_rect(bounds, 6, self.background_color)
.map_err(|_| DialogError::RenderError)?;
ctx.stroke_rounded_rect(bounds, 6, Border::one(self.border_color))
.map_err(|_| DialogError::RenderError)?;
let header_y = bounds.y + 10;
let (icon_symbol, icon_color) = match self.dialog_type {
DialogType::Info => ("(i)", Rgb565::CSS_CYAN),
DialogType::Warning => ("(!)", Rgb565::CSS_ORANGE),
DialogType::Error => ("(X)", Rgb565::CSS_RED),
DialogType::Success => ("(V)", Rgb565::CSS_GREEN),
DialogType::Question => ("(?)", Rgb565::CSS_GOLD),
};
ctx.draw_text(bounds.x + 12, header_y, icon_symbol, icon_color)
.map_err(|_| DialogError::RenderError)?;
ctx.draw_text(bounds.x + 32, header_y, &self.title, Rgb565::CSS_WHITE)
.map_err(|_| DialogError::RenderError)?;
ctx.draw_text(
bounds.x + 12,
header_y + 18,
&self.message,
Rgb565::new(20, 40, 30),
)
.map_err(|_| DialogError::RenderError)?;
if !self.actions.is_empty() {
let num_acts = self.actions.len() as i32;
let spacing = 6i32;
let total_spacing = spacing * (num_acts - 1);
let btn_w = ((bounds.w as i32 - 24 - total_spacing) / num_acts).max(36) as u32;
let btn_h = 20u32;
let btn_y = bounds.bottom() - 10 - btn_h as i32;
let start_x = bounds.x + 12;
for (i, action) in self.actions.iter().enumerate() {
let is_selected = i == self.selected_action;
let bx = start_x + (i as i32 * (btn_w as i32 + spacing));
let btn_rect = Rect::new(bx, btn_y, btn_w, btn_h);
let bg = if is_selected {
if action.is_destructive {
Rgb565::new(30, 4, 4)
} else {
Rgb565::new(0, 35, 45)
}
} else {
Rgb565::new(6, 12, 18)
};
let border = if is_selected {
if action.is_destructive {
Rgb565::CSS_RED
} else {
Rgb565::CSS_CYAN
}
} else {
Rgb565::new(10, 20, 30)
};
ctx.fill_rounded_rect(btn_rect, 3, bg)
.map_err(|_| DialogError::RenderError)?;
ctx.stroke_rounded_rect(btn_rect, 3, Border::one(border))
.map_err(|_| DialogError::RenderError)?;
let text_x = btn_rect.x + (btn_rect.w as i32 - (action.label.len() as i32 * 4)) / 2;
let text_y = btn_rect.y + 6;
let text_color = if is_selected {
Rgb565::CSS_WHITE
} else {
Rgb565::new(15, 30, 25)
};
ctx.draw_text(text_x, text_y, &action.label, text_color)
.map_err(|_| DialogError::RenderError)?;
}
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub struct ConfirmationDialogWidget {
pub dialog: ActionableDialogWidget<2>,
}
impl ConfirmationDialogWidget {
pub fn new(title: &str, message: &str, confirm_id: u16, cancel_id: u16) -> Self {
let mut dialog = ActionableDialogWidget::new(title, message, DialogType::Question);
let _ = dialog.add_action(DialogAction::new("CANCEL", cancel_id));
let _ = dialog.add_action(DialogAction::new("CONFIRM", confirm_id));
dialog.selected_action = 1; Self { dialog }
}
pub fn render<D, C>(
&self,
ctx: &mut RenderCtx<'_, D, C>,
bounds: Rect,
) -> Result<(), DialogError>
where
D: DrawTarget<Color = Rgb565>,
C: Compositor<D>,
{
self.dialog.render(ctx, bounds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::framebuffer::Framebuffer;
#[test]
fn test_dialog_actions_and_render() {
let screen = Rect::new(0, 0, 240, 240);
let mut fb = Framebuffer::<{ 240 * 240 }>::new(240, 240);
let mut ctx = RenderCtx::new(&mut fb, screen);
let mut dialog = ActionableDialogWidget::<3>::new(
"DELETE ENTRY?",
"This action cannot be undone.",
DialogType::Warning,
);
assert!(dialog.add_action(DialogAction::new("CANCEL", 1)).is_ok());
assert!(
dialog
.add_action(DialogAction::destructive("DELETE", 2))
.is_ok()
);
assert_eq!(dialog.current_action_id(), Some(1));
dialog.select_next();
assert_eq!(dialog.current_action_id(), Some(2));
let res = dialog.render(&mut ctx, Rect::new(10, 60, 220, 100));
assert!(res.is_ok());
}
#[test]
fn test_confirmation_dialog() {
let confirm = ConfirmationDialogWidget::new("SYNC DATA", "Upload 12 pending logs?", 10, 20);
assert_eq!(confirm.dialog.actions.len(), 2);
}
}