buffer_graphics_lib/text/
wrapping.rs

1#[cfg(feature = "serde")]
2use serde::{Deserialize, Serialize};
3
4/// Use `PixelFont::px_to_cols` to convert pixels to columns
5#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
6#[derive(Debug, Clone, Eq, PartialEq, Copy, Default)]
7pub enum WrappingStrategy {
8    #[default]
9    /// Don't wrap, may draw off screen
10    None,
11    /// Splits string at column
12    AtCol(usize),
13    /// Wraps at the first space before column (acts like AtCol if no space is on that line)
14    SpaceBeforeCol(usize),
15    /// Same as AtCol but adds a hyphen if it's splitting a word
16    AtColWithHyphen(usize),
17    /// Chops off the end of string it's over specific length
18    Cutoff(usize),
19    /// Same as Cutoff but adds an ellipsis
20    Ellipsis(usize),
21}
22
23impl WrappingStrategy {
24    pub fn wrap(&self, input: &str) -> Vec<String> {
25        match self {
26            WrappingStrategy::None => input.split('\n').map(|s| s.to_string()).collect(),
27            WrappingStrategy::AtCol(col) => {
28                if *col == 0 {
29                    return split_str_to_col(input);
30                }
31                let mut output = vec![];
32                let lines = WrappingStrategy::None.wrap(input);
33                for mut line in lines {
34                    while line.chars().count() > *col {
35                        output.push(line.chars().take(*col).collect());
36                        line = line.chars().skip(*col).collect();
37                    }
38                    output.push(line);
39                }
40                output
41            }
42            WrappingStrategy::SpaceBeforeCol(col) => {
43                if *col == 0 {
44                    return split_str_to_col(input);
45                }
46                let mut output = vec![];
47                let lines = WrappingStrategy::None.wrap(input);
48                for mut text in lines {
49                    while text.chars().count() > *col {
50                        let chars: Vec<char> = text.chars().collect();
51                        let line: String = text.chars().take(*col).collect();
52                        if line.ends_with(|c: char| c.is_whitespace())
53                            || text
54                                .chars()
55                                .nth(*col)
56                                .map(|c| c.is_whitespace())
57                                .unwrap_or(false)
58                        {
59                            output.push(text.chars().take(*col).collect());
60                            text = text.chars().skip(*col).collect();
61                        } else {
62                            let mut whitespace = chars
63                                .iter()
64                                .take(*col)
65                                .rposition(|c| c.is_whitespace())
66                                .unwrap_or(0);
67                            if whitespace == 0 {
68                                whitespace = *col
69                            }
70                            output.push(text.chars().take(whitespace).collect());
71                            text = text.chars().skip(whitespace).collect();
72                        }
73                    }
74                    output.push(text);
75                }
76                output.iter().map(|s| s.trim().to_string()).collect()
77            }
78            WrappingStrategy::AtColWithHyphen(col) => {
79                if *col == 0 {
80                    return split_str_to_col(input);
81                }
82                let mut output = vec![];
83                let lines = WrappingStrategy::None.wrap(input);
84                for mut line in lines {
85                    while line.chars().count() > *col {
86                        let text: String = line.chars().take(*col).collect();
87                        if text.chars().last().unwrap_or(' ').is_alphabetic()
88                            && line.chars().nth(*col).unwrap_or(' ').is_alphabetic()
89                        {
90                            output.push(format!("{text}-"));
91                        } else {
92                            output.push(text.clone());
93                        }
94                        line = line.chars().skip(*col).collect();
95                    }
96                    output.push(line)
97                }
98                output
99            }
100            WrappingStrategy::Cutoff(col) => {
101                if *col == 0 {
102                    return split_str_to_col(input);
103                }
104                input
105                    .split('\n')
106                    .map(|line| line.chars().take(*col).collect())
107                    .collect()
108            }
109            WrappingStrategy::Ellipsis(col) => {
110                if *col == 0 {
111                    return split_str_to_col(input);
112                }
113                input
114                    .split('\n')
115                    .map(|line| {
116                        if line.chars().count() >= *col {
117                            format!("{}…", line.chars().take(*col).collect::<String>())
118                        } else {
119                            line.to_string()
120                        }
121                    })
122                    .collect()
123            }
124        }
125    }
126}
127
128fn split_str_to_col(str: &str) -> Vec<String> {
129    str.chars()
130        .filter(|c| !c.is_whitespace())
131        .map(|c| c.to_string())
132        .collect()
133}
134
135#[cfg(test)]
136mod test {
137    use super::*;
138    use WrappingStrategy::*;
139
140    fn c(list: &[&str]) -> Vec<String> {
141        list.iter().map(|s| s.to_string()).collect()
142    }
143
144    #[test]
145    fn none() {
146        assert_eq!(None.wrap("this is a test"), c(&["this is a test"]));
147        assert_eq!(None.wrap("this is\na test"), c(&["this is", "a test"]));
148        assert_eq!(None.wrap("this is \n a test"), c(&["this is ", " a test"]));
149    }
150
151    #[test]
152    fn at_col() {
153        assert_eq!(
154            AtCol(4).wrap("some words, and some are longer"),
155            c(&["some", " wor", "ds, ", "and ", "some", " are", " lon", "ger"])
156        );
157
158        assert_eq!(
159            AtCol(5).wrap("Split Here\nBut mid word\nhere"),
160            c(&["Split", " Here", "But m", "id wo", "rd", "here"])
161        )
162    }
163
164    #[test]
165    fn at_col_hyphen() {
166        assert_eq!(
167            AtColWithHyphen(4).wrap("some words, and some are longer"),
168            c(&["some", " wor-", "ds, ", "and ", "some", " are", " lon-", "ger"])
169        );
170        assert_eq!(
171            AtColWithHyphen(4).wrap("smol large massive"),
172            c(&["smol", " lar-", "ge m-", "assi-", "ve"])
173        );
174        assert_eq!(
175            AtColWithHyphen(4).wrap("smol large\nmassive"),
176            c(&["smol", " lar-", "ge", "mass-", "ive"])
177        );
178    }
179
180    #[test]
181    fn space_before_col() {
182        assert_eq!(
183            SpaceBeforeCol(4).wrap("some words, and some are longer"),
184            c(&["some", "wor", "ds,", "and", "some", "are", "lon", "ger"])
185        );
186        assert_eq!(
187            SpaceBeforeCol(8).wrap("some words, and some are longer"),
188            c(&["some", "words,", "and some", "are", "longer"])
189        );
190    }
191
192    #[test]
193    fn cutoff() {
194        assert_eq!(Cutoff(30).wrap("short test"), c(&["short test"]));
195        assert_eq!(Cutoff(10).wrap("longer test string"), c(&["longer tes"]));
196        assert_eq!(
197            Cutoff(10).wrap("longer\ntest string"),
198            c(&["longer", "test strin"])
199        );
200    }
201
202    #[test]
203    fn ellipsis() {
204        assert_eq!(Ellipsis(30).wrap("short test"), c(&["short test"]));
205        assert_eq!(Ellipsis(10).wrap("longer test string"), c(&["longer tes…"]));
206        assert_eq!(
207            Ellipsis(10).wrap("longer\ntest string"),
208            c(&["longer", "test strin…"])
209        );
210    }
211}