#[cfg(not(feature = "egc"))]
use alloc::string::{String, ToString as _};
#[cfg(not(feature = "egc"))]
use alloc::vec;
#[cfg(not(feature = "egc"))]
use alloc::vec::Vec;
use retroglyph_core::color::Style;
#[cfg(feature = "egc")]
use retroglyph_core::grid::HasSize;
#[cfg(feature = "egc")]
use retroglyph_core::grid::Rect;
#[cfg(feature = "egc")]
use retroglyph_core::layout::TextLayout;
#[cfg(feature = "egc")]
use retroglyph_core::text::{Line, Span};
use super::{Measure, Widget};
use crate::Surface;
#[derive(Clone, Copy, Debug)]
pub struct Paragraph<'a> {
text: &'a str,
style: Style,
}
impl<'a> Paragraph<'a> {
#[must_use]
pub fn new(text: &'a str) -> Self {
Self {
text,
style: Style::new(),
}
}
#[must_use]
pub const fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[cfg(feature = "egc")]
fn line(&self) -> Line {
Line::from(Span::styled(self.text, self.style))
}
}
#[cfg(not(feature = "egc"))]
fn wrap(text: &str, max_width: u16) -> Vec<String> {
use retroglyph_core::text::char_width;
let mut lines: Vec<String> = vec![String::new()];
let mut col: u16 = 0;
for ch in text.chars() {
if ch == '\n' {
lines.push(String::new());
col = 0;
continue;
}
let cw = char_width(ch);
if cw == 0 {
continue; }
if col + cw > max_width && col > 0 {
let current = lines.last_mut().expect("always at least one line");
if let Some(space_idx) = current.rfind(' ') {
let remainder = current[space_idx + 1..].to_string();
current.truncate(space_idx); col = remainder.chars().map(char_width).sum();
lines.push(remainder);
} else {
lines.push(String::new());
col = 0;
if ch == ' ' {
continue;
}
}
}
lines.last_mut().expect("always at least one line").push(ch);
col += cw;
}
lines
}
#[cfg(feature = "egc")]
impl Measure for Paragraph<'_> {
fn height_for(&self, width: u16) -> u16 {
let line = self.line();
TextLayout::new(&line)
.rect(Rect::new(0, 0, width, u16::MAX))
.measure()
.height()
}
}
#[cfg(not(feature = "egc"))]
impl Measure for Paragraph<'_> {
fn height_for(&self, width: u16) -> u16 {
let lines = wrap(self.text, width);
#[allow(clippy::cast_possible_truncation)]
let height = lines.len().min(usize::from(u16::MAX)) as u16;
height
}
}
#[cfg(feature = "egc")]
impl Widget for Paragraph<'_> {
fn render(&self, surface: &mut Surface<'_>) {
let area = surface.area();
let line = self.line();
TextLayout::new(&line).rect(area).render_to_surface(surface);
}
}
#[cfg(not(feature = "egc"))]
impl Widget for Paragraph<'_> {
fn render(&self, surface: &mut Surface<'_>) {
let width = surface.area().width();
let height = surface.area().height();
let lines = wrap(self.text, width);
for (row, line) in lines.into_iter().take(usize::from(height)).enumerate() {
#[allow(clippy::cast_possible_truncation)] surface.print((0, row as u16), &line, self.style);
}
}
}
#[cfg(test)]
mod tests {
use alloc::string::String;
use retroglyph_core::grid::{Grid, Pos, Rect};
use super::*;
#[test]
fn height_for_matches_wrapped_line_count() {
let p = Paragraph::new("the quick brown fox jumps");
assert_eq!(p.height_for(10), 3); assert_eq!(p.height_for(100), 1);
}
#[test]
fn height_for_respects_hard_newlines() {
let p = Paragraph::new("first\nsecond\nthird");
assert_eq!(p.height_for(100), 3);
}
#[test]
fn render_draws_one_line_per_wrapped_row() {
let area = Rect::new(0, 0, 10, 5);
let mut grid = Grid::new(10, 5);
Paragraph::new("the quick brown fox jumps").render(&mut Surface::new(&mut grid, area, 0));
let row0: String = (0..10).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
let row1: String = (0..10).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
let row2: String = (0..10).map(|x| grid[Pos::new(x, 2)].glyph()).collect();
assert!(row0.starts_with("the quick"));
assert!(row1.starts_with("brown fox"));
assert!(row2.starts_with("jumps"));
}
#[test]
fn render_stops_at_the_area_bottom() {
let area = Rect::new(0, 0, 10, 1);
let mut grid = Grid::new(10, 2);
Paragraph::new("the quick brown fox jumps").render(&mut Surface::new(&mut grid, area, 0));
let row1: String = (0..10).map(|x| grid[Pos::new(x, 1)].glyph()).collect();
assert_eq!(row1.trim(), "");
}
#[cfg(feature = "egc")]
#[test]
fn paragraph_honours_the_surface_clip() {
let area = Rect::new(0, 0, 10, 3);
let mut grid = Grid::new(10, 3);
{
let mut surface = Surface::new(&mut grid, area, 0);
let mut clipped = surface.clip(Rect::new(0, 0, 10, 1));
Paragraph::new("aaaa bbbb cccc").render(&mut clipped);
}
let row0: String = (0..10).map(|x| grid[Pos::new(x, 0)].glyph()).collect();
assert_eq!(row0.trim_end(), "aaaa bbbb");
for row in 1..3 {
let r: String = (0..10).map(|x| grid[Pos::new(x, row)].glyph()).collect();
assert_eq!(r.trim(), "");
}
}
}