use core::{cmp, fmt};
use crate::Style;
use crate::layout::{LayoutDisplay, Renderable, SizeHint};
use crate::terminal::visual_line_width;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Text<'a> {
pub text: &'a str,
pub style: Option<Style>,
}
impl<'a> Text<'a> {
pub fn new(text: &'a str) -> Self {
Self { text, style: None }
}
pub fn with_style(mut self, style: Style) -> Self {
self.style = Some(style);
self
}
}
impl<'a> fmt::Display for Text<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display = LayoutDisplay {
layout: self,
width: 80,
};
fmt::Display::fmt(&display, f)
}
}
impl<'a> Renderable for Text<'a> {
fn measure(&self, max_width: usize) -> SizeHint {
let mut max_visual_len = 0;
for line in self.text.lines() {
max_visual_len = cmp::max(max_visual_len, visual_line_width(line));
}
SizeHint {
min: cmp::min(max_visual_len, max_width),
max: Some(cmp::min(max_visual_len, max_width)),
}
}
fn total_rows(&self, _width: usize) -> usize {
self.text.lines().count()
}
fn row_width(&self, row_idx: usize, _width: usize) -> usize {
self.text
.lines()
.nth(row_idx)
.map(visual_line_width)
.unwrap_or(0)
}
fn render_row(&self, row_idx: usize, _width: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(line) = self.text.lines().nth(row_idx) {
match self.style {
Some(style) => write!(f, "{}", style.apply(line))?,
None => f.write_str(line)?,
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::LayoutDisplay;
use crate::test_utils::MockTerminalGuard;
use crate::{Color, ColorLevel, Style};
#[test]
fn test_single_line_text() {
let left = format!("{}", Text::new("Single line text."));
let right = "Single line text.";
assert_eq!(left, right);
}
#[test]
fn test_multi_line_text() {
let text_raw = "This is line 1.\nThis is line 2.\nThis is line 3.";
let left = format!("{}", Text::new(text_raw));
assert_eq!(left, text_raw);
}
#[test]
fn test_empty_text() {
let txt = Text::new("");
assert_eq!(txt.total_rows(80), 0);
assert_eq!(txt.row_width(0, 80), 0);
assert_eq!(txt.measure(80).min, 0);
let left = format!("{}", txt);
assert_eq!(left, "");
}
#[test]
fn test_pure_newlines() {
let txt = Text::new("\n\n");
assert_eq!(txt.total_rows(80), 2);
assert_eq!(txt.row_width(0, 80), 0);
assert_eq!(txt.row_width(1, 80), 0);
}
#[test]
fn test_standard_emojis() {
let txt = Text::new("Hello 🦀 🚀");
assert_eq!(txt.row_width(0, 80), 11);
let hint = txt.measure(80);
assert_eq!(hint.min, 11);
}
#[test]
fn test_east_asian_cjk_characters() {
let txt = Text::new("Hello 华");
assert_eq!(txt.row_width(0, 80), 8);
}
#[test]
fn test_hindi_devanagari_scripts() {
let txt = Text::new("Hello नमस्ते");
assert_eq!(txt.row_width(0, 80), 10);
}
#[test]
fn test_measure_and_row_width_with_ansi_tags() {
let raw_styled = "\x1b[1mHello\x1b[0m \x1b[31mWorld!\x1b[0m";
let txt = Text::new(raw_styled);
assert_eq!(txt.total_rows(80), 1);
assert_eq!(txt.row_width(0, 80), 12);
let hint = txt.measure(80);
assert_eq!(hint.min, 12);
}
#[test]
fn test_measure_clamping_with_max_width() {
let txt = Text::new("LongStringUnbroken");
let hint = txt.measure(10);
assert_eq!(hint.min, 10);
assert_eq!(hint.max, Some(10));
}
#[test]
fn test_multi_line_asymmetrical_widths() {
let txt = Text::new("Short\nVeryLongLineHere\nTiny");
assert_eq!(txt.total_rows(80), 3);
assert_eq!(txt.row_width(0, 80), 5); assert_eq!(txt.row_width(1, 80), 16); assert_eq!(txt.row_width(2, 80), 4);
let hint = txt.measure(80);
assert_eq!(hint.min, 16);
}
#[test]
fn test_out_of_bounds_graceful_handling() {
let txt = Text::new("Line 1\nLine 2");
assert_eq!(txt.row_width(5, 80), 0);
struct Wrapper<'a>(&'a Text<'a>, usize);
impl<'a> fmt::Display for Wrapper<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.render_row(self.1, 80, f)
}
}
let rendered_invalid = format!("{}", Wrapper(&txt, 5));
assert_eq!(rendered_invalid, "");
}
#[test]
fn test_text_new_defaults_to_none_style() {
let text_element = Text::new("Hello, World!");
assert_eq!(text_element.text, "Hello, World!");
assert_eq!(text_element.style, None);
}
#[test]
fn test_text_with_style_builder() {
let custom_style = Style::new().fg(Color::Red).bold();
let text_element = Text::new("Styled Content").with_style(custom_style);
assert_eq!(text_element.style, Some(custom_style));
}
#[test]
fn test_render_row_without_style() {
let text_element = Text::new("Line 1\nLine 2");
let rendered_output = format!(
"{}",
LayoutDisplay {
layout: &text_element,
width: 80
}
);
assert_eq!(rendered_output, "Line 1\nLine 2");
}
#[test]
fn test_render_row_with_style_applies_ansi_escapes() {
let _guard = MockTerminalGuard::acquire(ColorLevel::TrueColor);
let custom_style = Style::new().fg(Color::Red);
let text_element = Text::new("Alert").with_style(custom_style);
let rendered = format!(
"{}",
LayoutDisplay {
layout: &text_element,
width: 80
}
);
assert_eq!(rendered, "\x1b[31mAlert\x1b[0m");
}
#[test]
fn test_row_width_ignores_ansi_escapes_via_visual_width() {
let _guard = MockTerminalGuard::acquire(ColorLevel::TrueColor);
let custom_style = Style::new().fg(Color::Green).bold();
let text_element = Text::new("Hello").with_style(custom_style);
let layout_width = text_element.row_width(0, 80);
assert_eq!(layout_width, 5);
}
#[test]
fn test_total_rows_and_measure_bounds() {
let text_element = Text::new("Short\nVery Long Line Here");
assert_eq!(text_element.total_rows(80), 2);
let size_hint = text_element.measure(80);
assert_eq!(size_hint.min, 19);
assert_eq!(size_hint.max, Some(19));
}
}