use retroglyph_core::{Color, Rect, Style};
use unicode_width::UnicodeWidthStr;
use super::{BoxBorder, Widget};
use crate::Surface;
use crate::draw::fill_rect;
use crate::text::truncate as truncate_to_cols;
use crate::{Align, Theme};
#[derive(Clone, Copy, Debug, Default)]
pub struct Panel<'a> {
title: Option<&'a str>,
title_align: Align,
border_style: Style,
fill_style: Style,
}
impl<'a> Panel<'a> {
#[must_use]
pub fn new() -> Self {
Self {
title_align: Align::Center,
..Self::default()
}
}
#[must_use]
pub const fn title(mut self, title: &'a str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub const fn title_align(mut self, align: Align) -> Self {
self.title_align = align;
self
}
#[must_use]
pub const fn border_style(mut self, style: Style) -> Self {
self.border_style = style;
self
}
#[must_use]
pub const fn fill_style(mut self, style: Style) -> Self {
self.fill_style = style;
self
}
#[must_use]
pub fn theme(self, theme: Theme) -> Self {
self.theme_on(theme, theme.panel_bg)
}
#[must_use]
pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
self.border_style = Style::new().fg(theme.border).bg(theme.title_bg);
self.fill_style = Style::new().bg(bg);
self
}
}
impl Widget for Panel<'_> {
fn render(&self, area: Rect, surface: &mut Surface<'_>) {
if area.width() < 2 || area.height() < 2 {
return;
}
let inner = Rect::new(
area.left() + 1,
area.top() + 1,
area.width().saturating_sub(2),
area.height().saturating_sub(2),
);
fill_rect(surface, inner, ' ', self.fill_style);
BoxBorder::new()
.style(self.border_style)
.render(area, surface);
if let Some(t) = self.title {
let max_title_w = area.width().saturating_sub(4) as usize; if max_title_w == 0 {
return;
}
let t = truncate_to_cols(t, max_title_w);
#[allow(clippy::cast_possible_truncation)]
let t_w = t.width() as u16;
let padded = t_w + 2;
let title_x = area.left() + 1 + self.title_align.offset(area.width() - 2, padded);
let title_y = area.top();
surface.put((title_x, title_y), ' ', self.border_style);
surface.print((title_x + 1, title_y), t, self.border_style);
surface.put((title_x + 1 + t_w, title_y), ' ', self.border_style);
}
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::{Color, Grid, Pos};
use super::*;
#[test]
fn draws_border_fill_and_title() {
let area = Rect::new(0, 0, 10, 4);
let border = Style::new().fg(Color::WHITE);
let fill = Style::new();
let mut grid = Grid::new(10, 4);
Panel::new()
.border_style(border)
.fill_style(fill)
.title("hi")
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), '┌');
assert_eq!(grid[Pos::new(1, 1)].glyph(), ' '); let top_row: String = (0..10).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
assert!(top_row.contains("hi"));
}
#[test]
fn long_title_is_truncated_to_fit() {
let area = Rect::new(0, 0, 8, 3); let mut grid = Grid::new(8, 3);
Panel::new()
.title("a very long title")
.render(area, &mut Surface::new(&mut grid, area, 0));
let top_row: String = (0..8).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
assert!(!top_row.contains("a very long title"));
}
#[test]
fn theme_maps_named_roles_onto_border_and_fill() {
let area = Rect::new(0, 0, 10, 4);
let mut grid = Grid::new(10, 4);
Panel::new()
.theme(Theme::DARK)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(
grid[Pos::new(0, 0)].style().foreground(),
Theme::DARK.border
);
assert_eq!(
grid[Pos::new(0, 0)].style().background(),
Theme::DARK.title_bg
);
assert_eq!(
grid[Pos::new(1, 1)].style().background(),
Theme::DARK.panel_bg
);
}
#[test]
fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
let area = Rect::new(0, 0, 10, 4);
let mut grid = Grid::new(10, 4);
Panel::new()
.theme_on(Theme::DARK, Color::Default)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(
grid[Pos::new(0, 0)].style().foreground(),
Theme::DARK.border
);
assert_eq!(
grid[Pos::new(0, 0)].style().background(),
Theme::DARK.title_bg
);
assert_eq!(grid[Pos::new(1, 1)].style().background(), Color::Default);
}
#[test]
fn left_aligned_title_starts_after_the_corner() {
let area = Rect::new(0, 0, 12, 3);
let mut grid = Grid::new(12, 3);
Panel::new()
.title("hi")
.title_align(Align::Left)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(1, 0)].glyph(), ' ');
assert_eq!(grid[Pos::new(2, 0)].glyph(), 'h');
assert_eq!(grid[Pos::new(3, 0)].glyph(), 'i');
}
#[test]
fn right_aligned_title_ends_before_the_corner() {
let area = Rect::new(0, 0, 12, 3);
let mut grid = Grid::new(12, 3);
Panel::new()
.title("hi")
.title_align(Align::Right)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(8, 0)].glyph(), 'h');
assert_eq!(grid[Pos::new(9, 0)].glyph(), 'i');
assert_eq!(grid[Pos::new(10, 0)].glyph(), ' ');
}
#[test]
fn too_small_is_a_no_op() {
let area = Rect::new(0, 0, 1, 1);
let mut grid = Grid::new(1, 1);
Panel::new().render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn wide_char_title_is_centred_by_display_width_not_byte_length() {
let area = Rect::new(0, 0, 10, 3); let mut grid = Grid::new(10, 3);
Panel::new()
.title("あ")
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(3, 0)].glyph(), ' ');
assert_eq!(grid[Pos::new(4, 0)].glyph(), 'あ');
assert_eq!(grid[Pos::new(5, 0)].glyph(), ' ');
}
}