1pub const CHAR_W: f32 = 6.0;
9pub const LINE_H: f32 = 9.0;
11pub const LINE_GAP: f32 = 1.0;
13
14pub fn char_width(c: char) -> f32 {
17 match c {
18 ' ' => 4.0,
19 '!' | ',' | '.' | ':' | ';' | '|' | '\'' => 2.0,
20 'i' => 2.0,
21 'l' => 3.0,
22 '`' => 3.0,
23 '(' | ')' | '*' | '<' | '>' | '{' | '}' => 5.0,
24 'f' | 'k' => 5.0,
25 '"' => 5.0,
26 't' | '[' | ']' | 'I' => 4.0,
27 '@' | '~' => 7.0,
28 c if c.is_ascii() => 6.0,
29 _ => 6.0, }
31}
32
33pub fn str_width(s: &str, font_scale: f32) -> f32 {
35 s.chars().map(char_width).sum::<f32>() * font_scale
36}
37
38pub fn wrap_text(text: &str, max_w: f32, font_scale: f32) -> Vec<String> {
41 if font_scale <= 0.0 || max_w <= 0.0 {
42 return vec![text.to_owned()];
43 }
44 let space_w = char_width(' ') * font_scale;
45 let mut lines: Vec<String> = Vec::new();
46 let mut cur = String::new();
47 let mut cur_w = 0.0f32;
48
49 for word in text.split(' ') {
50 if word.is_empty() {
51 continue;
52 }
53 let word_w = str_width(word, font_scale);
54
55 if cur_w == 0.0 {
56 append_word(
57 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale,
58 );
59 } else if cur_w + space_w + word_w <= max_w {
60 cur.push(' ');
61 cur.push_str(word);
62 cur_w += space_w + word_w;
63 } else {
64 lines.push(std::mem::take(&mut cur));
65 cur_w = 0.0;
66 append_word(
67 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale,
68 );
69 }
70 }
71 if cur_w > 0.0 || lines.is_empty() {
72 lines.push(cur);
73 }
74 lines
75}
76
77fn append_word(
79 lines: &mut Vec<String>,
80 cur: &mut String,
81 cur_w: &mut f32,
82 word: &str,
83 word_w: f32,
84 max_w: f32,
85 font_scale: f32,
86) {
87 if word_w <= max_w {
88 cur.push_str(word);
89 *cur_w = word_w;
90 return;
91 }
92 let mut line = String::new();
94 let mut w = 0.0f32;
95 for ch in word.chars() {
96 let cw = char_width(ch) * font_scale;
97 if w + cw > max_w && !line.is_empty() {
98 lines.push(std::mem::take(&mut line));
99 w = 0.0;
100 }
101 line.push(ch);
102 w += cw;
103 }
104 *cur = line;
105 *cur_w = w;
106}
107
108pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
110 wrap_text(text, max_w, font_scale).len().max(1)
111}
112
113pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
115 let n = line_count(text, max_w, font_scale);
116 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
117}
118
119pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
122 let all_lines: Vec<String> = text
123 .split('\n')
124 .flat_map(|para| {
125 if para.is_empty() {
126 vec![String::new()]
127 } else {
128 wrap_text(para, max_w, font_scale)
129 }
130 })
131 .collect();
132
133 let line_h = LINE_H * font_scale + LINE_GAP;
134 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
135 let per_page = per_page.max(1);
136
137 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
138}