use alloc::vec::Vec;
use retroglyph_core::color::Style;
use retroglyph_core::grid::Grid;
use retroglyph_core::text::{char_width, width_usize as measured_width};
use retroglyph_core::tile::Tile;
#[cfg(feature = "egc")]
use retroglyph_core::grid::{HasSize, Rect};
use crate::Surface;
use crate::text::truncate;
use crate::widget::Widget;
use retroglyph_core::symbols::border::PLAIN;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Sides {
pub top: u16,
pub right: u16,
pub bottom: u16,
pub left: u16,
}
impl Sides {
pub const ZERO: Self = Self {
top: 0,
right: 0,
bottom: 0,
left: 0,
};
#[must_use]
pub const fn all(n: u16) -> Self {
Self {
top: n,
right: n,
bottom: n,
left: n,
}
}
#[must_use]
pub const fn symmetric(vertical: u16, horizontal: u16) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
#[must_use]
pub const fn top(mut self, top: u16) -> Self {
self.top = top;
self
}
#[must_use]
pub const fn right(mut self, right: u16) -> Self {
self.right = right;
self
}
#[must_use]
pub const fn bottom(mut self, bottom: u16) -> Self {
self.bottom = bottom;
self
}
#[must_use]
pub const fn left(mut self, left: u16) -> Self {
self.left = left;
self
}
const fn horizontal(self) -> u16 {
self.left.saturating_add(self.right)
}
const fn vertical(self) -> u16 {
self.top.saturating_add(self.bottom)
}
}
#[derive(Clone, Copy, Debug)]
pub struct BoxStyle {
style: Style,
padding: Sides,
margin: Sides,
border: bool,
width: Option<u16>,
height: Option<u16>,
}
impl BoxStyle {
#[must_use]
pub const fn new(style: Style) -> Self {
Self {
style,
padding: Sides::ZERO,
margin: Sides::ZERO,
border: false,
width: None,
height: None,
}
}
#[must_use]
pub const fn padding(mut self, padding: Sides) -> Self {
self.padding = padding;
self
}
#[must_use]
pub const fn margin(mut self, margin: Sides) -> Self {
self.margin = margin;
self
}
#[must_use]
pub const fn border(mut self, border: bool) -> Self {
self.border = border;
self
}
#[must_use]
pub const fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
#[must_use]
pub const fn height(mut self, height: u16) -> Self {
self.height = Some(height);
self
}
#[must_use]
pub fn render(&self, text: &str) -> Grid {
let lines: Vec<&str> = text.split('\n').collect();
let content_w = self.width.unwrap_or_else(|| {
u16::try_from(lines.iter().map(|l| measured_width(l)).max().unwrap_or(0))
.unwrap_or(u16::MAX)
});
let content_h = self
.height
.unwrap_or_else(|| u16::try_from(lines.len()).unwrap_or(u16::MAX));
let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
for (row, line) in lines.iter().take(usize::from(content_h)).enumerate() {
let Ok(row) = u16::try_from(row) else { break };
let clipped = truncate(line, content_w);
let mut col = 0u16;
for ch in clipped.chars() {
let w = char_width(ch);
if col.saturating_add(w) > content_w {
break;
}
grid.put_tile(
0,
(content_x.saturating_add(col), content_y.saturating_add(row)),
Tile::new(ch, self.style),
);
col = col.saturating_add(w);
}
}
grid
}
#[cfg(feature = "egc")]
#[must_use]
pub fn render_wrapped(&self, text: &str) -> Grid {
use retroglyph_core::layout::TextLayout;
use retroglyph_core::text::{Line, Span};
let content_w = self.width.unwrap_or_else(|| {
u16::try_from(text.split('\n').map(measured_width).max().unwrap_or(0))
.unwrap_or(u16::MAX)
});
let line = Line::from(Span::styled(text, self.style));
let content_h = self.height.unwrap_or_else(|| {
TextLayout::new(&line)
.rect(Rect::new(0, 0, content_w, u16::MAX))
.measure()
.height()
});
let (mut grid, content_x, content_y) = self.scaffold(content_w, content_h);
TextLayout::new(&line)
.rect(Rect::new(content_x, content_y, content_w, content_h))
.render_to_grid(&mut grid, 0);
grid
}
fn scaffold(&self, content_w: u16, content_h: u16) -> (Grid, u16, u16) {
let border_wh = u16::from(self.border) * 2;
let inner_w = content_w
.saturating_add(self.padding.horizontal())
.saturating_add(border_wh);
let inner_h = content_h
.saturating_add(self.padding.vertical())
.saturating_add(border_wh);
let outer_w = inner_w.saturating_add(self.margin.horizontal()).max(1);
let outer_h = inner_h.saturating_add(self.margin.vertical()).max(1);
let mut grid = Grid::new(outer_w, outer_h);
let box_x = self.margin.left;
let box_y = self.margin.top;
fill_rect(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
if self.border {
draw_border(&mut grid, box_x, box_y, inner_w, inner_h, self.style);
}
let content_x = box_x
.saturating_add(u16::from(self.border))
.saturating_add(self.padding.left);
let content_y = box_y
.saturating_add(u16::from(self.border))
.saturating_add(self.padding.top);
(grid, content_x, content_y)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Boxed<'a> {
style: BoxStyle,
text: &'a str,
}
impl BoxStyle {
#[must_use]
pub const fn text(self, text: &str) -> Boxed<'_> {
Boxed { style: self, text }
}
}
impl Widget for Boxed<'_> {
fn render(&self, surface: &mut Surface<'_>) {
let grid = self.style.render(self.text);
surface.blit(&grid, 0, 0);
}
}
fn fill_rect(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
for dy in 0..h {
for dx in 0..w {
grid.put_tile(0, (x + dx, y + dy), Tile::new(' ', style));
}
}
}
fn draw_border(grid: &mut Grid, x: u16, y: u16, w: u16, h: u16, style: Style) {
let right = x + w - 1;
let bottom = y + h - 1;
grid.put_tile(0, (x, y), Tile::new(PLAIN.top_left, style));
grid.put_tile(0, (right, y), Tile::new(PLAIN.top_right, style));
grid.put_tile(0, (x, bottom), Tile::new(PLAIN.bottom_left, style));
grid.put_tile(0, (right, bottom), Tile::new(PLAIN.bottom_right, style));
for cx in (x + 1)..right {
grid.put_tile(0, (cx, y), Tile::new(PLAIN.horizontal, style));
grid.put_tile(0, (cx, bottom), Tile::new(PLAIN.horizontal, style));
}
for cy in (y + 1)..bottom {
grid.put_tile(0, (x, cy), Tile::new(PLAIN.vertical, style));
grid.put_tile(0, (right, cy), Tile::new(PLAIN.vertical, style));
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use super::*;
use retroglyph_core::grid::{Pos, Rect};
fn glyphs(grid: &Grid) -> Vec<String> {
(0..grid.height())
.map(|y| {
(0..grid.width())
.map(|x| grid[Pos::new(x, y)].glyph())
.collect()
})
.collect()
}
#[test]
fn boxed_render_draws_on_a_surface_that_is_not_on_layer_zero() {
use retroglyph_core::surface::{Layer, Surface};
let mut grid = Grid::new(6, 3);
let mut surface = Surface::new(&mut grid, Rect::new(0, 0, 6, 3), Layer::World.as_u8());
let boxed = BoxStyle::new(Style::default()).text("hi");
boxed.render(&mut surface.on_tier(Layer::Overlay));
assert_eq!(
grid.tile(Layer::Overlay.as_u8(), (0, 0)).map(Tile::glyph),
Some('h')
);
assert_eq!(
grid.tile(Layer::Overlay.as_u8(), (1, 0)).map(Tile::glyph),
Some('i')
);
}
#[test]
fn sides_helpers() {
assert_eq!(
Sides::all(2),
Sides {
top: 2,
right: 2,
bottom: 2,
left: 2
}
);
assert_eq!(
Sides::symmetric(1, 3),
Sides {
top: 1,
right: 3,
bottom: 1,
left: 3
}
);
}
#[test]
fn sizes_to_content_with_no_padding_or_border() {
let grid = BoxStyle::new(Style::default()).render("hi");
assert_eq!((grid.width(), grid.height()), (2, 1));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
}
#[test]
fn sizes_to_the_widest_of_multiple_lines() {
let grid = BoxStyle::new(Style::default()).render("a\nbcd\nef");
assert_eq!((grid.width(), grid.height()), (3, 3));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
assert_eq!(grid[Pos::new(1, 0)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'b');
assert_eq!(grid[Pos::new(2, 1)].glyph(), 'd');
}
#[test]
fn explicit_width_clips_longer_lines_and_pads_shorter_ones() {
let grid = BoxStyle::new(Style::default()).width(3).render("hello");
assert_eq!(grid.width(), 3);
let row: String = (0..3).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
assert_eq!(row, "hel");
}
#[test]
fn explicit_height_drops_extra_lines() {
let grid = BoxStyle::new(Style::default()).height(1).render("a\nb\nc");
assert_eq!(grid.height(), 1);
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
}
#[test]
fn padding_surrounds_content_with_the_box_style() {
let grid = BoxStyle::new(Style::default())
.padding(Sides::all(1))
.render("x");
assert_eq!((grid.width(), grid.height()), (3, 3));
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn render_does_not_overflow_on_a_near_u16_max_line_with_padding() {
let text = "a".repeat(65_535);
let grid = BoxStyle::new(Style::default())
.padding(Sides::all(2))
.render(&text);
assert_eq!(grid[Pos::new(2, 2)].glyph(), 'a');
}
#[test]
fn border_draws_a_box_around_padding_and_content() {
let grid = BoxStyle::new(Style::default()).border(true).render("x");
assert_eq!((grid.width(), grid.height()), (3, 3));
let rows = glyphs(&grid);
assert_eq!(rows[0], "┌─┐");
assert_eq!(rows[1], "│x│");
assert_eq!(rows[2], "└─┘");
}
#[test]
fn margin_is_left_transparent_outside_the_border() {
let grid = BoxStyle::new(Style::default())
.margin(Sides::all(1))
.render("x");
assert_eq!((grid.width(), grid.height()), (3, 3));
assert!(grid[Pos::new(0, 0)].is_empty());
assert_eq!(grid[Pos::new(1, 1)].glyph(), 'x');
}
#[test]
fn wide_characters_push_later_columns_over_by_their_width() {
use retroglyph_core::tile::TileFlags;
let grid = BoxStyle::new(Style::default()).render("aあb");
assert_eq!(grid.width(), 4);
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
assert_eq!(grid[Pos::new(1, 0)].glyph(), 'あ');
assert!(
grid[Pos::new(2, 0)]
.flags()
.contains(TileFlags::WIDE_CHAR_SPACER)
);
assert_eq!(grid[Pos::new(3, 0)].glyph(), 'b');
}
#[test]
fn control_characters_occupy_one_column_matching_core_text_char_width() {
let grid = BoxStyle::new(Style::default()).render("a\u{7}b");
assert_eq!(grid.width(), 3);
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'a');
assert_eq!(grid[Pos::new(2, 0)].glyph(), 'b');
}
#[test]
fn border_with_empty_content_is_still_at_least_a_2x2_box() {
let grid = BoxStyle::new(Style::default()).border(true).render("");
assert_eq!((grid.width(), grid.height()), (2, 3));
let rows = glyphs(&grid);
assert_eq!(rows[0], "┌┐");
assert_eq!(rows[2], "└┘");
}
#[test]
#[cfg(feature = "egc")]
fn render_wrapped_word_wraps_to_the_explicit_width() {
let grid = BoxStyle::new(Style::default())
.width(10)
.render_wrapped("the quick brown fox jumps");
assert_eq!(grid.width(), 10);
let rows = glyphs(&grid);
assert_eq!(rows[0].trim_end(), "the quick");
assert_eq!(rows[1].trim_end(), "brown fox");
assert_eq!(rows[2].trim_end(), "jumps");
}
#[test]
#[cfg(feature = "egc")]
fn render_wrapped_without_an_explicit_width_measures_but_does_not_wrap() {
let grid = BoxStyle::new(Style::default()).render_wrapped("hi");
assert_eq!((grid.width(), grid.height()), (2, 1));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h');
assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
}
#[test]
#[cfg(feature = "egc")]
fn render_wrapped_respects_padding_and_border_like_render() {
let grid = BoxStyle::new(Style::default())
.border(true)
.padding(Sides::all(1))
.width(3)
.render_wrapped("hi");
assert_eq!((grid.width(), grid.height()), (7, 5));
assert_eq!(grid[Pos::new(2, 2)].glyph(), 'h');
assert_eq!(grid[Pos::new(3, 2)].glyph(), 'i');
}
#[test]
fn boxed_widget_places_the_box_at_the_areas_top_left() {
let styled = BoxStyle::new(Style::default()).border(true).text("hi");
let area = Rect::new(2, 1, 10, 6);
let mut grid = Grid::new(12, 7);
styled.render(&mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(2, 1)].glyph(), '┌');
assert_eq!(grid[Pos::new(3, 2)].glyph(), 'h');
assert_eq!(grid[Pos::new(4, 2)].glyph(), 'i');
assert_eq!(grid[Pos::new(5, 3)].glyph(), '┘');
}
}