use retroglyph_core::color::{Color, Style};
use retroglyph_core::grid::Rect;
use super::panel::PanelChrome;
use super::{BorderType, Panel, PanelTitle, Widget};
use crate::Surface;
use crate::align::Align;
use crate::layout::centered_rect;
use crate::style::Sides;
use crate::theme::Theme;
#[derive(Clone, Copy, Debug)]
pub struct Modal<'a> {
width: u16,
height: u16,
chrome: PanelChrome<'a>,
titles: &'a [PanelTitle<'a>],
}
impl<'a> Modal<'a> {
#[must_use]
pub fn new(width: u16, height: u16) -> Self {
Self {
width,
height,
chrome: PanelChrome::default().title_align(Align::Center),
titles: &[],
}
.theme(Theme::DARK)
}
#[must_use]
pub const fn title(mut self, title: &'a str) -> Self {
self.chrome = self.chrome.title(title);
self
}
#[must_use]
pub const fn title_align(mut self, align: Align) -> Self {
self.chrome = self.chrome.title_align(align);
self
}
#[must_use]
pub const fn titles(mut self, titles: &'a [PanelTitle<'a>]) -> Self {
self.titles = titles;
self
}
#[must_use]
pub const fn border_style(mut self, style: Style) -> Self {
self.chrome = self.chrome.border_style(style);
self
}
#[must_use]
pub const fn fill_style(mut self, style: Style) -> Self {
self.chrome = self.chrome.fill_style(style);
self
}
#[must_use]
pub const fn border_type(mut self, border_type: BorderType) -> Self {
self.chrome = self.chrome.border_type(border_type);
self
}
#[must_use]
pub const fn padding(mut self, padding: Sides) -> Self {
self.chrome = self.chrome.padding(padding);
self
}
#[must_use]
pub fn theme(mut self, theme: Theme) -> Self {
self.chrome = self.chrome.theme(theme);
self
}
#[must_use]
pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
self.chrome = self.chrome.theme_on(theme, bg);
self
}
pub fn render(self, screen: Rect, surface: &mut Surface<'_>) -> Rect {
let rect = centered_rect(screen, self.width, self.height);
let mut panel = Panel::from_chrome(self.chrome);
for title in self.titles {
let (text, position, align) = title.parts();
panel = panel.add_title(text, position, align);
}
panel.render(&mut surface.scope(rect));
panel.inner(rect)
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use retroglyph_core::grid::{Grid, Pos};
use super::*;
use crate::widget::TitlePosition;
#[test]
fn modal_is_copy() {
const fn assert_copy<T: Copy>() {}
assert_copy::<Modal<'_>>();
}
#[test]
fn centers_the_box_and_returns_the_inner_content_rect() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
let inner = Modal::new(10, 4).render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(inner, Rect::new(6, 4, 8, 2));
assert_eq!(grid[Pos::new(5, 3)].glyph(), '┌');
assert_eq!(grid[Pos::new(14, 3)].glyph(), '┐');
}
#[test]
fn padding_shrinks_the_returned_inner_rect() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
let inner = Modal::new(10, 4)
.padding(Sides::symmetric(0, 1))
.render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(inner, Rect::new(7, 4, 6, 2));
}
#[test]
fn draws_only_the_box_leaving_the_rest_of_the_screen_untouched() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
Modal::new(10, 4).render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn theme_maps_named_roles_onto_border_and_fill() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
Modal::new(10, 4)
.theme(Theme::DARK)
.render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(
grid[Pos::new(5, 3)].style().foreground(),
Theme::DARK.border
);
assert_eq!(
grid[Pos::new(5, 3)].style().background(),
Theme::DARK.title_bg
);
assert_eq!(
grid[Pos::new(6, 4)].style().background(),
Theme::DARK.panel_bg
);
}
#[test]
fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
Modal::new(10, 4)
.theme_on(Theme::DARK, Color::Default)
.render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(
grid[Pos::new(5, 3)].style().foreground(),
Theme::DARK.border
);
assert_eq!(
grid[Pos::new(5, 3)].style().background(),
Theme::DARK.title_bg
);
assert_eq!(grid[Pos::new(6, 4)].style().background(), Color::Default);
}
#[test]
fn titles_draws_into_the_bottom_border_row() {
let screen = Rect::new(0, 0, 12, 8);
let mut grid = Grid::new(12, 8);
Modal::new(12, 4)
.titles(&[PanelTitle::new(
"hint",
TitlePosition::Bottom,
Align::Center,
)])
.render(screen, &mut Surface::new(&mut grid, screen, 0));
let bottom_row: String = (0..12).map(|x| grid[Pos::new(x, 5)].glyph()).collect();
assert!(bottom_row.contains("hint"));
}
#[test]
fn title_and_titles_coexist_on_different_edges() {
let screen = Rect::new(0, 0, 20, 8);
let mut grid = Grid::new(20, 8);
Modal::new(20, 4)
.title("Confirm")
.titles(&[PanelTitle::new(
"3 / 10",
TitlePosition::Bottom,
Align::Right,
)])
.render(screen, &mut Surface::new(&mut grid, screen, 0));
let top_row: String = (0..20).map(|x| grid[Pos::new(x, 2)].glyph()).collect();
let bottom_row: String = (0..20).map(|x| grid[Pos::new(x, 5)].glyph()).collect();
assert!(top_row.contains("Confirm"));
assert!(bottom_row.contains("3 / 10"));
}
#[test]
fn a_later_titles_call_replaces_the_earlier_one() {
let screen = Rect::new(0, 0, 12, 8);
let mut grid = Grid::new(12, 8);
Modal::new(12, 4)
.titles(&[PanelTitle::new(
"first",
TitlePosition::Bottom,
Align::Center,
)])
.titles(&[PanelTitle::new(
"second",
TitlePosition::Bottom,
Align::Center,
)])
.render(screen, &mut Surface::new(&mut grid, screen, 0));
let bottom_row: String = (0..12).map(|x| grid[Pos::new(x, 5)].glyph()).collect();
assert!(bottom_row.contains("second"));
assert!(!bottom_row.contains("first"));
}
#[test]
fn border_type_selects_the_glyph_set() {
let screen = Rect::new(0, 0, 20, 10);
let mut grid = Grid::new(20, 10);
Modal::new(10, 4)
.border_type(BorderType::Thick)
.render(screen, &mut Surface::new(&mut grid, screen, 0));
assert_eq!(grid[Pos::new(5, 3)].glyph(), '┏');
assert_eq!(grid[Pos::new(14, 3)].glyph(), '┓');
}
}