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
49const ADVANCE_CACHE_CAP: usize = 8192;
51
52struct HostMetrics {
53 width: Option<TextWidthFn>,
54 cache: Option<HashMap<String, HashMap<char, f32>>>,
57 cached: usize,
59}
60
61impl HostMetrics {
62 fn advance(&mut self, c: char, font: &str) -> Option<f32> {
65 let width = self.width?;
66 let cache = self.cache.get_or_insert_with(HashMap::new);
67 if let Some(w) = cache.get(font).and_then(|m| m.get(&c)) {
68 return Some(*w);
69 }
70 let mut buf = [0u8; 4];
71 let s: &str = c.encode_utf8(&mut buf);
72 let w = unsafe {
73 width(
74 yog_abi::YogStr::from_str(s),
75 yog_abi::YogStr::from_str(font),
76 )
77 };
78 if self.cached > ADVANCE_CACHE_CAP {
79 cache.clear();
80 self.cached = 0;
81 }
82 cache.entry(font.to_owned()).or_default().insert(c, w);
83 self.cached += 1;
84 Some(w)
85 }
86}
87
88static HOST: Mutex<HostMetrics> = Mutex::new(HostMetrics {
89 width: None,
90 cache: None,
91 cached: 0,
92});
93
94pub fn use_host_metrics(ctx: &GfxContext) {
98 if let Ok(mut h) = HOST.lock() {
99 h.width = Some(ctx.text_width_fn());
100 }
101}
102
103pub fn char_width_in(c: char, font: &str) -> f32 {
106 if is_default_font(font) {
107 return char_width(c);
108 }
109 HOST.lock()
110 .ok()
111 .and_then(|mut h| h.advance(c, font))
112 .unwrap_or_else(|| char_width(c))
113}
114
115pub fn str_width(s: &str, font_scale: f32) -> f32 {
117 s.chars().map(char_width).sum::<f32>() * font_scale
118}
119
120pub fn str_width_in(s: &str, font_scale: f32, font: &str) -> f32 {
122 if is_default_font(font) {
123 return str_width(s, font_scale);
124 }
125 let Ok(mut h) = HOST.lock() else {
126 return str_width(s, font_scale);
127 };
128 s.chars()
129 .map(|c| h.advance(c, font).unwrap_or_else(|| char_width(c)))
130 .sum::<f32>()
131 * font_scale
132}
133
134pub fn wrap_text(text: &str, max_w: f32, font_scale: f32) -> Vec<String> {
137 wrap_text_in(text, max_w, font_scale, "")
138}
139
140pub fn wrap_text_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> Vec<String> {
142 if font_scale <= 0.0 || max_w <= 0.0 {
143 return vec![text.to_owned()];
144 }
145 let mut lines = Lines::default();
146 wrap(text, max_w, font_scale, font, &mut lines);
147 lines.0
148}
149
150trait WrapSink {
153 fn push_str(&mut self, s: &str);
154 fn push_char(&mut self, c: char);
155 fn end_line(&mut self);
157}
158
159#[derive(Default)]
161struct Lines(Vec<String>, String);
162
163impl WrapSink for Lines {
164 fn push_str(&mut self, s: &str) {
165 self.1.push_str(s);
166 }
167 fn push_char(&mut self, c: char) {
168 self.1.push(c);
169 }
170 fn end_line(&mut self) {
171 self.0.push(std::mem::take(&mut self.1));
172 }
173}
174
175#[derive(Default)]
177struct LineCount(usize);
178
179impl WrapSink for LineCount {
180 fn push_str(&mut self, _: &str) {}
181 fn push_char(&mut self, _: char) {}
182 fn end_line(&mut self) {
183 self.0 += 1;
184 }
185}
186
187fn wrap(text: &str, max_w: f32, font_scale: f32, font: &str, out: &mut impl WrapSink) {
191 let space_w = char_width_in(' ', font) * font_scale;
192 let mut lines = 0usize;
193 let mut cur_w = 0.0f32;
194
195 for word in text.split(' ') {
196 if word.is_empty() {
197 continue;
198 }
199 let word_w = str_width_in(word, font_scale, font);
200 if cur_w > 0.0 && cur_w + space_w + word_w <= max_w {
201 out.push_char(' ');
202 out.push_str(word);
203 cur_w += space_w + word_w;
204 continue;
205 }
206 if cur_w > 0.0 {
207 out.end_line();
208 lines += 1;
209 }
210 if word_w <= max_w {
211 out.push_str(word);
212 cur_w = word_w;
213 continue;
214 }
215 let (mut w, mut line_empty) = (0.0f32, true);
217 for ch in word.chars() {
218 let cw = char_width_in(ch, font) * font_scale;
219 if w + cw > max_w && !line_empty {
220 out.end_line();
221 lines += 1;
222 w = 0.0;
223 }
224 out.push_char(ch);
225 line_empty = false;
226 w += cw;
227 }
228 cur_w = w;
229 }
230 if cur_w > 0.0 || lines == 0 {
231 out.end_line();
232 }
233}
234
235pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
237 line_count_in(text, max_w, font_scale, "")
238}
239
240fn line_count_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> usize {
242 if font_scale <= 0.0 || max_w <= 0.0 {
243 return 1;
244 }
245 let mut count = LineCount::default();
246 wrap(text, max_w, font_scale, font, &mut count);
247 count.0
248}
249
250pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
252 text_height_in(text, max_w, font_scale, "")
253}
254
255pub fn text_height_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> f32 {
257 let n = line_count_in(text, max_w, font_scale, font);
258 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
259}
260
261pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
264 let all_lines: Vec<String> = text
265 .split('\n')
266 .flat_map(|para| {
267 if para.is_empty() {
268 vec![String::new()]
269 } else {
270 wrap_text(para, max_w, font_scale)
271 }
272 })
273 .collect();
274
275 let line_h = LINE_H * font_scale + LINE_GAP;
276 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
277 let per_page = per_page.max(1);
278
279 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn wraps_words_and_hard_breaks_long_ones() {
288 assert_eq!(wrap_text("aaa bbb ccc", 40.0, 1.0), ["aaa bbb", "ccc"]);
289 assert_eq!(wrap_text("", 40.0, 1.0), [""]);
290 assert_eq!(wrap_text("mmmmmmmmmm", 30.0, 1.0), ["mmmmm", "mmmmm"]);
291 assert_eq!(
292 wrap_text("a mmmmmmmmmm b", 30.0, 1.0),
293 ["a", "mmmmm", "mmmmm", "b"]
294 );
295 }
296
297 #[test]
298 fn line_count_matches_the_wrapped_lines() {
299 let text = "The quick brown fox jumps over a supercalifragilistic dog";
300 for w in [0.0, 5.0, 20.0, 50.0, 116.0, 1000.0] {
301 for scale in [0.0, 0.5, 1.0, 2.0] {
302 let lines = if scale <= 0.0 || w <= 0.0 {
303 1
304 } else {
305 wrap_text(text, w, scale).len()
306 };
307 assert_eq!(line_count(text, w, scale), lines, "w={w} scale={scale}");
308 }
309 }
310 }
311}