use unicode_segmentation::UnicodeSegmentation;
use crate::{Cell, Glyph, Pos, Shape, Size, Surface};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Text {
lines: Vec<Vec<Glyph>>,
}
impl Text {
#[must_use]
pub fn new(content: &str) -> Option<Self> {
let lines = content
.split('\n')
.map(|line| line.graphemes(true).map(Glyph::new).collect())
.collect::<Option<Vec<_>>>()?;
Some(Self { lines })
}
#[must_use]
pub fn lines(&self) -> &[Vec<Glyph>] {
&self.lines
}
#[must_use]
pub fn width(&self) -> u16 {
let longest = self.lines.iter().map(Vec::len).max().unwrap_or_default();
u16::try_from(longest).unwrap_or(u16::MAX)
}
#[must_use]
pub fn height(&self) -> u16 {
u16::try_from(self.lines.len()).unwrap_or(u16::MAX)
}
}
#[derive(Debug)]
pub struct TextShape {
pub at: Pos,
pub area: Size,
pub text: Text,
}
impl Shape for TextShape {
fn draw(&self, surface: &mut dyn Surface) {
let dy = (i32::from(self.area.height) - i32::from(self.text.height())) / 2;
for (row, line) in self.text.lines().iter().enumerate() {
let width = i32::try_from(line.len()).unwrap_or(i32::MAX);
let dx = (i32::from(self.area.width) - width) / 2;
let Some(y) = coordinate(self.at.y, dy, row) else {
continue;
};
for (column, glyph) in line.iter().enumerate() {
let Some(x) = coordinate(self.at.x, dx, column) else {
continue;
};
surface.stamp(Pos { x, y }, Cell::Literal(glyph.clone()));
}
}
}
}
fn coordinate(base: i16, offset: i32, step: usize) -> Option<i16> {
let step = i32::try_from(step).ok()?;
i16::try_from(i32::from(base).checked_add(offset)?.checked_add(step)?).ok()
}
#[cfg(test)]
mod tests {
use super::{Text, TextShape};
use crate::shape::counting::CountingSurface;
use crate::{Buffer, FigureLayer, GlyphCatalog, Pos, Shape, Size, StampMode, Window, render};
fn text(content: &str) -> Text {
Text::new(content).unwrap_or_else(|| panic!("{content:?} has no control character"))
}
fn light() -> GlyphCatalog {
GlyphCatalog::light()
}
fn origin() -> Pos {
Pos { x: 0, y: 0 }
}
fn size(width: u16, height: u16) -> Size {
Size { width, height }
}
#[test]
fn a_text_splits_on_newlines_and_refuses_other_control_characters() {
let text = Text::new("one\ntwo").expect("\"one\\ntwo\" is two lines");
assert_eq!(text.lines().len(), 2);
assert_eq!(text.lines()[0][0].as_str(), "o");
assert_eq!(text.lines()[1][2].as_str(), "o");
for refused in ["a\tb", "a\rb", "a\r\nb", "a\u{7}b"] {
assert!(
Text::new(refused).is_none(),
"{refused:?} holds a control character"
);
}
}
#[test]
fn an_empty_text_is_one_empty_line_and_a_trailing_newline_adds_one() {
for (content, lines, width, height) in [
("", 1, 0, 1),
("one", 1, 3, 1),
("one\n", 2, 3, 2),
("one\n\n", 3, 3, 3),
] {
let text = Text::new(content)
.unwrap_or_else(|| panic!("{content:?} has no control character"));
assert_eq!(text.lines().len(), lines, "{content:?}");
assert_eq!(text.width(), width, "{content:?}");
assert_eq!(text.height(), height, "{content:?}");
}
}
#[test]
fn a_text_shape_centres_each_line_in_its_area() {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(8, 5),
});
TextShape {
at: origin(),
area: size(8, 5),
text: text("one\ntwo"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
" \n one \n two \n \n \n"
);
}
#[test]
fn a_text_shape_writes_a_line_at_the_computed_position() {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(8, 5),
});
TextShape {
at: origin(),
area: size(8, 5),
text: text("hi"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
" \n \n hi \n \n \n"
);
}
#[test]
fn a_zero_area_places_the_block_around_the_position() {
let mut buffer = Buffer::new(Window {
origin: Pos { x: 8, y: 4 },
size: size(6, 3),
});
TextShape {
at: Pos { x: 10, y: 5 },
area: size(0, 0),
text: text("abc"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
" \n abc \n \n"
);
}
#[test]
fn a_text_shape_does_not_clip_to_its_area() {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(8, 3),
});
TextShape {
at: Pos { x: 5, y: 1 },
area: size(1, 1),
text: text("abc"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
" \n abc \n \n"
);
}
#[test]
fn an_empty_line_writes_nothing_and_a_short_line_writes_no_spaces() {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(4, 3),
});
TextShape {
at: origin(),
area: size(4, 3),
text: text("a\n\nbc"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
" a \n \n bc \n"
);
}
#[test]
fn no_text_shape_writes_any_position_more_than_once() {
for (area, content) in [
(size(8, 5), "one\ntwo"),
(size(0, 0), "abc"),
(size(1, 1), "abc"),
(size(4, 3), "a\n\nbc"),
(size(4, 3), ""),
] {
let mut surface = CountingSurface::default();
TextShape {
at: Pos { x: 0, y: 0 },
area,
text: text(content),
}
.draw(&mut surface);
assert!(
surface.max_writes() <= 1,
"{content:?} in {area:?} wrote a position more than once"
);
}
}
#[test]
fn a_three_line_block_at_a_zero_area_sits_around_the_point() {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(10, 10),
});
TextShape {
at: Pos { x: 5, y: 5 },
area: size(0, 0),
text: text("ABC\nD\nEFG"),
}
.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
assert_eq!(
render(&buffer, &light(), buffer.window()),
concat!(
" \n",
" \n",
" \n",
" \n",
" ABC \n",
" D \n",
" EFG \n",
" \n",
" \n",
" \n",
)
);
}
#[test]
fn a_second_text_shape_overwrites_the_first_where_they_meet() {
let first = || TextShape {
at: Pos { x: 5, y: 5 },
area: size(0, 0),
text: text("ABC\nD\nEFG"),
};
let second = || TextShape {
at: Pos { x: 5, y: 5 },
area: size(6, 6),
text: text("123\n4\n567"),
};
let drawn = |lower: &TextShape, upper: &TextShape| {
let mut buffer = Buffer::new(Window {
origin: origin(),
size: size(10, 10),
});
lower.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
upper.draw(&mut FigureLayer::new(&mut buffer, StampMode::Above));
render(&buffer, &light(), buffer.window())
};
let first_then_second = drawn(&first(), &second());
assert_eq!(
first_then_second,
concat!(
" \n",
" \n",
" \n",
" \n",
" ABC \n",
" D \n",
" EF123 \n",
" 4 \n",
" 567 \n",
" \n",
)
);
let second_then_first = drawn(&second(), &first());
assert!(
second_then_first.lines().nth(6) == Some(" EFG23 "),
"the other order leaves `EFG23` on that row: {second_then_first:?}"
);
assert_ne!(first_then_second, second_then_first);
}
}