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 {
89 width(
90 yog_abi::YogStr::from_str(s),
91 yog_abi::YogStr::from_str(font),
92 )
93 };
94 let cache = h.cache.get_or_insert_with(HashMap::new);
95 if cache.len() > 8192 {
96 cache.clear();
97 }
98 cache.insert((font.to_owned(), c), w);
99 w
100}
101
102pub fn str_width(s: &str, font_scale: f32) -> f32 {
104 s.chars().map(char_width).sum::<f32>() * font_scale
105}
106
107pub fn str_width_in(s: &str, font_scale: f32, font: &str) -> f32 {
109 if is_default_font(font) {
110 return str_width(s, font_scale);
111 }
112 s.chars().map(|c| char_width_in(c, font)).sum::<f32>() * font_scale
113}
114
115pub fn wrap_text(text: &str, max_w: f32, font_scale: f32) -> Vec<String> {
118 wrap_text_in(text, max_w, font_scale, "")
119}
120
121pub fn wrap_text_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> Vec<String> {
123 if font_scale <= 0.0 || max_w <= 0.0 {
124 return vec![text.to_owned()];
125 }
126 let space_w = char_width_in(' ', font) * font_scale;
127 let mut lines: Vec<String> = Vec::new();
128 let mut cur = String::new();
129 let mut cur_w = 0.0f32;
130
131 for word in text.split(' ') {
132 if word.is_empty() {
133 continue;
134 }
135 let word_w = str_width_in(word, font_scale, font);
136
137 if cur_w == 0.0 {
138 append_word(
139 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale, font,
140 );
141 } else if cur_w + space_w + word_w <= max_w {
142 cur.push(' ');
143 cur.push_str(word);
144 cur_w += space_w + word_w;
145 } else {
146 lines.push(std::mem::take(&mut cur));
147 cur_w = 0.0;
148 append_word(
149 &mut lines, &mut cur, &mut cur_w, word, word_w, max_w, font_scale, font,
150 );
151 }
152 }
153 if cur_w > 0.0 || lines.is_empty() {
154 lines.push(cur);
155 }
156 lines
157}
158
159#[allow(clippy::too_many_arguments)]
161fn append_word(
162 lines: &mut Vec<String>,
163 cur: &mut String,
164 cur_w: &mut f32,
165 word: &str,
166 word_w: f32,
167 max_w: f32,
168 font_scale: f32,
169 font: &str,
170) {
171 if word_w <= max_w {
172 cur.push_str(word);
173 *cur_w = word_w;
174 return;
175 }
176 let mut line = String::new();
178 let mut w = 0.0f32;
179 for ch in word.chars() {
180 let cw = char_width_in(ch, font) * font_scale;
181 if w + cw > max_w && !line.is_empty() {
182 lines.push(std::mem::take(&mut line));
183 w = 0.0;
184 }
185 line.push(ch);
186 w += cw;
187 }
188 *cur = line;
189 *cur_w = w;
190}
191
192pub fn line_count(text: &str, max_w: f32, font_scale: f32) -> usize {
194 wrap_text(text, max_w, font_scale).len().max(1)
195}
196
197pub fn text_height(text: &str, max_w: f32, font_scale: f32) -> f32 {
199 text_height_in(text, max_w, font_scale, "")
200}
201
202pub fn text_height_in(text: &str, max_w: f32, font_scale: f32, font: &str) -> f32 {
204 let n = wrap_text_in(text, max_w, font_scale, font).len().max(1);
205 n as f32 * LINE_H * font_scale + (n.saturating_sub(1)) as f32 * LINE_GAP
206}
207
208pub fn paginate_text(text: &str, max_w: f32, max_h: f32, font_scale: f32) -> Vec<Vec<String>> {
211 let all_lines: Vec<String> = text
212 .split('\n')
213 .flat_map(|para| {
214 if para.is_empty() {
215 vec![String::new()]
216 } else {
217 wrap_text(para, max_w, font_scale)
218 }
219 })
220 .collect();
221
222 let line_h = LINE_H * font_scale + LINE_GAP;
223 let per_page = ((max_h + LINE_GAP) / line_h).floor() as usize;
224 let per_page = per_page.max(1);
225
226 all_lines.chunks(per_page).map(|c| c.to_vec()).collect()
227}