use super::align::{HAlign, VAlign};
use super::word_wrap::wrap_line;
use crate::grid::{Grid, Rect, Size};
use crate::surface::Surface;
use crate::text::Line;
pub struct TextLayout<'a> {
line: &'a Line,
rect: Rect,
h_align: HAlign,
v_align: VAlign,
}
impl<'a> TextLayout<'a> {
#[must_use]
pub const fn new(line: &'a Line) -> Self {
Self {
line,
rect: Rect::EMPTY,
h_align: HAlign::Left,
v_align: VAlign::Top,
}
}
#[must_use]
pub const fn rect(mut self, rect: Rect) -> Self {
self.rect = rect;
self
}
#[must_use]
pub const fn h_align(mut self, align: HAlign) -> Self {
self.h_align = align;
self
}
#[must_use]
pub const fn v_align(mut self, align: VAlign) -> Self {
self.v_align = align;
self
}
#[must_use]
pub fn measure(&self) -> Size {
let lines = wrap_line(self.line, self.rect.width());
let width = lines.iter().map(|l| l.width).max().unwrap_or(0);
#[allow(clippy::cast_possible_truncation)]
let height = lines.len().min(u16::MAX as usize) as u16;
Size::new(width, height)
}
pub fn render_to_surface(&self, surface: &mut Surface<'_>) {
let clipped = Self {
line: self.line,
rect: self.rect.intersect(surface.clip_rect()),
h_align: self.h_align,
v_align: self.v_align,
};
let layer = surface.layer();
clipped.render_to_grid(surface.grid_mut(), layer);
}
pub fn render_to_grid(&self, grid: &mut Grid, layer: u8) {
let lines = wrap_line(self.line, self.rect.width());
let rect = self.rect;
#[allow(clippy::cast_possible_truncation)]
let total_lines = lines.len().min(usize::from(rect.height())) as u16;
let y_offset = self.v_align.offset(rect.height(), total_lines);
for (line_idx, wrapped) in lines.into_iter().take(total_lines as usize).enumerate() {
let x_offset = self.h_align.offset(rect.width(), wrapped.width);
#[allow(clippy::cast_possible_truncation)]
let row = rect.top() + y_offset + line_idx as u16;
let mut cx = rect.left() + x_offset;
for glyph in wrapped.glyphs {
if cx + glyph.width > rect.right() {
break;
}
grid.write_grapheme(layer, cx, row, &glyph.grapheme, glyph.style);
cx += glyph.width;
}
}
}
}
#[cfg(test)]
mod tests {
use ixy::HasSize;
use super::*;
use crate::grid::Pos;
use alloc::string::String;
#[test]
fn test_measure_single_line() {
let line = Line::raw("hello");
let m = TextLayout::new(&line)
.rect(Rect::new(0, 0, 20, 5))
.measure();
assert_eq!(m.width(), 5);
assert_eq!(m.height(), 1);
}
#[test]
fn test_measure_wraps() {
let line = Line::raw("hello world");
let m = TextLayout::new(&line)
.rect(Rect::new(0, 0, 7, 10))
.measure();
assert_eq!(m.height(), 2);
assert_eq!(m.width(), 5);
}
#[test]
fn test_render_left_top() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 5));
let line = Line::raw("hi");
TextLayout::new(&line)
.rect(Rect::new(2, 1, 10, 3))
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(2, 1)].glyph(), 'h');
assert_eq!(term.grid()[Pos::new(3, 1)].glyph(), 'i');
assert_eq!(term.grid()[Pos::new(4, 1)].glyph(), ' '); }
#[test]
fn test_render_center_h() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 5));
let line = Line::raw("hi");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 10, 3))
.h_align(HAlign::Center)
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(4, 0)].glyph(), 'h');
assert_eq!(term.grid()[Pos::new(5, 0)].glyph(), 'i');
}
#[test]
fn test_render_right_h() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 5));
let line = Line::raw("hi");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 10, 3))
.h_align(HAlign::Right)
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(8, 0)].glyph(), 'h');
assert_eq!(term.grid()[Pos::new(9, 0)].glyph(), 'i');
}
#[test]
fn test_render_middle_v() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 10));
let line = Line::raw("hi");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 10, 5))
.v_align(VAlign::Middle)
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), 'h');
}
#[test]
fn test_render_bottom_v() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 10));
let line = Line::raw("hi");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 10, 5))
.v_align(VAlign::Bottom)
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(0, 4)].glyph(), 'h');
}
#[test]
fn test_render_clips_to_height() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(10, 10));
let line = Line::raw("a b c");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 1, 2))
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), 'a');
assert_eq!(term.grid()[Pos::new(0, 1)].glyph(), 'b');
assert_eq!(term.grid()[Pos::new(0, 2)].glyph(), ' '); }
#[test]
fn text_layout_render_to_surface_escapes_the_surface_clip() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(20, 5));
{
let mut surface = term.surface();
let mut bar = surface.clip(Rect::new(0, 0, 10, 1));
let line = Line::raw("hello world");
TextLayout::new(&line)
.rect(Rect::new(0, 0, 10, 4))
.render_to_surface(&mut bar);
}
let row0: String = (0..10)
.map(|x| term.grid()[Pos::new(x, 0)].glyph())
.collect();
assert_eq!(row0.trim_end(), "hello");
for x in 0..10 {
assert_eq!(term.grid()[Pos::new(x, 1)].glyph(), ' ');
}
}
#[test]
fn text_layout_wide_glyph_stays_inside_the_rect() {
use crate::backend::Headless;
use crate::terminal::Terminal;
let mut term = Terminal::new(Headless::new(10, 5));
let line = Line::raw("\u{3042}"); TextLayout::new(&line)
.rect(Rect::new(0, 0, 1, 1))
.render_to_surface(&mut term.surface());
assert_eq!(term.grid()[Pos::new(0, 0)].glyph(), ' ');
assert_eq!(term.grid()[Pos::new(1, 0)].glyph(), ' ');
}
}