1use std::collections::HashMap;
11use std::sync::Mutex;
12
13use yog_gfx::{GfxContext, TextWidthFn};
14
15pub const CHAR_W: f32 = 6.0;
18pub const LINE_H: f32 = 9.0;
20pub const LINE_GAP: f32 = 1.0;
22pub const DEFAULT_FONT: &str = "minecraft:default";
24
25pub fn char_width(c: char) -> f32 {
28 match c {
29 ' ' => 4.0,
30 '!' | ',' | '.' | ':' | ';' | '|' | '\'' => 2.0,
31 'i' => 2.0,
32 'l' => 3.0,
33 '`' => 3.0,
34 '(' | ')' | '*' | '<' | '>' | '{' | '}' => 5.0,
35 'f' | 'k' => 5.0,
36 '"' => 5.0,
37 't' | '[' | ']' | 'I' => 4.0,
38 '@' | '~' => 7.0,
39 c if c.is_ascii() => 6.0,
40 _ => 6.0, }
42}
43
44pub fn is_default_font(font: &str) -> bool {
46 font.is_empty() || font == DEFAULT_FONT
47}
48
49struct HostMetrics {
50 width: Option<TextWidthFn>,
51 cache: Option<HashMap<(String, char), f32>>,
54}
55
56static HOST: Mutex<HostMetrics> = Mutex::new(HostMetrics {
57 width: None,
58 cache: None,
59});
60
61pub fn use_host_metrics(ctx: &GfxContext) {
65 if let Ok(mut h) = HOST.lock() {
66 h.width = Some(ctx.text_width_fn());
67 }
68}
69
70pub fn char_width_in(c: char, font: &str) -> f32 {
73 if is_default_font(font) {
74 return char_width(c);
75 }
76 let Ok(mut h) = HOST.lock() else {
77 return char_width(c);
78 };
79 let Some(width) = h.width else {
80 return char_width(c);
81 };
82 let cache = h.cache.get_or_insert_with(HashMap::new);
83 if let Some(w) = cache.get(&(font.to_owned(), c)) {
84 return *w;
85 }
86 let mut buf = [0u8; 4];
87 let s: &str = c.encode_utf8(&mut buf);
88 let w = unsafe { width(yog_abi::YogStr::from_str(s), yog_abi::YogStr::from_str(font)) };
89 let cache = h.cache.get_or_insert_with(HashMap::new);
90 if cache.len() > 8192 {
91 cache.clear();
92 }
93 cache.insert((font.to_owned(), c), w);
94 w
95}
96
97pub fn str_width(s: &str, font_scale: f32) -> f32 {
99 s.chars().map(char_width).sum::<f32>() * font_scale
100}
101
102pub fn str_width_in(s: &str, font_scale: f32, font: &str) -> f32 {
104 if is_default_font(font) {
105 return str_width(s, font_scale);
106 }
107 s.chars().map(|c| char_width_in(c, font)).sum::<f32>() * font_scale
108}
109
110pub fn wrap_text(text: &str, max_w: f32, font_scale: f32) -> Vec<String> {
113 wrap_text_in(text, max_w, font_scale, "")
114}
115
116pub fn wrap_text_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> Vec<String> {
118 if font_scale <= 0.0 || max_w <= 0.0 {
119 return vec![text.to_owned()];
120 }
121 let space_w = char_width_in(' ', font) * font_scale;
122 let mut lines: Vec<String> = Vec::new();
123 let mut cur = String::new();
124 let mut cur_w = 0.0f32;
125
126 for word in text.split(' ') {
127 if word.is_empty() {
128 continue;
129 }
130 let word_w = str_width_in(word, font_scale, font);
131
132 if cur_w == 0.0 {
133 append_word(
134 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale, font,
135 );
136 } else if cur_w + space_w + word_w <= max_w {
137 cur.push(' ');
138 cur.push_str(word);
139 cur_w += space_w + word_w;
140 } else {
141 lines.push(std::mem::take(&mut cur));
142 cur_w = 0.0;
143 append_word(
144 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale, font,
145 );
146 }
147 }
148 if cur_w > 0.0 || lines.is_empty() {
149 lines.push(cur);
150 }
151 lines
152}
153
154#[allow(clippy::too_many_arguments)]
156fn append_word(
157 lines: &mut Vec<String>,
158 cur: &mut String,
159 cur_w: &mut f32,
160 word: &str,
161 word_w: f32,
162 max_w: f32,
163 font_scale: f32,
164 font: &str,
165) {
166 if word_w <= max_w {
167 cur.push_str(word);
168 *cur_w = word_w;
169 return;
170 }
171 let mut line = String::new();
173 let mut w = 0.0f32;
174 for ch in word.chars() {
175 let cw = char_width_in(ch, font) * font_scale;
176 if w + cw > max_w && !line.is_empty() {
177 lines.push(std::mem::take(&mut line));
178 w = 0.0;
179 }
180 line.push(ch);
181 w += cw;
182 }
183 *cur = line;
184 *cur_w = w;
185}
186
187pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
189 wrap_text(text, max_w, font_scale).len().max(1)
190}
191
192pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
194 text_height_in(text, max_w, font_scale, "")
195}
196
197pub fn text_height_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> f32 {
199 let n = wrap_text_in(text, max_w, font_scale, font).len().max(1);
200 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
201}
202
203pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
206 let all_lines: Vec<String> = text
207 .split('\n')
208 .flat_map(|para| {
209 if para.is_empty() {
210 vec![String::new()]
211 } else {
212 wrap_text(para, max_w, font_scale)
213 }
214 })
215 .collect();
216
217 let line_h = LINE_H * font_scale + LINE_GAP;
218 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
219 let per_page = per_page.max(1);
220
221 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
222}