use alloc::string::String;
use alloc::vec::Vec;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::font::{FontMetrics, WidgetFont, shape_text_ltr, wrap_greedy_ltr};
use rlvgl_core::renderer::{ClipRenderer, Renderer};
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Color, Rect, Widget};
use crate::button_matrix::{BUTTON_NONE, ButtonId, ButtonMatrix};
const HEADER_HEIGHT_PX: i32 = 32;
const FOOTER_HEIGHT_PX: i32 = 40;
pub struct MessageBox {
bounds: Rect,
title: String,
text: String,
buttons: ButtonMatrix,
active_button: ButtonId,
closed: bool,
pub style: Style,
pub text_color: Color,
pub button_text_color: Color,
font: WidgetFont,
}
impl MessageBox {
pub fn new(bounds: Rect, title: &str, text: &str, buttons: &[&str]) -> Self {
let footer_rect = Self::footer_rect_for(bounds);
let mut bm = ButtonMatrix::new(footer_rect);
let map: Vec<&str> = buttons.to_vec();
if !map.is_empty() {
bm.set_map(&map);
}
Self {
bounds,
title: String::from(title),
text: String::from(text),
buttons: bm,
active_button: BUTTON_NONE,
closed: false,
style: Style::default(),
text_color: Color(0, 0, 0, 255),
button_text_color: Color(50, 50, 50, 255),
font: WidgetFont::new(),
}
}
pub fn set_font(&mut self, font: &'static dyn FontMetrics) {
self.font.set(font);
}
pub fn title(&self) -> &str {
&self.title
}
pub fn set_title(&mut self, title: &str) {
self.title = String::from(title);
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: &str) {
self.text = String::from(text);
}
pub fn active_button(&self) -> ButtonId {
self.active_button
}
pub fn activate_button(&mut self, id: ButtonId) {
let count = self.buttons.button_count();
if id != BUTTON_NONE && (id.0 as usize) < count {
self.active_button = id;
}
}
pub fn button_text(&self, id: ButtonId) -> Option<&str> {
self.buttons.button_text(id)
}
pub fn button_count(&self) -> usize {
self.buttons.button_count()
}
pub fn close(&mut self) -> ButtonId {
self.closed = true;
self.active_button
}
pub fn is_closed(&self) -> bool {
self.closed
}
fn header_rect(&self) -> Rect {
Rect {
x: self.bounds.x,
y: self.bounds.y,
width: self.bounds.width,
height: HEADER_HEIGHT_PX.min(self.bounds.height),
}
}
fn content_rect(&self) -> Rect {
let top = self.bounds.y + HEADER_HEIGHT_PX;
let bottom = self.bounds.y + self.bounds.height - FOOTER_HEIGHT_PX;
Rect {
x: self.bounds.x,
y: top,
width: self.bounds.width,
height: (bottom - top).max(0),
}
}
fn footer_rect_for(bounds: Rect) -> Rect {
let top = bounds.y + bounds.height - FOOTER_HEIGHT_PX;
Rect {
x: bounds.x,
y: top.max(bounds.y),
width: bounds.width,
height: FOOTER_HEIGHT_PX.min(bounds.height),
}
}
fn draw_wrapped_text(&self, renderer: &mut dyn Renderer, text: &str, clip: Rect) {
let font = self.font.resolve();
let lm = font.line_metrics();
let line_h = lm.line_height as i32;
let wrapped = wrap_greedy_ltr(font, text, clip.width, 0, 0);
let mut clip_r = ClipRenderer::new(renderer, clip);
for (i, wl) in wrapped.lines.iter().enumerate() {
let baseline = clip.y + i as i32 * line_h + lm.ascent as i32;
if baseline > clip.y + clip.height {
break;
}
let segment = &text[wl.start..wl.end];
if segment.is_empty() {
continue;
}
let shaped = shape_text_ltr(font, segment, (clip.x, baseline), 0);
clip_r.draw_text_shaped(&shaped, (0, 0), self.text_color);
}
}
}
impl Widget for MessageBox {
fn bounds(&self) -> Rect {
self.bounds
}
fn widget_font_mut(&mut self) -> Option<&mut WidgetFont> {
Some(&mut self.font)
}
fn set_bounds(&mut self, new_bounds: Rect) {
self.bounds = new_bounds;
self.buttons.set_bounds(Self::footer_rect_for(new_bounds));
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
let header = self.header_rect();
let font = self.font.resolve();
let lm = font.line_metrics();
let title_baseline =
header.y + lm.ascent as i32 + (HEADER_HEIGHT_PX - lm.line_height as i32) / 2;
if !self.title.is_empty() {
let shaped = shape_text_ltr(font, &self.title, (header.x, title_baseline), 0);
let mut clip_r = ClipRenderer::new(renderer, header);
clip_r.draw_text_shaped(&shaped, (0, 0), self.text_color);
}
let content = self.content_rect();
if !self.text.is_empty() {
self.draw_wrapped_text(renderer, &self.text, content);
}
self.buttons.draw(renderer);
}
fn handle_event(&mut self, event: &Event) -> bool {
if self.buttons.handle_event(event) {
let pressed = self.buttons.selected_button();
if pressed != BUTTON_NONE {
self.active_button = pressed;
}
return true;
}
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use rlvgl_core::widget::Rect;
const BOUNDS: Rect = Rect {
x: 0,
y: 0,
width: 300,
height: 200,
};
#[test]
fn new_sets_title_and_text() {
let mb = MessageBox::new(BOUNDS, "Alert", "Something happened", &["OK", "Cancel"]);
assert_eq!(mb.title(), "Alert");
assert_eq!(mb.text(), "Something happened");
}
#[test]
fn button_count_matches_slice() {
let mb = MessageBox::new(BOUNDS, "T", "M", &["Yes", "No", "Maybe"]);
assert_eq!(mb.button_count(), 3);
}
#[test]
fn button_labels_are_accessible() {
let mb = MessageBox::new(BOUNDS, "T", "M", &["OK", "Cancel"]);
assert_eq!(mb.button_text(ButtonId(0)), Some("OK"));
assert_eq!(mb.button_text(ButtonId(1)), Some("Cancel"));
assert_eq!(mb.button_text(ButtonId(2)), None);
}
#[test]
fn no_buttons_is_valid() {
let mb = MessageBox::new(BOUNDS, "Info", "No action needed", &[]);
assert_eq!(mb.button_count(), 0);
}
#[test]
fn set_title_updates() {
let mut mb = MessageBox::new(BOUNDS, "Old", "msg", &["OK"]);
mb.set_title("New");
assert_eq!(mb.title(), "New");
}
#[test]
fn set_text_updates() {
let mut mb = MessageBox::new(BOUNDS, "T", "old msg", &["OK"]);
mb.set_text("new msg");
assert_eq!(mb.text(), "new msg");
}
#[test]
fn active_button_initially_none() {
let mb = MessageBox::new(BOUNDS, "T", "M", &["OK"]);
assert_eq!(mb.active_button(), BUTTON_NONE);
}
#[test]
fn activate_button_records_id() {
let mut mb = MessageBox::new(BOUNDS, "T", "M", &["OK", "Cancel"]);
mb.activate_button(ButtonId(1));
assert_eq!(mb.active_button(), ButtonId(1));
}
#[test]
fn activate_out_of_range_is_a_noop() {
let mut mb = MessageBox::new(BOUNDS, "T", "M", &["OK"]);
mb.activate_button(ButtonId(99));
assert_eq!(mb.active_button(), BUTTON_NONE);
}
#[test]
fn close_returns_active_button_and_sets_closed() {
let mut mb = MessageBox::new(BOUNDS, "T", "M", &["OK", "Cancel"]);
mb.activate_button(ButtonId(0));
let result = mb.close();
assert_eq!(result, ButtonId(0));
assert!(mb.is_closed());
}
#[test]
fn close_without_selection_returns_none() {
let mut mb = MessageBox::new(BOUNDS, "T", "M", &["OK"]);
let result = mb.close();
assert_eq!(result, BUTTON_NONE);
assert!(mb.is_closed());
}
#[test]
fn set_bounds_updates_footer_rect() {
let mut mb = MessageBox::new(BOUNDS, "T", "M", &["OK"]);
let new_bounds = Rect {
x: 10,
y: 20,
width: 400,
height: 250,
};
mb.set_bounds(new_bounds);
assert_eq!(mb.bounds(), new_bounds);
let footer = MessageBox::footer_rect_for(new_bounds);
let expected_footer_top = new_bounds.y + new_bounds.height - FOOTER_HEIGHT_PX;
assert_eq!(footer.y, expected_footer_top);
assert_eq!(footer.width, new_bounds.width);
}
#[test]
fn layout_bands_are_contiguous() {
let mb = MessageBox::new(BOUNDS, "T", "M", &["OK"]);
let header = mb.header_rect();
let content = mb.content_rect();
let footer = MessageBox::footer_rect_for(mb.bounds);
assert_eq!(header.y, BOUNDS.y);
assert_eq!(header.y + header.height, content.y);
assert_eq!(content.y + content.height, footer.y);
assert_eq!(footer.y + footer.height, BOUNDS.y + BOUNDS.height);
}
}