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() { continue; }
51 let word_w = str_width(word, font_scale);
52
53 if cur_w == 0.0 {
54 append_word(&mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale);
55 } else if cur_w + space_w + word_w <= max_w {
56 cur.push(' ');
57 cur.push_str(word);
58 cur_w += space_w + word_w;
59 } else {
60 lines.push(std::mem::take(&mut cur));
61 cur_w = 0.0;
62 append_word(&mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale);
63 }
64 }
65 if cur_w > 0.0 || lines.is_empty() {
66 lines.push(cur);
67 }
68 lines
69}
70
71fn append_word(
73 lines: &mut Vec<String>, cur: &mut String, cur_w: &mut f32,
74 word: &str, word_w: f32, max_w: f32, font_scale: f32,
75) {
76 if word_w <= max_w {
77 cur.push_str(word);
78 *cur_w = word_w;
79 return;
80 }
81 let mut line = String::new();
83 let mut w = 0.0f32;
84 for ch in word.chars() {
85 let cw = char_width(ch) * font_scale;
86 if w + cw > max_w && !line.is_empty() {
87 lines.push(std::mem::take(&mut line));
88 w = 0.0;
89 }
90 line.push(ch);
91 w += cw;
92 }
93 *cur = line;
94 *cur_w = w;
95}
96
97pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
99 wrap_text(text, max_w, font_scale).len().max(1)
100}
101
102pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
104 let n = line_count(text, max_w, font_scale);
105 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
106}
107
108pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
111 let all_lines: Vec<String> = text.split('\n').flat_map(|para| {
112 if para.is_empty() { vec![String::new()] } else { wrap_text(para, max_w, font_scale) }
113 }).collect();
114
115 let line_h = LINE_H * font_scale + LINE_GAP;
116 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
117 let per_page = per_page.max(1);
118
119 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
120}