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) {
190 for para in text.split('\n') {
191 wrap_paragraph(para, max_w, font_scale, font, out);
192 }
193}
194
195fn wrap_paragraph(text: &str, max_w: f32, font_scale: f32, font: &str, out: &mut impl WrapSink) {
198 let space_w = char_width_in(' ', font) * font_scale;
199 let mut lines = 0usize;
200 let mut cur_w = 0.0f32;
201
202 for word in text.split(' ') {
203 if word.is_empty() {
204 continue;
205 }
206 let word_w = str_width_in(word, font_scale, font);
207 if cur_w > 0.0 && cur_w + space_w + word_w <= max_w {
208 out.push_char(' ');
209 out.push_str(word);
210 cur_w += space_w + word_w;
211 continue;
212 }
213 if cur_w > 0.0 {
214 out.end_line();
215 lines += 1;
216 }
217 if word_w <= max_w {
218 out.push_str(word);
219 cur_w = word_w;
220 continue;
221 }
222 let (mut w, mut line_empty) = (0.0f32, true);
224 for ch in word.chars() {
225 let cw = char_width_in(ch, font) * font_scale;
226 if w + cw > max_w && !line_empty {
227 out.end_line();
228 lines += 1;
229 w = 0.0;
230 }
231 out.push_char(ch);
232 line_empty = false;
233 w += cw;
234 }
235 cur_w = w;
236 }
237 if cur_w > 0.0 || lines == 0 {
238 out.end_line();
239 }
240}
241
242pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
244 line_count_in(text, max_w, font_scale, "")
245}
246
247fn line_count_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> usize {
249 if font_scale <= 0.0 || max_w <= 0.0 {
250 return 1;
251 }
252 let mut count = LineCount::default();
253 wrap(text, max_w, font_scale, font, &mut count);
254 count.0
255}
256
257pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
259 text_height_in(text, max_w, font_scale, "")
260}
261
262pub fn text_height_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> f32 {
264 let n = line_count_in(text, max_w, font_scale, font);
265 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
266}
267
268pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
271 let all_lines: Vec<String> = text
272 .split('\n')
273 .flat_map(|para| {
274 if para.is_empty() {
275 vec![String::new()]
276 } else {
277 wrap_text(para, max_w, font_scale)
278 }
279 })
280 .collect();
281
282 let line_h = LINE_H * font_scale + LINE_GAP;
283 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
284 let per_page = per_page.max(1);
285
286 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn wraps_words_and_hard_breaks_long_ones() {
295 assert_eq!(wrap_text("aaa bbb ccc", 40.0, 1.0), ["aaa bbb", "ccc"]);
296 assert_eq!(wrap_text("", 40.0, 1.0), [""]);
297 assert_eq!(wrap_text("aaa\nbbb", 400.0, 1.0), ["aaa", "bbb"]);
298 assert_eq!(wrap_text("aaa\n\nbbb", 400.0, 1.0), ["aaa", "", "bbb"]);
299 assert_eq!(wrap_text("mmmmmmmmmm", 30.0, 1.0), ["mmmmm", "mmmmm"]);
300 assert_eq!(
301 wrap_text("a mmmmmmmmmm b", 30.0, 1.0),
302 ["a", "mmmmm", "mmmmm", "b"]
303 );
304 }
305
306 #[test]
307 fn line_count_matches_the_wrapped_lines() {
308 let text = "The quick brown fox jumps over a supercalifragilistic dog";
309 for w in [0.0, 5.0, 20.0, 50.0, 116.0, 1000.0] {
310 for scale in [0.0, 0.5, 1.0, 2.0] {
311 let lines = if scale <= 0.0 || w <= 0.0 {
312 1
313 } else {
314 wrap_text(text, w, scale).len()
315 };
316 assert_eq!(line_count(text, w, scale), lines, "w={w} scale={scale}");
317 }
318 }
319 }
320}