Skip to main content

urushi/text/
tab.rs

1use std::fmt;
2
3use super::PrintableText;
4
5pub(crate) static DEFAULT_TAB_POLICY: TabPolicy = TabPolicy::spaces(4);
6
7/// A fixed-width replacement for one tab during text layout.
8///
9/// A policy without a marker produces `width` spaces. A policy with a marker
10/// writes that printable text first and pads its right side with spaces until
11/// the replacement occupies `width` cells. Literal tabs are preserved only by
12/// the non-layout text renderer; resolved views never contain them.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TabPolicy {
15    width: u16,
16    marker: Option<String>,
17}
18
19impl Default for TabPolicy {
20    fn default() -> Self {
21        Self::spaces(4)
22    }
23}
24
25impl TabPolicy {
26    /// Replaces each tab with `width` spaces.
27    pub const fn spaces(width: u16) -> Self {
28        Self {
29            width,
30            marker: None,
31        }
32    }
33
34    /// Replaces each tab with `marker`, padded on the right to `width` cells.
35    ///
36    /// An empty marker is canonicalized to no marker. Other markers must be
37    /// printable text on one line, have no zero-width graphemes, and fit
38    /// inside the requested width.
39    pub fn with_marker(width: u16, marker: impl Into<String>) -> Result<Self, InvalidTabMarker> {
40        let marker = marker.into();
41        if marker.is_empty() {
42            return Ok(Self::spaces(width));
43        }
44        if marker.chars().any(char::is_control) {
45            return Err(InvalidTabMarker::ControlCharacter);
46        }
47        let printable = PrintableText::new(&marker);
48        if printable.graphemes().any(|grapheme| grapheme.width() == 0) {
49            return Err(InvalidTabMarker::ZeroWidthGrapheme);
50        }
51        let marker_width = printable.width();
52        if marker_width > usize::from(width) {
53            return Err(InvalidTabMarker::TooWide {
54                width,
55                marker_width,
56            });
57        }
58        Ok(Self {
59            width,
60            marker: Some(marker),
61        })
62    }
63
64    /// Returns the fixed cell width of one replacement.
65    pub const fn width(&self) -> u16 {
66        self.width
67    }
68
69    /// Returns the optional visible marker written before the padding.
70    pub fn marker(&self) -> Option<&str> {
71        self.marker.as_deref()
72    }
73}
74
75/// Why a visible tab marker cannot participate in layout.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum InvalidTabMarker {
78    /// The marker contains a terminal control character.
79    ControlCharacter,
80    /// The marker contains a grapheme with no independently visible cells.
81    ZeroWidthGrapheme,
82    /// The marker occupies more cells than the configured replacement width.
83    TooWide { width: u16, marker_width: usize },
84}
85
86impl fmt::Display for InvalidTabMarker {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            Self::ControlCharacter => {
90                formatter.write_str("a tab marker must not contain control characters")
91            }
92            Self::ZeroWidthGrapheme => {
93                formatter.write_str("every tab marker grapheme must occupy at least one cell")
94            }
95            Self::TooWide {
96                width,
97                marker_width,
98            } => write!(
99                formatter,
100                "tab marker occupies {marker_width} cells, exceeding its width of {width}"
101            ),
102        }
103    }
104}
105
106impl std::error::Error for InvalidTabMarker {}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn empty_marker_is_canonicalized_to_spaces() {
114        assert_eq!(TabPolicy::with_marker(3, "").unwrap(), TabPolicy::spaces(3));
115    }
116
117    #[test]
118    fn marker_must_be_printable_and_fit_its_width() {
119        assert_eq!(
120            TabPolicy::with_marker(4, "→\t"),
121            Err(InvalidTabMarker::ControlCharacter)
122        );
123        assert_eq!(
124            TabPolicy::with_marker(1, "日本"),
125            Err(InvalidTabMarker::TooWide {
126                width: 1,
127                marker_width: 4,
128            })
129        );
130        assert_eq!(
131            TabPolicy::with_marker(1, "\u{301}"),
132            Err(InvalidTabMarker::ZeroWidthGrapheme)
133        );
134        assert_eq!(
135            TabPolicy::with_marker(1, "\u{301}x"),
136            Err(InvalidTabMarker::ZeroWidthGrapheme)
137        );
138    }
139}