use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub fn measure_text_width(text: &str) -> usize {
text.graphemes(true).map(UnicodeWidthStr::width).sum()
}
pub fn display_width(text: &str) -> usize {
measure_text_width(text)
}
pub fn measure_text(text: &str) -> (usize, usize) {
let lines: Vec<&str> = text.lines().collect();
let height = lines.len().max(1);
let width = lines.iter().map(|line| line.width()).max().unwrap_or(0);
(width, height)
}
pub fn wrap_text(text: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
}
let mut result = String::new();
let mut current_width = 0;
for grapheme in text.graphemes(true) {
let grapheme_width = UnicodeWidthStr::width(grapheme);
if grapheme == "\n" {
result.push('\n');
current_width = 0;
} else if current_width + grapheme_width > max_width {
result.push('\n');
result.push_str(grapheme);
current_width = grapheme_width;
} else {
result.push_str(grapheme);
current_width += grapheme_width;
}
}
result
}
pub fn truncate_text(text: &str, max_width: usize, ellipsis: &str) -> String {
let text_width = measure_text_width(text);
if text_width <= max_width {
return text.to_string();
}
let ellipsis_width = measure_text_width(ellipsis);
if max_width <= ellipsis_width {
let mut result = String::new();
let mut width = 0;
for g in ellipsis.graphemes(true) {
let gw = UnicodeWidthStr::width(g);
if width + gw > max_width {
break;
}
result.push_str(g);
width += gw;
}
return result;
}
let target_width = max_width - ellipsis_width;
let mut result = String::new();
let mut current_width = 0;
for grapheme in text.graphemes(true) {
let grapheme_width = UnicodeWidthStr::width(grapheme);
if current_width + grapheme_width > target_width {
break;
}
result.push_str(grapheme);
current_width += grapheme_width;
}
result.push_str(ellipsis);
result
}
pub fn truncate_start(text: &str, max_width: usize, ellipsis: &str) -> String {
let text_width = measure_text_width(text);
if text_width <= max_width {
return text.to_string();
}
let ellipsis_width = measure_text_width(ellipsis);
if max_width <= ellipsis_width {
let mut result = String::new();
let mut width = 0;
for g in ellipsis.graphemes(true) {
let gw = UnicodeWidthStr::width(g);
if width + gw > max_width {
break;
}
result.push_str(g);
width += gw;
}
return result;
}
let target_width = max_width - ellipsis_width;
let graphemes: Vec<&str> = text.graphemes(true).collect();
let mut result = String::new();
let mut current_width = 0;
let mut end_graphemes = Vec::new();
for grapheme in graphemes.iter().rev() {
let grapheme_width = UnicodeWidthStr::width(*grapheme);
if current_width + grapheme_width > target_width {
break;
}
end_graphemes.push(*grapheme);
current_width += grapheme_width;
}
end_graphemes.reverse();
result.push_str(ellipsis);
for g in end_graphemes {
result.push_str(g);
}
result
}
pub fn truncate_middle(text: &str, max_width: usize, ellipsis: &str) -> String {
let text_width = measure_text_width(text);
if text_width <= max_width {
return text.to_string();
}
let ellipsis_width = measure_text_width(ellipsis);
if max_width <= ellipsis_width {
let mut result = String::new();
let mut width = 0;
for g in ellipsis.graphemes(true) {
let gw = UnicodeWidthStr::width(g);
if width + gw > max_width {
break;
}
result.push_str(g);
width += gw;
}
return result;
}
let available = max_width - ellipsis_width;
let left_width = available / 2;
let right_width = available - left_width;
let graphemes: Vec<&str> = text.graphemes(true).collect();
let mut left = String::new();
let mut current_width = 0;
for grapheme in &graphemes {
let grapheme_width = UnicodeWidthStr::width(*grapheme);
if current_width + grapheme_width > left_width {
break;
}
left.push_str(grapheme);
current_width += grapheme_width;
}
let mut right_graphemes = Vec::new();
current_width = 0;
for grapheme in graphemes.iter().rev() {
let grapheme_width = UnicodeWidthStr::width(*grapheme);
if current_width + grapheme_width > right_width {
break;
}
right_graphemes.push(*grapheme);
current_width += grapheme_width;
}
right_graphemes.reverse();
let mut right = String::new();
for g in right_graphemes {
right.push_str(g);
}
format!("{}{}{}", left, ellipsis, right)
}
pub fn pad_text(text: &str, width: usize, align: TextAlign) -> String {
let text_width = text.width();
if text_width >= width {
return text.to_string();
}
let padding = width - text_width;
match align {
TextAlign::Left => format!("{}{}", text, " ".repeat(padding)),
TextAlign::Right => format!("{}{}", " ".repeat(padding), text),
TextAlign::Center => {
let left_pad = padding / 2;
let right_pad = padding - left_pad;
format!("{}{}{}", " ".repeat(left_pad), text, " ".repeat(right_pad))
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub enum TextAlign {
#[default]
Left,
Right,
Center,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_measure_ascii() {
assert_eq!(measure_text_width("hello"), 5);
assert_eq!(measure_text_width("hello world"), 11);
}
#[test]
fn test_measure_unicode() {
assert_eq!(measure_text_width("δ½ ε₯½"), 4);
assert_eq!(measure_text_width("Hello δΈη"), 10);
}
#[test]
fn test_measure_text_dimensions() {
let (w, h) = measure_text("hello\nworld");
assert_eq!(w, 5);
assert_eq!(h, 2);
}
#[test]
fn test_wrap_text() {
let wrapped = wrap_text("hello world", 6);
assert!(wrapped.contains('\n'));
}
#[test]
fn test_truncate_text() {
let truncated = truncate_text("hello world", 8, "...");
assert_eq!(truncated, "hello...");
}
#[test]
fn test_truncate_start() {
let truncated = truncate_start("hello world", 8, "...");
assert_eq!(truncated, "...world");
}
#[test]
fn test_truncate_middle() {
let truncated = truncate_middle("hello world", 9, "...");
assert_eq!(truncated, "hel...rld");
}
#[test]
fn test_pad_text() {
assert_eq!(pad_text("hi", 5, TextAlign::Left), "hi ");
assert_eq!(pad_text("hi", 5, TextAlign::Right), " hi");
assert_eq!(pad_text("hi", 5, TextAlign::Center), " hi ");
}
#[test]
fn test_grapheme_clusters_emoji() {
let family = "π¨βπ©βπ§βπ¦";
let graphemes: Vec<&str> = family.graphemes(true).collect();
assert_eq!(graphemes.len(), 1, "Family emoji should be 1 grapheme");
}
#[test]
fn test_grapheme_clusters_combining() {
let combined = "Γ©"; let graphemes: Vec<&str> = combined.graphemes(true).collect();
assert!(graphemes.len() <= 2); }
#[test]
fn test_truncate_preserves_graphemes() {
let text = "hello δ½ ε₯½";
let truncated = truncate_text(text, 8, "β¦");
assert!(measure_text_width(&truncated) <= 8);
}
#[test]
fn test_zero_width_characters() {
let zwj = "\u{200D}"; assert_eq!(measure_text_width(zwj), 0);
}
}