use std::fmt; use super::{Color, Filled, Line, Paint}; use unicode_display_width as unicode; use unicode_segmentation::UnicodeSegmentation as _; /// Text that can be displayed on the terminal, measured, truncated and padded. pub trait Cell: fmt::Display { /// Type after truncation. type Truncated: Cell; /// Type after padding. type Padded: Cell; /// Cell display width in number of terminal columns. fn width(&self) -> usize; /// Background color of cell. fn background(&self) -> Color { Color::Unset } /// Truncate cell if longer than given width. Shows the delimiter if truncated. #[must_use] fn truncate(&self, width: usize, delim: &str) -> Self::Truncated; /// Pad the cell so that it is the given width, while keeping the content left-aligned. #[must_use] fn pad(&self, width: usize) -> Self::Padded; } impl Cell for Paint { type Truncated = Self; type Padded = Self; fn width(&self) -> usize { Cell::width(self.content()) } fn background(&self) -> Color { self.style.background } fn truncate(&self, width: usize, delim: &str) -> Self { Self { item: self.item.truncate(width, delim), style: self.style, } } fn pad(&self, width: usize) -> Self { Self { item: self.item.pad(width), style: self.style, } } } impl Cell for Line { type Truncated = Line; type Padded = Line; fn width(&self) -> usize { Line::width(self) } fn pad(&self, width: usize) -> Self::Padded { let mut line = self.clone(); Line::pad(&mut line, width); line } fn truncate(&self, width: usize, delim: &str) -> Self::Truncated { let mut line = self.clone(); Line::truncate(&mut line, width, delim); line } } impl Cell for Paint<&str> { type Truncated = Paint; type Padded = Paint; fn width(&self) -> usize { Cell::width(self.item) } fn background(&self) -> Color { self.style.background } fn truncate(&self, width: usize, delim: &str) -> Paint { Paint { item: self.item.truncate(width, delim), style: self.style, } } fn pad(&self, width: usize) -> Paint { Paint { item: self.item.pad(width), style: self.style, } } } impl Cell for String { type Truncated = Self; type Padded = Self; fn width(&self) -> usize { Cell::width(self.as_str()) } fn truncate(&self, width: usize, delim: &str) -> Self { self.as_str().truncate(width, delim) } fn pad(&self, width: usize) -> Self { self.as_str().pad(width) } } impl Cell for str { type Truncated = String; type Padded = String; fn width(&self) -> usize { self.graphemes(true) .map(|g| unicode::width(g) as usize) .sum() } fn truncate(&self, width: usize, delim: &str) -> String { if width < Cell::width(self) { let d = Cell::width(delim); if width < d { // If we can't even fit the delimiter, just return an empty string. return String::new(); } // Find the unicode byte boundary where the display width is the largest, // while being smaller than the given max width. let mut cols = 0; // Number of visual columns we need. let mut boundary = 0; // Boundary in bytes. for g in self.graphemes(true) { let c = Cell::width(g); if cols + c + d > width { break; } boundary += g.len(); cols += c; } // Don't add the delimiter if we just trimmed whitespace. if self[boundary..].trim().is_empty() { self[..boundary + 1].to_owned() } else { format!("{}{delim}", &self[..boundary]) } } else { self.to_owned() } } fn pad(&self, max: usize) -> String { let width = Cell::width(self); if width < max { format!("{self}{}", " ".repeat(max - width)) } else { self.to_owned() } } } impl Cell for &T { type Truncated = T::Truncated; type Padded = T::Padded; fn width(&self) -> usize { T::width(self) } fn truncate(&self, width: usize, delim: &str) -> Self::Truncated { T::truncate(self, width, delim) } fn pad(&self, width: usize) -> Self::Padded { T::pad(self, width) } } impl Cell for Filled { type Truncated = T::Truncated; type Padded = T::Padded; fn width(&self) -> usize { T::width(&self.item) } fn background(&self) -> Color { self.color } fn truncate(&self, width: usize, delim: &str) -> Self::Truncated { T::truncate(&self.item, width, delim) } fn pad(&self, width: usize) -> Self::Padded { T::pad(&self.item, width) } } #[cfg(test)] mod test { #[test] fn test_width() { assert_eq!(unicode_display_width::width("❤️"), 2); assert_eq!(unicode_display_width::width("🪵"), 2); } }