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 round_to_1_64_px_ties_to_even(v: f64) -> f64 {
34 if !(v.is_finite() && v >= 0.0) {
35 return 0.0;
36 }
37 let x = v * 64.0;
38 let f = x.floor();
39 let frac = x - f;
40 let i = if frac < 0.5 {
41 f
42 } else if frac > 0.5 {
43 f + 1.0
44 } else {
45 let fi = f as i64;
46 if fi % 2 == 0 { f } else { f + 1.0 }
47 };
48 let out = i / 64.0;
49 if out == -0.0 { 0.0 } else { out }
50}
51
52pub fn wrap_text_lines_px(
53 text: &str,
54 style: &TextStyle,
55 max_width_px: Option<f64>,
56 wrap_mode: WrapMode,
57) -> Vec<String> {
58 let font_size = style.font_size.max(1.0);
59 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
60 let break_long_words = wrap_mode == WrapMode::SvgLike;
61
62 fn split_token_to_width_px(tok: &str, max_width_px: f64, font_size: f64) -> (String, String) {
63 let max_em = max_width_px / font_size;
64 let mut em = 0.0;
65 let chars = tok.chars().collect::<Vec<_>>();
66 let mut split_at = 0usize;
67 for (idx, ch) in chars.iter().enumerate() {
68 em += estimate_char_width_em(*ch);
69 if em > max_em && idx > 0 {
70 break;
71 }
72 split_at = idx + 1;
73 if em >= max_em {
74 break;
75 }
76 }
77 if split_at == 0 {
78 split_at = 1.min(chars.len());
79 }
80 let head = chars.iter().take(split_at).collect::<String>();
81 let tail = chars.iter().skip(split_at).collect::<String>();
82 (head, tail)
83 }
84
85 fn wrap_line_to_width_px(
86 line: &str,
87 max_width_px: f64,
88 font_size: f64,
89 break_long_words: bool,
90 ) -> Vec<String> {
91 let mut tokens =
92 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
93 let mut out: Vec<String> = Vec::new();
94 let mut cur = String::new();
95
96 while let Some(tok) = tokens.pop_front() {
97 if cur.is_empty() && tok == " " {
98 continue;
99 }
100
101 let candidate = format!("{cur}{tok}");
102 let candidate_trimmed = candidate.trim_end();
103 if estimate_line_width_px(candidate_trimmed, font_size) <= max_width_px {
104 cur = candidate;
105 continue;
106 }
107
108 if !cur.trim().is_empty() {
109 out.push(cur.trim_end().to_string());
110 cur.clear();
111 tokens.push_front(tok);
112 continue;
113 }
114
115 if tok == " " {
116 continue;
117 }
118
119 if !break_long_words {
120 out.push(tok);
121 } else {
122 let (head, tail) = split_token_to_width_px(&tok, max_width_px, font_size);
123 out.push(head);
124 if !tail.is_empty() {
125 tokens.push_front(tail);
126 }
127 }
128 }
129
130 if !cur.trim().is_empty() {
131 out.push(cur.trim_end().to_string());
132 }
133
134 if out.is_empty() {
135 vec!["".to_string()]
136 } else {
137 out
138 }
139 }
140
141 let mut lines: Vec<String> = Vec::new();
142 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
143 if let Some(w) = max_width_px {
144 lines.extend(wrap_line_to_width_px(&line, w, font_size, break_long_words));
145 } else {
146 lines.push(line);
147 }
148 }
149
150 if lines.is_empty() {
151 vec!["".to_string()]
152 } else {
153 lines
154 }
155}
156
157pub fn wrap_text_lines_measurer(
163 text: &str,
164 measurer: &dyn TextMeasurer,
165 style: &TextStyle,
166 max_width_px: Option<f64>,
167) -> Vec<String> {
168 fn wrap_line(
169 line: &str,
170 measurer: &dyn TextMeasurer,
171 style: &TextStyle,
172 max_width_px: f64,
173 ) -> Vec<String> {
174 use std::collections::VecDeque;
175
176 if !max_width_px.is_finite() || max_width_px <= 0.0 {
177 return vec![line.to_string()];
178 }
179
180 let mut tokens = VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
181 let mut out: Vec<String> = Vec::new();
182 let mut cur = String::new();
183
184 while let Some(tok) = tokens.pop_front() {
185 if cur.is_empty() && tok == " " {
186 continue;
187 }
188
189 let candidate = format!("{cur}{tok}");
190 if measurer.measure(candidate.trim_end(), style).width <= max_width_px {
191 cur = candidate;
192 continue;
193 }
194
195 if !cur.trim().is_empty() {
196 out.push(cur.trim_end().to_string());
197 cur.clear();
198 tokens.push_front(tok);
199 continue;
200 }
201
202 if tok == " " {
203 continue;
204 }
205
206 let chars = tok.chars().collect::<Vec<_>>();
208 let mut cut = 1usize;
209 while cut < chars.len() {
210 let head: String = chars[..cut].iter().collect();
211 if measurer.measure(&head, style).width > max_width_px {
212 break;
213 }
214 cut += 1;
215 }
216 cut = cut.saturating_sub(1).max(1);
217 let head: String = chars[..cut].iter().collect();
218 let tail: String = chars[cut..].iter().collect();
219 out.push(head);
220 if !tail.is_empty() {
221 tokens.push_front(tail);
222 }
223 }
224
225 if !cur.trim().is_empty() {
226 out.push(cur.trim_end().to_string());
227 }
228
229 if out.is_empty() {
230 vec!["".to_string()]
231 } else {
232 out
233 }
234 }
235
236 let mut out: Vec<String> = Vec::new();
237 for line in split_html_br_lines(text) {
238 if let Some(w) = max_width_px {
239 out.extend(wrap_line(line, measurer, style, w));
240 } else {
241 out.push(line.to_string());
242 }
243 }
244 if out.is_empty() {
245 vec!["".to_string()]
246 } else {
247 out
248 }
249}
250
251pub(crate) fn wrap_svg_text_lines_by_measurement(
256 measurer: &dyn TextMeasurer,
257 text: &str,
258 style: &TextStyle,
259 max_width_px: Option<f64>,
260 break_long_words: bool,
261) -> Vec<String> {
262 const EPS_PX: f64 = 0.125;
263 let max_width_px = max_width_px.filter(|w| w.is_finite() && *w > 0.0);
264
265 fn measure_w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
266 measurer.measure(s, style).width
267 }
268
269 fn split_token_to_width_px(
270 measurer: &dyn TextMeasurer,
271 style: &TextStyle,
272 tok: &str,
273 max_width_px: f64,
274 ) -> (String, String) {
275 if max_width_px <= 0.0 {
276 return (tok.to_string(), String::new());
277 }
278 let chars = tok.chars().collect::<Vec<_>>();
279 if chars.is_empty() {
280 return (String::new(), String::new());
281 }
282
283 let mut split_at = 1usize;
284 for i in 1..=chars.len() {
285 let head = chars[..i].iter().collect::<String>();
286 let w = measure_w_px(measurer, style, &head);
287 if w.is_finite() && w <= max_width_px + EPS_PX {
288 split_at = i;
289 } else {
290 break;
291 }
292 }
293 let head = chars[..split_at].iter().collect::<String>();
294 let tail = chars[split_at..].iter().collect::<String>();
295 (head, tail)
296 }
297
298 fn wrap_line_to_width_px(
299 measurer: &dyn TextMeasurer,
300 style: &TextStyle,
301 line: &str,
302 max_width_px: f64,
303 break_long_words: bool,
304 ) -> Vec<String> {
305 let mut tokens =
306 std::collections::VecDeque::from(DeterministicTextMeasurer::split_line_to_words(line));
307 let mut out: Vec<String> = Vec::new();
308 let mut cur = String::new();
309
310 while let Some(tok) = tokens.pop_front() {
311 if cur.is_empty() && tok == " " {
312 continue;
313 }
314
315 let candidate = format!("{cur}{tok}");
316 let candidate_trimmed = candidate.trim_end();
317 if measure_w_px(measurer, style, candidate_trimmed) <= max_width_px + EPS_PX {
318 cur = candidate;
319 continue;
320 }
321
322 if !cur.trim().is_empty() {
323 out.push(cur.trim_end().to_string());
324 cur.clear();
325 tokens.push_front(tok);
326 continue;
327 }
328
329 if tok == " " {
330 continue;
331 }
332
333 if measure_w_px(measurer, style, tok.as_str()) <= max_width_px + EPS_PX {
334 cur = tok;
335 continue;
336 }
337
338 if !break_long_words {
339 out.push(tok);
340 continue;
341 }
342
343 let (head, tail) = split_token_to_width_px(measurer, style, &tok, max_width_px);
344 out.push(head);
345 if !tail.is_empty() {
346 tokens.push_front(tail);
347 }
348 }
349
350 if !cur.trim().is_empty() {
351 out.push(cur.trim_end().to_string());
352 }
353
354 if out.is_empty() {
355 vec!["".to_string()]
356 } else {
357 out
358 }
359 }
360
361 let mut lines = Vec::new();
362 for line in DeterministicTextMeasurer::normalized_text_lines(text) {
363 if let Some(w) = max_width_px {
364 lines.extend(wrap_line_to_width_px(
365 measurer,
366 style,
367 &line,
368 w,
369 break_long_words,
370 ));
371 } else {
372 lines.push(line);
373 }
374 }
375
376 if lines.is_empty() {
377 vec!["".to_string()]
378 } else {
379 lines
380 }
381}
382
383pub fn split_html_br_lines(text: &str) -> Vec<&str> {
389 let b = text.as_bytes();
390 let mut parts: Vec<&str> = Vec::new();
391 let mut start = 0usize;
392 let mut i = 0usize;
393 while i + 3 < b.len() {
394 if b[i] != b'<' {
395 i += 1;
396 continue;
397 }
398 let b1 = b[i + 1];
399 let b2 = b[i + 2];
400 if !matches!(b1, b'b' | b'B') || !matches!(b2, b'r' | b'R') {
401 i += 1;
402 continue;
403 }
404 let mut j = i + 3;
405 while j < b.len() && matches!(b[j], b' ' | b'\t' | b'\r' | b'\n') {
406 j += 1;
407 }
408 if j < b.len() && b[j] == b'/' {
409 j += 1;
410 }
411 if j < b.len() && b[j] == b'>' {
412 parts.push(&text[start..i]);
413 start = j + 1;
414 i = start;
415 continue;
416 }
417 i += 1;
418 }
419 parts.push(&text[start..]);
420 parts
421}
422
423pub fn wrap_label_like_mermaid_lines(
428 label: &str,
429 measurer: &dyn TextMeasurer,
430 style: &TextStyle,
431 max_width_px: f64,
432) -> Vec<String> {
433 if label.is_empty() {
434 return Vec::new();
435 }
436 if !max_width_px.is_finite() || max_width_px <= 0.0 {
437 return vec![label.to_string()];
438 }
439
440 if split_html_br_lines(label).len() > 1 {
442 return split_html_br_lines(label)
443 .into_iter()
444 .map(|s| s.to_string())
445 .collect();
446 }
447
448 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
449 measurer
451 .measure_svg_simple_text_bbox_width_px(s, style)
452 .round()
453 }
454
455 fn break_string_like_mermaid(
456 word: &str,
457 max_width_px: f64,
458 measurer: &dyn TextMeasurer,
459 style: &TextStyle,
460 ) -> (Vec<String>, String) {
461 let chars: Vec<char> = word.chars().collect();
462 let mut lines: Vec<String> = Vec::new();
463 let mut current = String::new();
464 for (idx, ch) in chars.iter().enumerate() {
465 let next_line = format!("{current}{ch}");
466 let line_w = w_px(measurer, style, &next_line);
467 if line_w >= max_width_px {
468 let is_last = idx + 1 == chars.len();
469 if is_last {
470 lines.push(next_line);
471 } else {
472 lines.push(format!("{next_line}-"));
473 }
474 current.clear();
475 } else {
476 current = next_line;
477 }
478 }
479 (lines, current)
480 }
481
482 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
484 if words.is_empty() {
485 return vec![label.to_string()];
486 }
487
488 let mut completed: Vec<String> = Vec::new();
489 let mut next_line = String::new();
490 for (idx, word) in words.iter().enumerate() {
491 let word_len = w_px(measurer, style, &format!("{word} "));
492 let next_len = w_px(measurer, style, &next_line);
493 if word_len > max_width_px {
494 let (hyphenated, remaining) =
495 break_string_like_mermaid(word, max_width_px, measurer, style);
496 completed.push(next_line.clone());
497 completed.extend(hyphenated);
498 next_line = remaining;
499 } else if next_len + word_len >= max_width_px {
500 completed.push(next_line.clone());
501 next_line = (*word).to_string();
502 } else if next_line.is_empty() {
503 next_line = (*word).to_string();
504 } else {
505 next_line.push(' ');
506 next_line.push_str(word);
507 }
508
509 let is_last = idx + 1 == words.len();
510 if is_last {
511 completed.push(next_line.clone());
512 }
513 }
514
515 completed.into_iter().filter(|l| !l.is_empty()).collect()
516}
517
518pub fn wrap_label_like_mermaid_lines_relaxed(
524 label: &str,
525 measurer: &dyn TextMeasurer,
526 style: &TextStyle,
527 max_width_px: f64,
528) -> Vec<String> {
529 if label.is_empty() {
530 return Vec::new();
531 }
532 if !max_width_px.is_finite() || max_width_px <= 0.0 {
533 return vec![label.to_string()];
534 }
535
536 if split_html_br_lines(label).len() > 1 {
537 return split_html_br_lines(label)
538 .into_iter()
539 .map(|s| s.to_string())
540 .collect();
541 }
542
543 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
544 measurer.measure(s, style).width.round()
545 }
546
547 fn break_string_like_mermaid(
548 word: &str,
549 max_width_px: f64,
550 measurer: &dyn TextMeasurer,
551 style: &TextStyle,
552 ) -> (Vec<String>, String) {
553 let chars: Vec<char> = word.chars().collect();
554 let mut lines: Vec<String> = Vec::new();
555 let mut current = String::new();
556 for (idx, ch) in chars.iter().enumerate() {
557 let next_line = format!("{current}{ch}");
558 let line_w = w_px(measurer, style, &next_line);
559 if line_w >= max_width_px {
560 let is_last = idx + 1 == chars.len();
561 if is_last {
562 lines.push(next_line);
563 } else {
564 lines.push(format!("{next_line}-"));
565 }
566 current.clear();
567 } else {
568 current = next_line;
569 }
570 }
571 (lines, current)
572 }
573
574 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
575 if words.is_empty() {
576 return vec![label.to_string()];
577 }
578
579 let mut completed: Vec<String> = Vec::new();
580 let mut next_line = String::new();
581 for (idx, word) in words.iter().enumerate() {
582 let word_len = w_px(measurer, style, &format!("{word} "));
583 let next_len = w_px(measurer, style, &next_line);
584 if word_len > max_width_px {
585 let (hyphenated, remaining) =
586 break_string_like_mermaid(word, max_width_px, measurer, style);
587 completed.push(next_line.clone());
588 completed.extend(hyphenated);
589 next_line = remaining;
590 } else if next_len + word_len >= max_width_px {
591 completed.push(next_line.clone());
592 next_line = (*word).to_string();
593 } else if next_line.is_empty() {
594 next_line = (*word).to_string();
595 } else {
596 next_line.push(' ');
597 next_line.push_str(word);
598 }
599
600 let is_last = idx + 1 == words.len();
601 if is_last {
602 completed.push(next_line.clone());
603 }
604 }
605
606 completed.into_iter().filter(|l| !l.is_empty()).collect()
607}
608
609pub fn wrap_label_like_mermaid_lines_floored_bbox(
615 label: &str,
616 measurer: &dyn TextMeasurer,
617 style: &TextStyle,
618 max_width_px: f64,
619) -> Vec<String> {
620 if label.is_empty() {
621 return Vec::new();
622 }
623 if !max_width_px.is_finite() || max_width_px <= 0.0 {
624 return vec![label.to_string()];
625 }
626
627 if split_html_br_lines(label).len() > 1 {
628 return split_html_br_lines(label)
629 .into_iter()
630 .map(|s| s.to_string())
631 .collect();
632 }
633
634 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
635 measurer
636 .measure_svg_simple_text_bbox_width_px(s, style)
637 .floor()
638 }
639
640 fn break_string_like_mermaid(
641 word: &str,
642 max_width_px: f64,
643 measurer: &dyn TextMeasurer,
644 style: &TextStyle,
645 ) -> (Vec<String>, String) {
646 let chars: Vec<char> = word.chars().collect();
647 let mut lines: Vec<String> = Vec::new();
648 let mut current = String::new();
649 for (idx, ch) in chars.iter().enumerate() {
650 let next_line = format!("{current}{ch}");
651 let line_w = w_px(measurer, style, &next_line);
652 if line_w >= max_width_px {
653 let is_last = idx + 1 == chars.len();
654 if is_last {
655 lines.push(next_line);
656 } else {
657 lines.push(format!("{next_line}-"));
658 }
659 current.clear();
660 } else {
661 current = next_line;
662 }
663 }
664 (lines, current)
665 }
666
667 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
668 if words.is_empty() {
669 return vec![label.to_string()];
670 }
671
672 let mut completed: Vec<String> = Vec::new();
673 let mut next_line = String::new();
674 for (idx, word) in words.iter().enumerate() {
675 let word_len = w_px(measurer, style, &format!("{word} "));
676 let next_len = w_px(measurer, style, &next_line);
677 if word_len > max_width_px {
678 let (hyphenated, remaining) =
679 break_string_like_mermaid(word, max_width_px, measurer, style);
680 completed.push(next_line.clone());
681 completed.extend(hyphenated);
682 next_line = remaining;
683 } else if next_len + word_len >= max_width_px {
684 completed.push(next_line.clone());
685 next_line = (*word).to_string();
686 } else if next_line.is_empty() {
687 next_line = (*word).to_string();
688 } else {
689 next_line.push(' ');
690 next_line.push_str(word);
691 }
692
693 let is_last = idx + 1 == words.len();
694 if is_last {
695 completed.push(next_line.clone());
696 }
697 }
698
699 completed.into_iter().filter(|l| !l.is_empty()).collect()
700}