use crate::buffer::OptimizedBuffer;
use crate::cell::{Cell, CellContent};
use crate::color::Rgba;
use crate::grapheme_pool::GraphemePool;
use crate::style::Style;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Debug)]
pub struct BoxStyle {
pub top_left: char,
pub top_right: char,
pub bottom_left: char,
pub bottom_right: char,
pub horizontal: char,
pub vertical: char,
pub style: Style,
}
#[derive(Clone, Copy, Debug)]
pub struct BoxSides {
pub top: bool,
pub right: bool,
pub bottom: bool,
pub left: bool,
}
impl Default for BoxSides {
fn default() -> Self {
Self {
top: true,
right: true,
bottom: true,
left: true,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub enum TitleAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Clone, Debug)]
pub struct BoxOptions {
pub style: BoxStyle,
pub sides: BoxSides,
pub fill: Option<Rgba>,
pub title: Option<String>,
pub title_align: TitleAlign,
}
impl BoxOptions {
#[must_use]
pub fn new(style: BoxStyle) -> Self {
Self {
style,
sides: BoxSides::default(),
fill: None,
title: None,
title_align: TitleAlign::Left,
}
}
}
impl BoxStyle {
#[must_use]
pub fn single(style: Style) -> Self {
Self {
top_left: '┌',
top_right: '┐',
bottom_left: '└',
bottom_right: '┘',
horizontal: '─',
vertical: '│',
style,
}
}
#[must_use]
pub fn double(style: Style) -> Self {
Self {
top_left: '╔',
top_right: '╗',
bottom_left: '╚',
bottom_right: '╝',
horizontal: '═',
vertical: '║',
style,
}
}
#[must_use]
pub fn rounded(style: Style) -> Self {
Self {
top_left: '╭',
top_right: '╮',
bottom_left: '╰',
bottom_right: '╯',
horizontal: '─',
vertical: '│',
style,
}
}
#[must_use]
pub fn heavy(style: Style) -> Self {
Self {
top_left: '┏',
top_right: '┓',
bottom_left: '┗',
bottom_right: '┛',
horizontal: '━',
vertical: '┃',
style,
}
}
#[must_use]
pub fn ascii(style: Style) -> Self {
Self {
top_left: '+',
top_right: '+',
bottom_left: '+',
bottom_right: '+',
horizontal: '-',
vertical: '|',
style,
}
}
}
impl Default for BoxStyle {
fn default() -> Self {
Self::single(Style::NONE)
}
}
pub fn draw_text(buffer: &mut OptimizedBuffer, x: u32, y: u32, text: &str, style: Style) {
let mut col = x;
let mut row = y;
let fg = style.fg.unwrap_or(Rgba::WHITE);
let bg = style.bg.unwrap_or(Rgba::TRANSPARENT);
let attrs = style.attributes;
if text.is_ascii() {
for &byte in text.as_bytes() {
if byte == b'\n' {
row += 1;
col = x;
continue;
}
if byte == b'\r' {
col = x;
continue;
}
let ch = byte as char;
let width = u32::from((' '..='~').contains(&ch));
let cell = Cell {
content: CellContent::Char(ch),
fg,
bg,
attributes: attrs,
};
buffer.set_blended(col, row, cell);
col += width;
}
return;
}
for grapheme in text.graphemes(true) {
if grapheme == "\n" {
row += 1;
col = x;
continue;
}
if grapheme == "\r" {
col = x;
continue;
}
let cell = Cell::from_grapheme(grapheme, style);
let width = cell.display_width();
buffer.set_blended(col, row, cell);
for i in 1..width {
buffer.set_blended(col + i as u32, row, Cell::continuation(bg));
}
col += width as u32;
}
}
pub fn draw_text_with_pool(
buffer: &mut OptimizedBuffer,
pool: &mut GraphemePool,
x: u32,
y: u32,
text: &str,
style: Style,
) {
let mut col = x;
let mut row = y;
let fg = style.fg.unwrap_or(Rgba::WHITE);
let bg = style.bg.unwrap_or(Rgba::TRANSPARENT);
let attrs = style.attributes;
for grapheme in text.graphemes(true) {
if grapheme == "\n" {
row += 1;
col = x;
continue;
}
if grapheme == "\r" {
col = x;
continue;
}
let (content, width) = if grapheme.len() == 1 {
let ch = grapheme.as_bytes()[0] as char;
let w = usize::from((' '..='~').contains(&ch));
(CellContent::Char(ch), w)
} else if grapheme.chars().count() == 1 {
let ch = grapheme
.chars()
.next()
.expect("chars().count() == 1 but next() returned None");
let w = crate::unicode::display_width_char(ch);
(CellContent::Char(ch), w)
} else {
let id = pool.intern(grapheme);
(CellContent::Grapheme(id), id.width())
};
let cell = Cell {
content,
fg,
bg,
attributes: attrs,
};
buffer.set_blended_with_pool(pool, col, row, cell);
for i in 1..width {
buffer.set_blended_with_pool(pool, col + i as u32, row, Cell::continuation(bg));
}
col += width as u32;
}
}
pub fn draw_char_with_pool(
buffer: &mut OptimizedBuffer,
pool: &mut GraphemePool,
x: u32,
y: u32,
grapheme: &str,
style: Style,
) {
let fg = style.fg.unwrap_or(Rgba::WHITE);
let bg = style.bg.unwrap_or(Rgba::TRANSPARENT);
let attrs = style.attributes;
let (content, width) = if grapheme.len() == 1 {
let ch = grapheme.as_bytes()[0] as char;
let w = usize::from((' '..='~').contains(&ch));
(CellContent::Char(ch), w)
} else if grapheme.chars().count() == 1 {
let ch = grapheme
.chars()
.next()
.expect("chars().count() == 1 but next() returned None");
let w = crate::unicode::display_width_char(ch);
(CellContent::Char(ch), w)
} else {
let id = pool.intern(grapheme);
(CellContent::Grapheme(id), id.width())
};
let cell = Cell {
content,
fg,
bg,
attributes: attrs,
};
buffer.set_blended_with_pool(pool, x, y, cell);
for i in 1..width {
buffer.set_blended_with_pool(pool, x + i as u32, y, Cell::continuation(bg));
}
}
pub fn draw_box(buffer: &mut OptimizedBuffer, x: u32, y: u32, w: u32, h: u32, box_style: BoxStyle) {
if w < 2 || h < 2 {
return;
}
let style = box_style.style;
buffer.set_blended(x, y, Cell::new(box_style.top_left, style));
buffer.set_blended(x + w - 1, y, Cell::new(box_style.top_right, style));
buffer.set_blended(x, y + h - 1, Cell::new(box_style.bottom_left, style));
buffer.set_blended(
x + w - 1,
y + h - 1,
Cell::new(box_style.bottom_right, style),
);
for col in (x + 1)..(x + w - 1) {
buffer.set_blended(col, y, Cell::new(box_style.horizontal, style));
buffer.set_blended(col, y + h - 1, Cell::new(box_style.horizontal, style));
}
for row in (y + 1)..(y + h - 1) {
buffer.set_blended(x, row, Cell::new(box_style.vertical, style));
buffer.set_blended(x + w - 1, row, Cell::new(box_style.vertical, style));
}
}
pub fn draw_box_with_options(
buffer: &mut OptimizedBuffer,
x: u32,
y: u32,
w: u32,
h: u32,
options: BoxOptions,
) {
if w < 2 || h < 2 {
return;
}
let style = options.style.style;
if let Some(bg) = options.fill {
if w > 2 && h > 2 {
buffer.fill_rect(x + 1, y + 1, w - 2, h - 2, bg);
}
}
if options.sides.top && options.sides.left {
buffer.set_blended(x, y, Cell::new(options.style.top_left, style));
}
if options.sides.top && options.sides.right {
buffer.set_blended(x + w - 1, y, Cell::new(options.style.top_right, style));
}
if options.sides.bottom && options.sides.left {
buffer.set_blended(x, y + h - 1, Cell::new(options.style.bottom_left, style));
}
if options.sides.bottom && options.sides.right {
buffer.set_blended(
x + w - 1,
y + h - 1,
Cell::new(options.style.bottom_right, style),
);
}
if options.sides.top {
for col in (x + 1)..(x + w - 1) {
buffer.set_blended(col, y, Cell::new(options.style.horizontal, style));
}
}
if options.sides.bottom {
for col in (x + 1)..(x + w - 1) {
buffer.set_blended(col, y + h - 1, Cell::new(options.style.horizontal, style));
}
}
if options.sides.left {
for row in (y + 1)..(y + h - 1) {
buffer.set_blended(x, row, Cell::new(options.style.vertical, style));
}
}
if options.sides.right {
for row in (y + 1)..(y + h - 1) {
buffer.set_blended(x + w - 1, row, Cell::new(options.style.vertical, style));
}
}
if let Some(title) = options.title {
if options.sides.top && w > 2 {
let title_width = crate::unicode::display_width(&title) as i32;
let box_width = w as i32;
let min_title_space = 4;
if title_width > 0 && box_width >= title_width + min_title_space {
let padding = 2;
let start_x = x as i32;
let end_x = start_x + box_width - 1;
let mut title_x = match options.title_align {
TitleAlign::Left => start_x + padding,
TitleAlign::Center => {
let centered = (box_width - title_width) / 2;
start_x + padding.max(centered)
}
TitleAlign::Right => start_x + box_width - padding - title_width,
};
let min_x = start_x + padding;
let max_x = end_x - padding - title_width + 1;
title_x = title_x.clamp(min_x, max_x);
buffer.draw_text(title_x as u32, y, &title, style);
}
}
}
}
pub fn draw_hline(buffer: &mut OptimizedBuffer, x: u32, y: u32, len: u32, ch: char, style: Style) {
for col in x..x.saturating_add(len) {
buffer.set_blended(col, y, Cell::new(ch, style));
}
}
pub fn draw_vline(buffer: &mut OptimizedBuffer, x: u32, y: u32, len: u32, ch: char, style: Style) {
for row in y..y.saturating_add(len) {
buffer.set_blended(x, row, Cell::new(ch, style));
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_draw_text() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 0, 0, "Hello", Style::fg(Rgba::RED));
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('H')
);
assert_eq!(
buffer.get(4, 0).unwrap().content,
crate::cell::CellContent::Char('o')
);
}
#[test]
fn test_draw_text_with_newline() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 0, 0, "Hello\nWorld", Style::NONE);
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('H')
);
assert_eq!(
buffer.get(4, 0).unwrap().content,
crate::cell::CellContent::Char('o')
);
assert_eq!(
buffer.get(0, 1).unwrap().content,
crate::cell::CellContent::Char('W')
);
assert_eq!(
buffer.get(4, 1).unwrap().content,
crate::cell::CellContent::Char('d')
);
}
#[test]
fn test_draw_text_with_carriage_return() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 0, 0, "XXXXX\rHello", Style::NONE);
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('H')
);
assert_eq!(
buffer.get(4, 0).unwrap().content,
crate::cell::CellContent::Char('o')
);
}
#[test]
fn test_draw_text_with_crlf() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 0, 0, "Line1\r\nLine2", Style::NONE);
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('L')
);
assert_eq!(
buffer.get(0, 1).unwrap().content,
crate::cell::CellContent::Char('L')
);
assert_eq!(
buffer.get(4, 1).unwrap().content,
crate::cell::CellContent::Char('2')
);
}
#[test]
fn test_draw_text_multiline_with_offset() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 5, 2, "A\nB\nC", Style::NONE);
assert_eq!(
buffer.get(5, 2).unwrap().content,
crate::cell::CellContent::Char('A')
);
assert_eq!(
buffer.get(5, 3).unwrap().content,
crate::cell::CellContent::Char('B')
);
assert_eq!(
buffer.get(5, 4).unwrap().content,
crate::cell::CellContent::Char('C')
);
}
#[test]
fn test_draw_text_with_pool_multiline() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buffer, &mut pool, 0, 0, "Line1\nLine2", Style::NONE);
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('L')
);
assert_eq!(
buffer.get(0, 1).unwrap().content,
crate::cell::CellContent::Char('L')
);
assert_eq!(
buffer.get(4, 1).unwrap().content,
crate::cell::CellContent::Char('2')
);
}
#[test]
fn test_draw_wide_char() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_text(&mut buffer, 0, 0, "漢字", Style::NONE);
assert!(!buffer.get(0, 0).unwrap().is_continuation());
assert!(buffer.get(1, 0).unwrap().is_continuation());
assert!(!buffer.get(2, 0).unwrap().is_continuation());
assert!(buffer.get(3, 0).unwrap().is_continuation());
}
#[test]
fn test_draw_box() {
let mut buffer = OptimizedBuffer::new(80, 24);
draw_box(&mut buffer, 0, 0, 10, 5, BoxStyle::single(Style::NONE));
assert_eq!(
buffer.get(0, 0).unwrap().content,
crate::cell::CellContent::Char('┌')
);
assert_eq!(
buffer.get(9, 0).unwrap().content,
crate::cell::CellContent::Char('┐')
);
assert_eq!(
buffer.get(0, 4).unwrap().content,
crate::cell::CellContent::Char('└')
);
assert_eq!(
buffer.get(9, 4).unwrap().content,
crate::cell::CellContent::Char('┘')
);
}
#[test]
fn test_draw_box_with_options_title() {
let mut buffer = OptimizedBuffer::new(20, 5);
let options = BoxOptions {
style: BoxStyle::single(Style::NONE),
sides: BoxSides::default(),
fill: None,
title: Some("Title".to_string()),
title_align: TitleAlign::Left,
};
draw_box_with_options(&mut buffer, 0, 0, 10, 4, options);
assert_eq!(
buffer.get(1, 0).unwrap().content,
crate::cell::CellContent::Char('─')
);
assert_eq!(
buffer.get(2, 0).unwrap().content,
crate::cell::CellContent::Char('T')
);
}
#[test]
fn test_draw_text_with_pool_ascii() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buffer, &mut pool, 0, 0, "Hello", Style::fg(Rgba::RED));
assert_eq!(buffer.get(0, 0).unwrap().content, CellContent::Char('H'));
assert_eq!(buffer.get(4, 0).unwrap().content, CellContent::Char('o'));
assert_eq!(pool.active_count(), 0);
}
#[test]
fn test_draw_text_with_pool_emoji() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buffer, &mut pool, 0, 0, "Hi 👨👩👧!", Style::NONE);
assert!(matches!(
buffer.get(0, 0).unwrap().content,
CellContent::Char('H')
));
assert!(matches!(
buffer.get(1, 0).unwrap().content,
CellContent::Char('i')
));
assert!(matches!(
buffer.get(2, 0).unwrap().content,
CellContent::Char(' ')
));
let emoji_cell = buffer.get(3, 0).unwrap();
assert!(matches!(emoji_cell.content, CellContent::Grapheme(_)));
assert_eq!(emoji_cell.display_width(), 2);
assert!(buffer.get(4, 0).unwrap().is_continuation());
assert!(matches!(
buffer.get(5, 0).unwrap().content,
CellContent::Char('!')
));
assert_eq!(pool.active_count(), 1);
if let CellContent::Grapheme(id) = emoji_cell.content {
assert_eq!(pool.get(id), Some("👨👩👧"));
}
}
#[test]
fn test_draw_text_with_pool_single_codepoint_emoji() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buffer, &mut pool, 0, 0, "👍", Style::NONE);
let cell = buffer.get(0, 0).unwrap();
assert!(matches!(cell.content, CellContent::Char('👍')));
assert_eq!(cell.display_width(), 2);
assert!(buffer.get(1, 0).unwrap().is_continuation());
assert_eq!(pool.active_count(), 0);
}
#[test]
fn test_draw_text_with_pool_deduplication() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buffer, &mut pool, 0, 0, "👨👩👧👨👩👧", Style::NONE);
assert_eq!(pool.active_count(), 1);
let cell1 = buffer.get(0, 0).unwrap();
let cell2 = buffer.get(2, 0).unwrap();
match (cell1.content, cell2.content) {
(CellContent::Grapheme(id1), CellContent::Grapheme(id2)) => {
assert_eq!(id1, id2);
assert_eq!(pool.refcount(id1), 2);
}
other => {
assert!(
matches!(other, (CellContent::Grapheme(_), CellContent::Grapheme(_))),
"Expected grapheme content for pooled multi-codepoint cells"
);
}
}
}
#[test]
fn test_draw_char_with_pool() {
let mut buffer = OptimizedBuffer::new(80, 24);
let mut pool = GraphemePool::new();
draw_char_with_pool(&mut buffer, &mut pool, 0, 0, "A", Style::NONE);
assert!(matches!(
buffer.get(0, 0).unwrap().content,
CellContent::Char('A')
));
draw_char_with_pool(&mut buffer, &mut pool, 5, 0, "👨👩👧", Style::NONE);
let cell = buffer.get(5, 0).unwrap();
assert!(cell.content.is_grapheme());
assert_eq!(cell.display_width(), 2);
assert!(buffer.get(6, 0).unwrap().is_continuation());
if let CellContent::Grapheme(id) = cell.content {
assert_eq!(pool.get(id), Some("👨👩👧"));
}
}
#[test]
fn test_draw_text_consistency() {
let mut buf1 = OptimizedBuffer::new(10, 1);
draw_text(&mut buf1, 0, 0, "A\tB", Style::NONE);
let mut buf2 = OptimizedBuffer::new(10, 1);
let mut pool = GraphemePool::new();
draw_text_with_pool(&mut buf2, &mut pool, 0, 0, "A\tB", Style::NONE);
assert_eq!(buf1.get(0, 0).unwrap().content, CellContent::Char('A'));
assert_eq!(buf2.get(0, 0).unwrap().content, CellContent::Char('A'));
assert_eq!(buf1.get(1, 0).unwrap().content, CellContent::Char('B'));
assert_eq!(buf2.get(1, 0).unwrap().content, CellContent::Char('B'));
}
}