1use super::{
6 DeterministicTextMeasurer, TextMeasurer, TextStyle, WrapMode, estimate_char_width_em,
7 estimate_line_width_px,
8};
9
10pub fn ceil_to_1_64_px(v: f64) -> f64 {
11 if !(v.is_finite() && v >= 0.0) {
12 return 0.0;
13 }
14 let x = v * 64.0;
17 let r = x.round();
18 if (x - r).abs() < 1e-4 {
19 return r / 64.0;
20 }
21 ((x) - 1e-5).ceil() / 64.0
22}
23
24pub fn round_to_1_64_px(v: f64) -> f64 {
25 if !(v.is_finite() && v >= 0.0) {
26 return 0.0;
27 }
28 let x = v * 64.0;
29 let r = (x + 0.5).floor();
30 r / 64.0
31}
32
33pub fn wrap_text_lines_px(
34 text: &str,
35 style: &TextStyle,
36 max_width_px: Option<f64>,
37 wrap_mode: WrapMode,
38) -> Vec<String> {
39 let font_size = style.font_size.max(1.0);
40 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
41 let break_long_words = wrap_mode == WrapMode::SvgLike;
42
43 fn split_token_to_width_px(tok: &str, max_width_px: f64, font_size: f64) -> (String, String) {
44 let max_em = max_width_px / font_size;
45 let mut em = 0.0;
46 let chars = tok.chars().collect::<Vec<_>>();
47 let mut split_at = 0usize;
48 for (idx, ch) in chars.iter().enumerate() {
49 em += estimate_char_width_em(*ch);
50 if em > max_em && idx > 0 {
51 break;
52 }
53 split_at = idx + 1;
54 if em >= max_em {
55 break;
56 }
57 }
58 if split_at == 0 {
59 split_at = 1.min(chars.len());
60 }
61 let head = chars.iter().take(split_at).collect::<String>();
62 let tail = chars.iter().skip(split_at).collect::<String>();
63 (head, tail)
64 }
65
66 fn wrap_line_to_width_px(
67 line: &str,
68 max_width_px: f64,
69 font_size: f64,
70 break_long_words: bool,
71 ) -> Vec<String> {
72 let mut tokens =
73 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
74 let mut out: Vec<String> = Vec::new();
75 let mut cur = String::new();
76
77 while let Some(tok) = tokens.pop_front() {
78 if cur.is_empty() && tok == " " {
79 continue;
80 }
81
82 let candidate = format!("{cur}{tok}");
83 let candidate_trimmed = candidate.trim_end();
84 if estimate_line_width_px(candidate_trimmed, font_size) <= max_width_px {
85 cur = candidate;
86 continue;
87 }
88
89 if !cur.trim().is_empty() {
90 out.push(cur.trim_end().to_string());
91 cur.clear();
92 tokens.push_front(tok);
93 continue;
94 }
95
96 if tok == " " {
97 continue;
98 }
99
100 if !break_long_words {
101 out.push(tok);
102 } else {
103 let (head, tail) = split_token_to_width_px(&tok, max_width_px, font_size);
104 out.push(head);
105 if !tail.is_empty() {
106 tokens.push_front(tail);
107 }
108 }
109 }
110
111 if !cur.trim().is_empty() {
112 out.push(cur.trim_end().to_string());
113 }
114
115 if out.is_empty() {
116 vec!["".to_string()]
117 } else {
118 out
119 }
120 }
121
122 let mut lines: Vec<String> = Vec::new();
123 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
124 if let Some(w) = max_width_px {
125 lines.extend(wrap_line_to_width_px(&line, w, font_size, break_long_words));
126 } else {
127 lines.push(line);
128 }
129 }
130
131 if lines.is_empty() {
132 vec!["".to_string()]
133 } else {
134 lines
135 }
136}
137
138pub fn wrap_text_lines_measurer(
144 text: &str,
145 measurer: &dyn TextMeasurer,
146 style: &TextStyle,
147 max_width_px: Option<f64>,
148) -> Vec<String> {
149 fn wrap_line(
150 line: &str,
151 measurer: &dyn TextMeasurer,
152 style: &TextStyle,
153 max_width_px: f64,
154 ) -> Vec<String> {
155 use std::collections::VecDeque;
156
157 if !max_width_px.is_finite() || max_width_px <= 0.0 {
158 return vec![line.to_string()];
159 }
160
161 let mut tokens = VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
162 let mut out: Vec<String> = Vec::new();
163 let mut cur = String::new();
164
165 while let Some(tok) = tokens.pop_front() {
166 if cur.is_empty() && tok == " " {
167 continue;
168 }
169
170 let candidate = format!("{cur}{tok}");
171 if measurer.measure(candidate.trim_end(), style).width <= max_width_px {
172 cur = candidate;
173 continue;
174 }
175
176 if !cur.trim().is_empty() {
177 out.push(cur.trim_end().to_string());
178 cur.clear();
179 tokens.push_front(tok);
180 continue;
181 }
182
183 if tok == " " {
184 continue;
185 }
186
187 let chars = tok.chars().collect::<Vec<_>>();
189 let mut cut = 1usize;
190 while cut < chars.len() {
191 let head: String = chars[..cut].iter().collect();
192 if measurer.measure(&head, style).width > max_width_px {
193 break;
194 }
195 cut += 1;
196 }
197 cut = cut.saturating_sub(1).max(1);
198 let head: String = chars[..cut].iter().collect();
199 let tail: String = chars[cut..].iter().collect();
200 out.push(head);
201 if !tail.is_empty() {
202 tokens.push_front(tail);
203 }
204 }
205
206 if !cur.trim().is_empty() {
207 out.push(cur.trim_end().to_string());
208 }
209
210 if out.is_empty() {
211 vec!["".to_string()]
212 } else {
213 out
214 }
215 }
216
217 let mut out: Vec<String> = Vec::new();
218 for line in split_html_br_lines(text) {
219 if let Some(w) = max_width_px {
220 out.extend(wrap_line(line, measurer, style, w));
221 } else {
222 out.push(line.to_string());
223 }
224 }
225 if out.is_empty() {
226 vec!["".to_string()]
227 } else {
228 out
229 }
230}
231
232pub fn split_html_br_lines(text: &str) -> Vec<&str> {
238 let b = text.as_bytes();
239 let mut parts: Vec<&str> = Vec::new();
240 let mut start = 0usize;
241 let mut i = 0usize;
242 while i + 3 < b.len() {
243 if b[i] != b'<' {
244 i += 1;
245 continue;
246 }
247 let b1 = b[i + 1];
248 let b2 = b[i + 2];
249 if !matches!(b1, b'b' | b'B') || !matches!(b2, b'r' | b'R') {
250 i += 1;
251 continue;
252 }
253 let mut j = i + 3;
254 while j < b.len() && matches!(b[j], b' ' | b'\t' | b'\r' | b'\n') {
255 j += 1;
256 }
257 if j < b.len() && b[j] == b'/' {
258 j += 1;
259 }
260 if j < b.len() && b[j] == b'>' {
261 parts.push(&text[start..i]);
262 start = j + 1;
263 i = start;
264 continue;
265 }
266 i += 1;
267 }
268 parts.push(&text[start..]);
269 parts
270}
271
272pub fn wrap_label_like_mermaid_lines(
277 label: &str,
278 measurer: &dyn TextMeasurer,
279 style: &TextStyle,
280 max_width_px: f64,
281) -> Vec<String> {
282 if label.is_empty() {
283 return Vec::new();
284 }
285 if !max_width_px.is_finite() || max_width_px <= 0.0 {
286 return vec![label.to_string()];
287 }
288
289 if split_html_br_lines(label).len() > 1 {
291 return split_html_br_lines(label)
292 .into_iter()
293 .map(|s| s.to_string())
294 .collect();
295 }
296
297 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
298 measurer
300 .measure_svg_simple_text_bbox_width_px(s, style)
301 .round()
302 }
303
304 fn break_string_like_mermaid(
305 word: &str,
306 max_width_px: f64,
307 measurer: &dyn TextMeasurer,
308 style: &TextStyle,
309 ) -> (Vec<String>, String) {
310 let chars: Vec<char> = word.chars().collect();
311 let mut lines: Vec<String> = Vec::new();
312 let mut current = String::new();
313 for (idx, ch) in chars.iter().enumerate() {
314 let next_line = format!("{current}{ch}");
315 let line_w = w_px(measurer, style, &next_line);
316 if line_w >= max_width_px {
317 let is_last = idx + 1 == chars.len();
318 if is_last {
319 lines.push(next_line);
320 } else {
321 lines.push(format!("{next_line}-"));
322 }
323 current.clear();
324 } else {
325 current = next_line;
326 }
327 }
328 (lines, current)
329 }
330
331 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
333 if words.is_empty() {
334 return vec![label.to_string()];
335 }
336
337 let mut completed: Vec<String> = Vec::new();
338 let mut next_line = String::new();
339 for (idx, word) in words.iter().enumerate() {
340 let word_len = w_px(measurer, style, &format!("{word} "));
341 let next_len = w_px(measurer, style, &next_line);
342 if word_len > max_width_px {
343 let (hyphenated, remaining) =
344 break_string_like_mermaid(word, max_width_px, measurer, style);
345 completed.push(next_line.clone());
346 completed.extend(hyphenated);
347 next_line = remaining;
348 } else if next_len + word_len >= max_width_px {
349 completed.push(next_line.clone());
350 next_line = (*word).to_string();
351 } else if next_line.is_empty() {
352 next_line = (*word).to_string();
353 } else {
354 next_line.push(' ');
355 next_line.push_str(word);
356 }
357
358 let is_last = idx + 1 == words.len();
359 if is_last {
360 completed.push(next_line.clone());
361 }
362 }
363
364 completed.into_iter().filter(|l| !l.is_empty()).collect()
365}
366
367pub fn wrap_label_like_mermaid_lines_relaxed(
373 label: &str,
374 measurer: &dyn TextMeasurer,
375 style: &TextStyle,
376 max_width_px: f64,
377) -> Vec<String> {
378 if label.is_empty() {
379 return Vec::new();
380 }
381 if !max_width_px.is_finite() || max_width_px <= 0.0 {
382 return vec![label.to_string()];
383 }
384
385 if split_html_br_lines(label).len() > 1 {
386 return split_html_br_lines(label)
387 .into_iter()
388 .map(|s| s.to_string())
389 .collect();
390 }
391
392 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
393 measurer.measure(s, style).width.round()
394 }
395
396 fn break_string_like_mermaid(
397 word: &str,
398 max_width_px: f64,
399 measurer: &dyn TextMeasurer,
400 style: &TextStyle,
401 ) -> (Vec<String>, String) {
402 let chars: Vec<char> = word.chars().collect();
403 let mut lines: Vec<String> = Vec::new();
404 let mut current = String::new();
405 for (idx, ch) in chars.iter().enumerate() {
406 let next_line = format!("{current}{ch}");
407 let line_w = w_px(measurer, style, &next_line);
408 if line_w >= max_width_px {
409 let is_last = idx + 1 == chars.len();
410 if is_last {
411 lines.push(next_line);
412 } else {
413 lines.push(format!("{next_line}-"));
414 }
415 current.clear();
416 } else {
417 current = next_line;
418 }
419 }
420 (lines, current)
421 }
422
423 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
424 if words.is_empty() {
425 return vec![label.to_string()];
426 }
427
428 let mut completed: Vec<String> = Vec::new();
429 let mut next_line = String::new();
430 for (idx, word) in words.iter().enumerate() {
431 let word_len = w_px(measurer, style, &format!("{word} "));
432 let next_len = w_px(measurer, style, &next_line);
433 if word_len > max_width_px {
434 let (hyphenated, remaining) =
435 break_string_like_mermaid(word, max_width_px, measurer, style);
436 completed.push(next_line.clone());
437 completed.extend(hyphenated);
438 next_line = remaining;
439 } else if next_len + word_len >= max_width_px {
440 completed.push(next_line.clone());
441 next_line = (*word).to_string();
442 } else if next_line.is_empty() {
443 next_line = (*word).to_string();
444 } else {
445 next_line.push(' ');
446 next_line.push_str(word);
447 }
448
449 let is_last = idx + 1 == words.len();
450 if is_last {
451 completed.push(next_line.clone());
452 }
453 }
454
455 completed.into_iter().filter(|l| !l.is_empty()).collect()
456}
457
458pub fn wrap_label_like_mermaid_lines_floored_bbox(
464 label: &str,
465 measurer: &dyn TextMeasurer,
466 style: &TextStyle,
467 max_width_px: f64,
468) -> Vec<String> {
469 if label.is_empty() {
470 return Vec::new();
471 }
472 if !max_width_px.is_finite() || max_width_px <= 0.0 {
473 return vec![label.to_string()];
474 }
475
476 if split_html_br_lines(label).len() > 1 {
477 return split_html_br_lines(label)
478 .into_iter()
479 .map(|s| s.to_string())
480 .collect();
481 }
482
483 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
484 measurer
485 .measure_svg_simple_text_bbox_width_px(s, style)
486 .floor()
487 }
488
489 fn break_string_like_mermaid(
490 word: &str,
491 max_width_px: f64,
492 measurer: &dyn TextMeasurer,
493 style: &TextStyle,
494 ) -> (Vec<String>, String) {
495 let chars: Vec<char> = word.chars().collect();
496 let mut lines: Vec<String> = Vec::new();
497 let mut current = String::new();
498 for (idx, ch) in chars.iter().enumerate() {
499 let next_line = format!("{current}{ch}");
500 let line_w = w_px(measurer, style, &next_line);
501 if line_w >= max_width_px {
502 let is_last = idx + 1 == chars.len();
503 if is_last {
504 lines.push(next_line);
505 } else {
506 lines.push(format!("{next_line}-"));
507 }
508 current.clear();
509 } else {
510 current = next_line;
511 }
512 }
513 (lines, current)
514 }
515
516 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
517 if words.is_empty() {
518 return vec![label.to_string()];
519 }
520
521 let mut completed: Vec<String> = Vec::new();
522 let mut next_line = String::new();
523 for (idx, word) in words.iter().enumerate() {
524 let word_len = w_px(measurer, style, &format!("{word} "));
525 let next_len = w_px(measurer, style, &next_line);
526 if word_len > max_width_px {
527 let (hyphenated, remaining) =
528 break_string_like_mermaid(word, max_width_px, measurer, style);
529 completed.push(next_line.clone());
530 completed.extend(hyphenated);
531 next_line = remaining;
532 } else if next_len + word_len >= max_width_px {
533 completed.push(next_line.clone());
534 next_line = (*word).to_string();
535 } else if next_line.is_empty() {
536 next_line = (*word).to_string();
537 } else {
538 next_line.push(' ');
539 next_line.push_str(word);
540 }
541
542 let is_last = idx + 1 == words.len();
543 if is_last {
544 completed.push(next_line.clone());
545 }
546 }
547
548 completed.into_iter().filter(|l| !l.is_empty()).collect()
549}