218 lines
5.3 KiB
Rust
218 lines
5.3 KiB
Rust
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<String> {
|
|
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<String>;
|
|
type Padded = Paint<String>;
|
|
|
|
fn width(&self) -> usize {
|
|
Cell::width(self.item)
|
|
}
|
|
|
|
fn background(&self) -> Color {
|
|
self.style.background
|
|
}
|
|
|
|
fn truncate(&self, width: usize, delim: &str) -> Paint<String> {
|
|
Paint {
|
|
item: self.item.truncate(width, delim),
|
|
style: self.style,
|
|
}
|
|
}
|
|
|
|
fn pad(&self, width: usize) -> Paint<String> {
|
|
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<T: Cell + ?Sized> 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<T: Cell + fmt::Display> Cell for Filled<T> {
|
|
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);
|
|
}
|
|
}
|