tty_form/
text.rs

1use tty_interface::Style;
2
3/// A segment of multi-part formatted text content.
4pub type Segment = Vec<Text>;
5
6/// A collection of text segments representing each item in the drawer and each rendered
7/// vertically-separated.
8pub type DrawerContents = Vec<Segment>;
9
10/// A tuple of text content and optional styling.
11#[derive(Debug, Clone, Eq, PartialEq)]
12pub struct Text(String, Option<Style>);
13
14impl Text {
15    /// Create a new, unstyled text segment.
16    pub fn new(content: String) -> Self {
17        Self(content, None)
18    }
19
20    /// Create a new, styled text segment.
21    pub fn new_styled(content: String, style: Style) -> Self {
22        Self(content, Some(style))
23    }
24
25    /// This text's content.
26    pub fn content(&self) -> &str {
27        &self.0
28    }
29
30    /// This text's styling, if specified.
31    pub fn style(&self) -> Option<&Style> {
32        self.1.as_ref()
33    }
34
35    /// This text as a single-element vector.
36    pub fn as_segment(self) -> Segment {
37        vec![self]
38    }
39}
40
41pub(crate) fn set_segment_style(segment: &mut Segment, style: Style) {
42    let segment_length = get_segment_length(segment);
43    set_segment_subset_style(segment, 0, segment_length, style);
44}
45
46/// Update a segment's style for some subset of its graphemes.
47pub(crate) fn set_segment_subset_style(
48    segment: &mut Segment,
49    start: usize,
50    end: usize,
51    style: Style,
52) {
53    let mut index = 0;
54    let mut i = 0;
55    loop {
56        if i == segment.len() {
57            break;
58        }
59
60        let text = &segment[i];
61
62        let start_intersects = start > index && start < index + text.content().len();
63        let end_intersects = end > index && end < index + text.content().len();
64
65        if start_intersects {
66            let (first, second) = split_text(text, start - index);
67
68            segment[i] = first;
69            segment.insert(i + 1, second);
70        } else if end_intersects {
71            let (first, second) = split_text(text, end - index);
72
73            segment[i] = first;
74            segment.insert(i + 1, second);
75        }
76
77        index += segment[i].content().len();
78        i += 1;
79    }
80
81    index = 0;
82    for text in segment {
83        if index >= start && index < end {
84            text.1 = Some(style);
85        }
86
87        index += text.content().len();
88    }
89}
90
91pub(crate) fn get_segment_length(segment: &Segment) -> usize {
92    segment.iter().map(|text| text.content().len()).sum()
93}
94
95fn split_text(text: &Text, index: usize) -> (Text, Text) {
96    let (prefix, suffix) = text.0.split_at(index);
97
98    let first = Text(prefix.to_string(), text.1);
99    let second = Text(suffix.to_string(), text.1);
100
101    (first, second)
102}
103
104#[cfg(test)]
105mod tests {
106    use tty_interface::Color;
107
108    use crate::text::Text;
109
110    use super::set_segment_subset_style;
111
112    macro_rules! text {
113        ($content: expr) => {
114            Text::new($content.to_string())
115        };
116    }
117
118    macro_rules! text_styled {
119        ($content: expr, $color: expr) => {
120            Text::new_styled($content.to_string(), $color.as_style())
121        };
122    }
123
124    #[test]
125    fn test_set_segment_style_entirely() {
126        let mut segment = vec![
127            text!("TEST1"),
128            text_styled!("TEST2", Color::Red),
129            text_styled!("TEST3", Color::Blue),
130            text!("TEST4"),
131        ];
132
133        set_segment_subset_style(&mut segment, 0, 20, Color::Green.as_style());
134
135        assert_eq!(
136            vec![
137                text_styled!("TEST1", Color::Green),
138                text_styled!("TEST2", Color::Green),
139                text_styled!("TEST3", Color::Green),
140                text_styled!("TEST4", Color::Green),
141            ],
142            segment
143        );
144    }
145
146    #[test]
147    fn test_set_segment_style_neatly() {
148        let mut segment = vec![
149            text!("TEST1"),
150            text_styled!("TEST2", Color::Red),
151            text_styled!("TEST3", Color::Blue),
152            text!("TEST4"),
153        ];
154
155        set_segment_subset_style(&mut segment, 5, 15, Color::Green.as_style());
156
157        assert_eq!(
158            vec![
159                text!("TEST1"),
160                text_styled!("TEST2", Color::Green),
161                text_styled!("TEST3", Color::Green),
162                text!("TEST4"),
163            ],
164            segment
165        );
166    }
167
168    #[test]
169    fn test_set_segment_style_split() {
170        let mut segment = vec![
171            text!("TEST1"),
172            text_styled!("TEST2", Color::Red),
173            text_styled!("TEST3", Color::Blue),
174            text!("TEST4"),
175        ];
176
177        set_segment_subset_style(&mut segment, 3, 7, Color::Green.as_style());
178        set_segment_subset_style(&mut segment, 11, 14, Color::Magenta.as_style());
179
180        assert_eq!(
181            vec![
182                text!("TES"),
183                text_styled!("T1", Color::Green),
184                text_styled!("TE", Color::Green),
185                text_styled!("ST2", Color::Red),
186                text_styled!("T", Color::Blue),
187                text_styled!("EST", Color::Magenta),
188                text_styled!("3", Color::Blue),
189                text!("TEST4"),
190            ],
191            segment
192        );
193    }
194}