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 fn split_html_br_lines(text: &str) -> Vec<&str> {
257 let b = text.as_bytes();
258 let mut parts: Vec<&str> = Vec::new();
259 let mut start = 0usize;
260 let mut i = 0usize;
261 while i + 3 < b.len() {
262 if b[i] != b'<' {
263 i += 1;
264 continue;
265 }
266 let b1 = b[i + 1];
267 let b2 = b[i + 2];
268 if !matches!(b1, b'b' | b'B') || !matches!(b2, b'r' | b'R') {
269 i += 1;
270 continue;
271 }
272 let mut j = i + 3;
273 while j < b.len() && matches!(b[j], b' ' | b'\t' | b'\r' | b'\n') {
274 j += 1;
275 }
276 if j < b.len() && b[j] == b'/' {
277 j += 1;
278 }
279 if j < b.len() && b[j] == b'>' {
280 parts.push(&text[start..i]);
281 start = j + 1;
282 i = start;
283 continue;
284 }
285 i += 1;
286 }
287 parts.push(&text[start..]);
288 parts
289}
290
291pub fn wrap_label_like_mermaid_lines(
296 label: &str,
297 measurer: &dyn TextMeasurer,
298 style: &TextStyle,
299 max_width_px: f64,
300) -> Vec<String> {
301 if label.is_empty() {
302 return Vec::new();
303 }
304 if !max_width_px.is_finite() || max_width_px <= 0.0 {
305 return vec![label.to_string()];
306 }
307
308 if split_html_br_lines(label).len() > 1 {
310 return split_html_br_lines(label)
311 .into_iter()
312 .map(|s| s.to_string())
313 .collect();
314 }
315
316 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
317 measurer
319 .measure_svg_simple_text_bbox_width_px(s, style)
320 .round()
321 }
322
323 fn break_string_like_mermaid(
324 word: &str,
325 max_width_px: f64,
326 measurer: &dyn TextMeasurer,
327 style: &TextStyle,
328 ) -> (Vec<String>, String) {
329 let chars: Vec<char> = word.chars().collect();
330 let mut lines: Vec<String> = Vec::new();
331 let mut current = String::new();
332 for (idx, ch) in chars.iter().enumerate() {
333 let next_line = format!("{current}{ch}");
334 let line_w = w_px(measurer, style, &next_line);
335 if line_w >= max_width_px {
336 let is_last = idx + 1 == chars.len();
337 if is_last {
338 lines.push(next_line);
339 } else {
340 lines.push(format!("{next_line}-"));
341 }
342 current.clear();
343 } else {
344 current = next_line;
345 }
346 }
347 (lines, current)
348 }
349
350 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
352 if words.is_empty() {
353 return vec![label.to_string()];
354 }
355
356 let mut completed: Vec<String> = Vec::new();
357 let mut next_line = String::new();
358 for (idx, word) in words.iter().enumerate() {
359 let word_len = w_px(measurer, style, &format!("{word} "));
360 let next_len = w_px(measurer, style, &next_line);
361 if word_len > max_width_px {
362 let (hyphenated, remaining) =
363 break_string_like_mermaid(word, max_width_px, measurer, style);
364 completed.push(next_line.clone());
365 completed.extend(hyphenated);
366 next_line = remaining;
367 } else if next_len + word_len >= max_width_px {
368 completed.push(next_line.clone());
369 next_line = (*word).to_string();
370 } else if next_line.is_empty() {
371 next_line = (*word).to_string();
372 } else {
373 next_line.push(' ');
374 next_line.push_str(word);
375 }
376
377 let is_last = idx + 1 == words.len();
378 if is_last {
379 completed.push(next_line.clone());
380 }
381 }
382
383 completed.into_iter().filter(|l| !l.is_empty()).collect()
384}
385
386pub fn wrap_label_like_mermaid_lines_relaxed(
392 label: &str,
393 measurer: &dyn TextMeasurer,
394 style: &TextStyle,
395 max_width_px: f64,
396) -> Vec<String> {
397 if label.is_empty() {
398 return Vec::new();
399 }
400 if !max_width_px.is_finite() || max_width_px <= 0.0 {
401 return vec![label.to_string()];
402 }
403
404 if split_html_br_lines(label).len() > 1 {
405 return split_html_br_lines(label)
406 .into_iter()
407 .map(|s| s.to_string())
408 .collect();
409 }
410
411 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
412 measurer.measure(s, style).width.round()
413 }
414
415 fn break_string_like_mermaid(
416 word: &str,
417 max_width_px: f64,
418 measurer: &dyn TextMeasurer,
419 style: &TextStyle,
420 ) -> (Vec<String>, String) {
421 let chars: Vec<char> = word.chars().collect();
422 let mut lines: Vec<String> = Vec::new();
423 let mut current = String::new();
424 for (idx, ch) in chars.iter().enumerate() {
425 let next_line = format!("{current}{ch}");
426 let line_w = w_px(measurer, style, &next_line);
427 if line_w >= max_width_px {
428 let is_last = idx + 1 == chars.len();
429 if is_last {
430 lines.push(next_line);
431 } else {
432 lines.push(format!("{next_line}-"));
433 }
434 current.clear();
435 } else {
436 current = next_line;
437 }
438 }
439 (lines, current)
440 }
441
442 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
443 if words.is_empty() {
444 return vec![label.to_string()];
445 }
446
447 let mut completed: Vec<String> = Vec::new();
448 let mut next_line = String::new();
449 for (idx, word) in words.iter().enumerate() {
450 let word_len = w_px(measurer, style, &format!("{word} "));
451 let next_len = w_px(measurer, style, &next_line);
452 if word_len > max_width_px {
453 let (hyphenated, remaining) =
454 break_string_like_mermaid(word, max_width_px, measurer, style);
455 completed.push(next_line.clone());
456 completed.extend(hyphenated);
457 next_line = remaining;
458 } else if next_len + word_len >= max_width_px {
459 completed.push(next_line.clone());
460 next_line = (*word).to_string();
461 } else if next_line.is_empty() {
462 next_line = (*word).to_string();
463 } else {
464 next_line.push(' ');
465 next_line.push_str(word);
466 }
467
468 let is_last = idx + 1 == words.len();
469 if is_last {
470 completed.push(next_line.clone());
471 }
472 }
473
474 completed.into_iter().filter(|l| !l.is_empty()).collect()
475}
476
477pub fn wrap_label_like_mermaid_lines_floored_bbox(
483 label: &str,
484 measurer: &dyn TextMeasurer,
485 style: &TextStyle,
486 max_width_px: f64,
487) -> Vec<String> {
488 if label.is_empty() {
489 return Vec::new();
490 }
491 if !max_width_px.is_finite() || max_width_px <= 0.0 {
492 return vec![label.to_string()];
493 }
494
495 if split_html_br_lines(label).len() > 1 {
496 return split_html_br_lines(label)
497 .into_iter()
498 .map(|s| s.to_string())
499 .collect();
500 }
501
502 fn w_px(measurer: &dyn TextMeasurer, style: &TextStyle, s: &str) -> f64 {
503 measurer
504 .measure_svg_simple_text_bbox_width_px(s, style)
505 .floor()
506 }
507
508 fn break_string_like_mermaid(
509 word: &str,
510 max_width_px: f64,
511 measurer: &dyn TextMeasurer,
512 style: &TextStyle,
513 ) -> (Vec<String>, String) {
514 let chars: Vec<char> = word.chars().collect();
515 let mut lines: Vec<String> = Vec::new();
516 let mut current = String::new();
517 for (idx, ch) in chars.iter().enumerate() {
518 let next_line = format!("{current}{ch}");
519 let line_w = w_px(measurer, style, &next_line);
520 if line_w >= max_width_px {
521 let is_last = idx + 1 == chars.len();
522 if is_last {
523 lines.push(next_line);
524 } else {
525 lines.push(format!("{next_line}-"));
526 }
527 current.clear();
528 } else {
529 current = next_line;
530 }
531 }
532 (lines, current)
533 }
534
535 let words: Vec<&str> = label.split(' ').filter(|w| !w.is_empty()).collect();
536 if words.is_empty() {
537 return vec![label.to_string()];
538 }
539
540 let mut completed: Vec<String> = Vec::new();
541 let mut next_line = String::new();
542 for (idx, word) in words.iter().enumerate() {
543 let word_len = w_px(measurer, style, &format!("{word} "));
544 let next_len = w_px(measurer, style, &next_line);
545 if word_len > max_width_px {
546 let (hyphenated, remaining) =
547 break_string_like_mermaid(word, max_width_px, measurer, style);
548 completed.push(next_line.clone());
549 completed.extend(hyphenated);
550 next_line = remaining;
551 } else if next_len + word_len >= max_width_px {
552 completed.push(next_line.clone());
553 next_line = (*word).to_string();
554 } else if next_line.is_empty() {
555 next_line = (*word).to_string();
556 } else {
557 next_line.push(' ');
558 next_line.push_str(word);
559 }
560
561 let is_last = idx + 1 == words.len();
562 if is_last {
563 completed.push(next_line.clone());
564 }
565 }
566
567 completed.into_iter().filter(|l| !l.is_empty()).collect()
568}