use crate::grid::{Grid, Rect};
use crate::style::Style;
use crate::surface::Surface;
use crate::text::Line;
use alloc::string::String;
use alloc::vec::Vec;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum HAlign {
#[default]
Left,
Center,
Right,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum VAlign {
#[default]
Top,
Middle,
Bottom,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct TextMetrics {
pub width: u16,
pub height: u16,
}
struct WrappedGlyph {
grapheme: String,
style: Style,
width: u16,
}
struct WrappedLine {
glyphs: Vec<WrappedGlyph>,
width: u16,
}
fn wrap_line(line: &Line, max_width: u16) -> Vec<WrappedLine> {
let mut lines: Vec<WrappedLine> = alloc::vec![WrappedLine {
glyphs: Vec::new(),
width: 0,
}];
let mut col: u16 = 0;
for span in &line.spans {
for grapheme in span.content.graphemes(true) {
if grapheme == "\n" {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
col = 0;
continue;
}
#[allow(clippy::cast_possible_truncation)]
let gw = grapheme.width() as u16;
if gw == 0 {
continue; }
if col + gw > max_width && col > 0 {
let current = lines.last_mut().expect("always at least one line");
if let Some(space_idx) = current.glyphs.iter().rposition(|g| g.grapheme == " ") {
let remainder: Vec<WrappedGlyph> =
current.glyphs.drain(space_idx + 1..).collect();
current.glyphs.pop();
current.width = current.glyphs.iter().map(|g| g.width).sum();
let new_width: u16 = remainder.iter().map(|g| g.width).sum();
col = new_width;
lines.push(WrappedLine {
glyphs: remainder,
width: new_width,
});
} else {
lines.push(WrappedLine {
glyphs: Vec::new(),
width: 0,
});
col = 0;
if grapheme == " " {
continue;
}
}
}
let current = lines.last_mut().expect("always at least one line");
current.width += gw;
current.glyphs.push(WrappedGlyph {
grapheme: String::from(grapheme),
style: span.style,
width: gw,
});
col += gw;
}
}
lines
}
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) -> TextMetrics {
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;
TextMetrics { width, height }
}
pub fn render_to_surface(&self, surface: &mut Surface<'_>) {
let clipped = Self {
line: self.line,
rect: self.rect.intersect(surface.area()),
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 = match self.v_align {
VAlign::Top => 0,
VAlign::Middle => rect.height().saturating_sub(total_lines) / 2,
VAlign::Bottom => rect.height().saturating_sub(total_lines),
};
for (line_idx, wrapped) in lines.into_iter().take(total_lines as usize).enumerate() {
let x_offset = match self.h_align {
HAlign::Left => 0,
HAlign::Center => rect.width().saturating_sub(wrapped.width) / 2,
HAlign::Right => rect.width().saturating_sub(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 >= rect.right() {
break;
}
grid.write_grapheme(layer, cx, row, &glyph.grapheme, glyph.style);
cx += glyph.width;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::Color;
use crate::grid::Pos;
use crate::style::Style;
use crate::text::{Line, Span};
fn red() -> Style {
Style::new().fg(Color::RED)
}
#[test]
fn test_wrap_no_wrap_needed() {
let line = Line::raw("hello");
let lines = wrap_line(&line, 10);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].width, 5);
}
#[test]
fn test_wrap_hard_newline() {
let line = Line::raw("hi\nthere");
let lines = wrap_line(&line, 20);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 2);
assert_eq!(lines[1].width, 5);
}
#[test]
fn test_wrap_soft_break_on_space() {
let line = Line::raw("hello world");
let lines = wrap_line(&line, 7);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 5); assert_eq!(lines[1].width, 5); }
#[test]
fn test_wrap_force_break_no_space() {
let line = Line::raw("abcdefgh");
let lines = wrap_line(&line, 4);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 4);
assert_eq!(lines[1].width, 4);
}
#[test]
fn test_wrap_wide_chars() {
let line = Line::raw("䏿–‡ä¸");
let lines = wrap_line(&line, 4);
assert_eq!(lines.len(), 2);
assert_eq!(lines[0].width, 4);
assert_eq!(lines[1].width, 2);
}
#[test]
fn test_wrap_multi_span() {
let line = Line::from(vec![Span::raw("foo "), Span::styled("bar", red())]);
let lines = wrap_line(&line, 20);
assert_eq!(lines.len(), 1);
assert_eq!(lines[0].width, 7);
let bar_count = lines[0].glyphs.iter().filter(|g| g.style == red()).count();
assert_eq!(bar_count, 3);
}
#[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(), ' '); }
}