Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
heartwood crates radicle-term src cell.rs
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);
    }
}